knockout.js, breeze and dynatree/fancytree - javascript

This is really tricky to get my head around as I'm not used to this style of programming/data management.
All I'm trying to do at the moment is pass a json object returned via breeze into a dynatree or fancytree.
The examples that exist online all assume that the tree will do the ajax call via "initajax" or that some weirdly convoluted custom binding handler is needed into which various objects are passed:
ko.bindingHandlers.dynatree = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
setTimeout(function () {
$(element).dynatree({
noLink: true, // noLink is required to 'unlock' the checkboxes
minExpandLevel: 2
})
// the timeout value shows the correct knockout bindings BEFORE dynatree kicks in.
}, 1000);
}
}
This all seems too complicated to me, surely? I already have the json object, I know that's working. If I use knockout to "foreach" bind it to some plain html then all data is displayed just fine. In my mind all I need to do is initialize the tree div and pass it the json object... It's just that I have no idea how to do that!
I've tried using the jsfiddle here: http://jsfiddle.net/Ebram/UhA3m/5/ but chrome developer tools complain about the element having no "dynatree" method when the custom binding handler fires. It's passing in a "ul" element and that could be the problem - surely it should be passing in the tree div, not the ul element?
Anyhow, if anyone could point me in the right direction I'd hugely appreciate it. As I'm using John Papa's SPA methodology, I'm also unsure as to where I would put any separate js initialization code as the underlying viewmodel isn't the right place for me to be doing a $(#tree).dynatree initialization type call, is it? I must admit I've not got my head around this yet.
I suppose all I'm looking for is something along the lines of "once the viewmodel for this view has finished loading and the knockout binding is being done, initialize the dynatree div and pass this json object to the tree" if that makes sense in pseudocode?

I can hopefully point you in the approximate right direction.
It seems dynatree can also take JSON from a file as well as an AJAX request. In this example Lazy Loading, if you look in the source code, there's:
// In real life we would call a URL on the server like this:
...
// .. but here we use a local file instead:
Storing your data in a file to get it in seems awfully wasteful. Now that we know it's a little flexible in what it gets, let's see where it uses the data and maybe we can get it to use a local variable instead. let see where it loads it
Looking in the dynatree source, there's a function associated with appendAjax. (line 1774 in my source.) A little short on time at the moment, but I'd find where it gets the JSON and what it does with it. Perhaps you can do the same thing outside or mod the handling of ajaxOptions to take a variable with the JSON.

Related

Running client side javascript without loading a new (blank) view on Odoo 8

I need to run some client-side javascript from a button in a form view in Odoo 8. This button runs a python method which returns this dictionary:
{"type": "ir.actions.client",
"tag": "my_module.do_something",}
do_something is defined in a .js file as follows:
openerp.my_module = function (instance) {
instance.web.client_actions.add("my_module.do_something", "instance.my_module.Action");
instance.my_module.Action = instance.web.Widget.extend({
init: function() {
// Do a lot of nice things here
}
});
};
Now, the javascript is loaded and executed properly, but even before launching the init function, Odoo loads a brand new, blank view, and once the javascript is over I can't get browse any other menu entry. In fact, wherever I click I get this error:
Uncaught TypeError: Cannot read property 'callbackList' of undefined
What I need instead is to run the javascript from the form view where the button belongs, without loading a new view, so both getting the javascript stuff done and leaving all callbacks and the whole environment in a good state. My gut feeling is that I shouldn't override the init funcion (or maybe the whole thing is broken, I'm quite new to Odoo client-side js) , but I couldn't find docs neither a good example to call js the way I want. Any idea to get that?
Sorry, I don't work on v8 since a lot of time and I don't remember how to add that, but this might help you: https://github.com/odoo/odoo/blob/8.0/doc/howtos/web.rst
Plus, if you search into v8 code base you can find some occurence of client actions in web module docs https://github.com/odoo/odoo/search?utf8=%E2%9C%93&q=instance.web.client_actions.add
Thanks to the pointers simahawk posted in another answer, I have been able to fix my js, which is now doing exactly what I needed. For your reference, the code is as follows:
openerp.my_module = function (instance) {
instance.web.client_actions.add("my_module.do_something", "instance.my_module.action");
instance.my_module.action = function (parent, action) {
// Do a lot of nice things here
}
};

restangular save ignores changes to restangular object

I'm trying to call save on a restangularized object, but the save method is completely ignoring any changes made to the object, it seems to have bound the original unmodified object.
When I run this in the debugger I see that when my saveSkill method (see below) is entered right before I call save on it the skill object will reflect the changes I made to it's name and description fields. If I then do a "step into" I go into Restangular.save method. However, the 'this' variable within the restangular.save method has my old skill, with the name and description equal to whatever they were when loaded. It's ignoring the changes I made to my skill.
The only way I could see this happening is if someone called bind on the save, though I can't why rectangular would do that? My only guess is it's due to my calling $object, but I can't find much in way of documentation to confirm this.
I'm afraid I can't copy and paste, all my code examples are typed by hand so forgive any obvious syntax issues as typos. I don't know who much I need to describe so here is the shortened version, I can retype more if needed:
state('skill.detail', {
url: '/:id',
data: {pageTitle: 'Skill Detail'},
tempalte: 'template.tpl.html'
controller: 'SkillFormController',
resolve: {
isCreate: (function(){ return false;},
skill: function(SkillService, $stateParams){
return SkillService.get($stateParams.id, {"$expand": "people"}).$object;
},
});
my SkillService looks like this:
angular.module('project.skill').('SkillService', ['Restangular, function(Retangular) {
var route="skills";
var SkillService= Restangular.all(route);
SkillService.restangularize= function(element, parent) {
var skill=Restangular.restangluarizeElement(parent, elment, route);
return skill;
};
return SkillService;
}];
Inside of my template.tpl.html I have your standard text boxes bound to name and description, and a save button. The save button calls saveSkill(skill) of my SkillFormController which looks like this:
$scope.saveSkill=function(skill) {
skill.save().then(function returnedSkill) {
toaster.pop('success', "YES!", returnedSkill.name + " saved.");
...(other irrelevant stuff)
};
If it matters I have an addElementTransformer hook that runs a method calling skilll.addRestangularMethod() to add a getPeople method to all skill objects. I don't include the code since I doubt it's relevant, but if needed to I can elaborate on it.
I got this to work, though I honestly still don't know entirely why it works I know the fix I used.
First, as stated in comments restangular does bind all of it's methods to the original restangularizedObject. This usually works since it's simply aliasing the restangularied object, so long as you use that object your modifications will work.
This can be an issue with Restangular.copy() vs angular.copy. Restangualar.copy() makes sure to restangularize the copied object properly, rebinding restangualr methods to the new copy objects. If you call only Angular.copy() instead of Restangualar.copy() you will get results like mine above.
However, I was not doing any copy of the object (okay, I saved a master copy to revert to if cancel was hit, but that used Restangular.copy() and besides which wasn't being used in my simple save scenario).
As far as I can tell my problem was using the .$object call on the restangular promise. I walked through restangular enough to see it was doing some extra logic restangularizing methods after a promise returns, but I didn't get to the point of following the $object's logic. However, replacing the $object call with a then() function that did nothing but save the returned result has fixed my issues. If someone can explain how I would love to update this question, but I can't justify using work time to try to further hunt down a fixed problem even if I really would like to understand the cause better.

Since Meteor upgrade to 0.8.0, Template "rendered" callback is not fired when Session variable dependancy changes

I'm stuck with a problem since I upgraded to 0.8.0.
The Template rendered is not being fired anymore (except the first time).
I followed the recommendations as in:
https://github.com/avital/meteor-ui-new-rendered-callback/blob/master/new2/client/each.js
This didn't helped, and so I finally made this small piece of code (by modifying the new2 example).
The main difference is that the update is triggered by a Session variable change instead of a DB change.
This perfectly shows the problem, as rendered is fired only twice with this example:
client/each.js
Template.list.items = function () {
return (Session.get('items') || 'None')
};
var renderCount = 1;
var logRender = function () {
console.log("rendered #" + renderCount);
renderCount++;
};
Template.list.rendered = function () {
logRender();
};
Template.justName.rendered = function () {
logRender();
};
setInterval(function () {
Session.set('items', {name: Random.choice(["one", "two", "three"])});
}, 1000);
client/each.html
<body>
{{> list}}
</body>
<template name="list">
{{#with items}}
{{> justName}}
{{/with}}
</template>
<template name="justName">
{{name}}
</template>
How can I do to get the Template.justName.rendered callback properly fired when content update is triggered by a Session.set?
Thanks,
I do have an instant solution for you, but it probably requires a tiny bit of re-thinking your actual code. This is by the way the same problem as here:
Meteor 0.8.0 - Failed to operate DOM in rendered callback
But the question is posed in such a different context that it makes sense to answer it twice.
So why does it not trigger the rendered callback? Because it does not re-render.
Blaze treats the whole thing of "how to react on a changed dependencies" very differently, "better" one might say: it will identify the DOM node where your "one", "two" or "three" (in your case it's the template itself) was stored in and just replace the part that has changed, which is the text content "one", "two" or "three". The DOM node itself as well as the template stay completely intact. That also means, that everything you could have been doing with this DOM node won't have to be re-done in almost every practical scenario. I.e. if you animate it, change it's text color using jQuery, the color and animation will just stay on the screen, so you won't need the rendered callback to re-do that.
In your case, the problem is easily solved by just rearanging what you want to do on "rerender":
var whatever = function(){
// whatever you want to do on data-change, in your case calling "logRender" (which needs to be renamed with Blaze, anyway..)
logRender();
}
And then the only thing you have to do is firing it whenever your data change, either manually, like this:
setInterval(function () {
Session.set('items', {name: Random.choice(["one", "two", "three"])});
// calling the function when changing the data, knowing that it WON'T destroy the DOM node it affects
whatever();
}, 1000);
or reactively, like this:
Deps.autorun(function(){
Session.get("items"); // our dependency, just has to be there, but you can also use it
whatever(); // will be fired whenever dependency changes
});
The core idea is to eliminate the need to re-do something you did in the rendered callback, since the DOM and the identity of its objects (and all the beautiful jQuery effects) are still intact. So all that is left to re-do is something that only would depend on the particular reactive data-change, which is why there is the Deps.autorun().
In your particular example, your "logRender" function did not have any reactive dependencies, but if you add some and put it into the Deps.autorun(), it will be reliably re-run whenever the dependency changes.
As a conclusion, Meteor 0.7.x and below drove us to make the mistake of treating the "rendered" callback function as a general purpose autorun function, which is why we are running into trouble now and have to fix our apps.
As noted in the comments, this is a indeed a design change with Meteor.
Prior to Meteor 0.8, a template was a function that generated HTML. This function would be re-computed whenever any of its reactive dependencies changed, which resulted in a recreation of all the DOM nodes generated by the template (apart from any sub-templates or isolated nodes). Whenever this re-draw happened, the rendered callback was triggered.
This behavior creates quite a performance hit because it requires re-rendering of potentially a lot of HTML, including for identifiers and helpers depending on data that hadn't changed. Additionally, it made it difficult to use other libraries like jQuery to modify the DOM elements that were created, because Meteor basically had control of the entire process and the jQuery code would have to be carefully re-run each time.
Meteor 0.8 fixes this by only rendering the pieces of the DOM that have actually changed, down to the granularity of the identifiers in your template - it is much more fine-grained. As a result, the template's rendered callback is only triggered once when your template hits the page, and is never called again afterward. This solves a lot of performance issues and allows jQuery and other DOM manipulations to work seamlessly with Meteor, but also means that you won't get the automatic callback signalling when something has changed. You can, however, achieve this with helpers that use reactive variables for specific things that change.
For a more detailed listing of how Spacebars, the new Handlebars replacement, works, see https://github.com/meteor/meteor/blob/devel/packages/spacebars/README.md
See also the new documentation about the rendered callback: http://docs.meteor.com/#template_rendered
So, I was doing a lot of digging yesterday to try and figure out basically the exact same issues you are having. I am still digging, but I did come across this Devshop Talk about Integrating Other Clientside JS Libraries. In it Ted Blackman describes a package he made to trigger events when a Session variable changed. It sounds like what you need. This talk was given prior to 0.8.0 so I am not sure how the package would be effected, but it might be worth a shot.
Devshop Talk - https://www.youtube.com/watch?v=NdBPY98o6eM
Session Extras - https://atmospherejs.com/package/session-extras
Event Horizon - https://atmospherejs.com/package/event-horizon

Querying the DOM in Windows 8 Apps from within a method

I'm struggling with this even after reading the MSDN documentation and the following online guides:
Codefoster
Stephen Walter
I think my problem is easy to fix and that I just am thinking about something in the wrong way. Basically I am querying my web service and on success running the following method. I am then trying to bind the result to my listview. For now I am using a hardcoded value publicMembers.itemlistwhich has been declared at the top of the document just to make sure I can actually bind to the list before doing it with my query results. Ignore line 2 for now.
Success Method:
_lookUpSuccess: function xhrSucceed(Result) {
var response = JSON.parse(Result.responseText);
listView = document.querySelector("#termTest");
ui.setOptions(listView, {
itemDataSource: publicMembers.itemList,
itemTemplate: document.querySelector(".itemtemplate"),
})
},
Now, instead of using document.querySelector, I have also tried with WinJS.Utilities.id and WinJS.Utilities.query, neither of which worked either. This doesn't break my code and introduce an error but it doesn't bind to the listview either so I think I have an issue in querying the right DOM element. However exactly the same code does work if I add it to the top of the script, it is only when I place it in this method that it stops working.
EDIT: Also to clarify, when I debug publicMembers.itemList is storing the values I expect them to be.
Please point out if I have explained things poorly and I will try and improve my question.
Thanks.
I haven't used WinJS.UI.setOptions, but rather this other way of setting the data source. Can you see if it works?
_lookUpSuccess: function xhrSucceed(result) {
var response = JSON.parse(result.responseText);
listView = document.querySelector("#termTest");
listView.winControl.itemDataSource = publicMembers.itemList;
},
This would assume you're defining the itemTemplate as part of the data-win-options attribute of your ListView's HTML element. You could also probably just do listView.winControl.itemTemplate = document.querySelector(".itemtemplate") if you prefer to set it programmatically.

Event handling in Dojo

Taking Jeff Atwood's advice, I decided to use a JavaScript library for the very basic to-do list application I'm writing. I picked the Dojo toolkit, version 1.1.1. At first, all was fine: the drag-and-drop code I wrote worked first time, you can drag tasks on-screen to change their order of precedence, and each drag-and-drop operation calls an event handler that sends an AJAX call to the server to let it know that order has been changed.
Then I went to add in the email tracking functionality. Standard stuff: new incoming emails have a unique ID number attached to their subject line, all subsequent emails about that problem can be tracked by simply leaving that ID number in the subject when you reply. So, we have a list of open tasks, each with their own ID number, and each of those tasks has a time-ordered list of associated emails. I wanted the text of those emails to be available to the user as they were looking at their list of tasks, so I made each task box a Dijit "Tree" control - top level contains the task description, branches contain email dates, and a single "leaf" off of each of those branches contains the email text.
First problem: I wanted the tree view to be fully-collapsed by default. After searching Google quite extensively, I found a number of solutions, all of which seemed to be valid for previous versions of Dojo but not the one I was using. I eventually figured out that the best solution would seem to be to have a event handler called when the Tree control had loaded that simply collapsed each branch/leaf. Unfortunately, even though the Tree control had been instantiated and its "startup" event handler called, the branches and leaves still hadn't loaded (the data was still being loaded via an AJAX call). So, I modified the system so that all email text and Tree structure is added server-side. This means the whole fully-populated Tree control is available when its startup event handler is called.
So, the startup event handler fully collapses the tree. Next, I couldn't find a "proper" way to have nice formatted text for the email leaves. I can put the email text in the leaf just fine, but any HTML gets escaped out and shows up in the web page. Cue more rummaging around Dojo's documentation (tends to be out of date, with code and examples for pre-1.0 versions) and Google. I eventually came up with the solution of getting JavaScript to go and read the SPAN element that's inside each leaf node and un-escape the escaped HTML code in it's innerHTML. I figured I'd put code to do this in with the fully-collapse-the-tree code, in the Tree control's startup event handler.
However... it turns out that the SPAN element isn't actually created until the user clicks on the expando (the little "+" symbol in a tree view you click to expand a node). Okay, fair enough - I'll add the re-formatting code to the onExpand() event handler, or whatever it's called. Which doesn't seem to exist. I've searched to documentation, I've searched Google... I'm quite possibly mis-understanding Dojo's "publish/subscribe" event handling system, but I think that mainly because there doesn't seem to be any comprehensive documentation for it anywhere (like, where do I find out what events I can subscribe to?).
So, in the end, the best solution I can come up with is to add an onClick event handler (not a "Dojo" event, but a plain JavaScript event that Dojo knows nothing about) to the expando node of each Tree branch that re-formats the HTML inside the SPAN element of each leaf. Except... when that is called, the SPAN element still doesn't exist (sometimes - other times it's been cached, just to further confuse you). Therefore, I have the event handler set up a timer that periodically calls a function that checks to see if the relevant SPAN element has turned up yet before then re-formatting it.
// An event handler called whenever a "email title" tree node is expanded.
function formatTreeNode(nodeID) {
if (dijit.byId(nodeID).getChildren().length != 0) {
clearInterval(nodeUpdateIntervalID);
messageBody = dijit.byId(nodeID).getChildren()[0].labelNode.innerHTML
if (messageBody.indexOf("<b>Message text:</b>") == -1) {
messageBody = messageBody.replace(/>/g, ">");
messageBody = messageBody.replace(/</g, "<");
messageBody = messageBody.replace(/&/g, "&");
dijit.byId(nodeID).getChildren()[0].labelNode.innerHTML = "<b>Message text:</b><div style=\"font-family:courier\">"+messageBody+"</div>";
}
}
}
// An event handler called when a tree node has been set up - we changed the default fully-expanded to fully-collapsed.
function setupTree(theTree) {
dijit.byId("tree-"+theTree).rootNode.collapse();
messageNode = dijit.byId("tree-"+theTree).rootNode.getChildren();
for (pl = 0; pl < messageNode.length; pl++) {
messageNode[pl].collapse();
messageNode[pl].expandoNode.onclick = eval("nodeUpdateIntervalID = setInterval(\"formatTreeNode('"+messageNode[pl].id+"')\",200); formatTreeNode('"+messageNode[pl].id+"');");
}
}
The above has the feel of a truly horrible hack, and I feel sure I must have taken a wrong turn somewhere early on in my thought process. Can someone please tell me:
The correct way to go about putting nicely-formatted text inside a Dojo/Dijit Tree control.
The correct way to handle Dojo events, like where I can figure out what events are available for me to subscribe to.
A better JavaScript library to use (can I do what I want to with JQuery and avoid the all-around-the-houses approach seen above?).
PS: If you're naming a software project, give thought to its name's uniqueness in Google - I'm sure searching for "Dojo" documentation in Google would be easier without all the martial arts results getting in the way.
PPS: Firefox spellchecker knows how to spell "Atwood", correcting me when I put two 'T's instead of one. Is Jeff just that famous now?
I assume that you followed the dijit.Tree and dojo.data in Dojo 1.1 tutorial which directed you to pass the data to the tree control using a data store. That had me banging my head of a brick wall for a while.
Its not really a great approach and the alternative is not really well documented. You need to create a use model instead. I have included an example below of a tree model that I created for displaying the structure of an LDAP directory.
You will find the default implementation of the model in your dojo distribution at ./dijit/_tree/model.js. The comments should help you understand the functions supported by the model.
The IDirectoryService class the code below are stubs for server-side Java POJOs generated by Direct Web Remoting (DWR). I highly recommend DWR if you going to be doing a lot of client-server interaction.
dojo.declare("LDAPDirectoryTreeModel", [ dijit.tree.model ], {
getRoot : function(onItem) {
IDirectoryService.getRoots( function(roots) {
onItem(roots[0])
});
},
mayHaveChildren : function(item) {
return true;
},
getChildren : function(parentItem, onComplete) {
IDirectoryService.getChildrenImpl(parentItem, onComplete);
},
getIdentity : function(item) {
return item.dn;
},
getLabel : function(item) {
return item.rdn;
}
});
And here is an extract from the my JSP page where I created the model and used it to populate the tree control.
<div
dojoType="LDAPDirectoryTreeModel"
jsid="treeModel"
id="treeModel">
</div>
<div
jsid="tree"
id="tree"
dojoType="dijit.Tree" model="treeModel"
labelAttr="name"
label="${directory.host}:${directory.port}">
</div>

Categories

Resources