Clearing all observable bindings in Knockout - javascript

I'm working on a control panel application right now, where each tool loads its own Javascript file, most of which contain some Knockout bindings. Knockout itself is being loaded in the document head, but tools are loaded asynchronous into a #body div, so my concern is that elements will continue to be bound, even after a different tool is loaded. I assume this would result in memory leaks and probably some glitches, if the same element is bound multiple times. Is it possible to clear all Knockout bindings at once, before I load a new tool?

The general pattern that I would recommend is something like:
//obviously doesn't have to be an object literal
var viewModel = {
currentTool: ko.observable()
};
ko.applyBindings(viewModel);
Then, bind your page like:
<div data-bind="with: currentTool">
...content here
</div>
Now, when the page is initially bound, the area will not be rendered as currentTool is undefined, but KO will copy off the children to use as a "template".
When you populate the currentTool observable, it will render a copy of the elements and bind the content.
When you change currentTool, then KO will clean up the existing bindings and elements, and render/bind a new copy of the elements.
So, you only call ko.applyBindings once and continue to update currentTool based on what you want to display.

Related

Why does knockout template binding stop working after manually reordering - and reverting - dom items?

I am using a knockout foreach (more specifically, template: { foreach: items }) binding to display a list of elements.
I then proceed to take the following actions:
Swap the first and second elements of the observable array. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Now, swap the first and second DOM elements. I see the changes reflected on screen, as expected.
Repeat the previous action to revert to the initial state. Again, this works as expected.
Even though we have manually tampered with the DOM, we have reverted to exactly the initial state, without invoking knockout during the DOM tampering. This means the state is restored to the last time knockout was aware of it, so it should look to knockout as if nothing ever changed to begin with.
However, if I perform the first action again, that is, swap the first two elements in the array, the changes are not reflected on screen.
Here is a jsfiddle to illustrate the problem: https://jsfiddle.net/k7u5wep9/.
I know that manually tampering with the DOM managed by knockout is a bad idea and that it can lead to undefined behaviour. This is unfortunately unavoidable in my situation due to third party code. What stumps me is that, even after reverting the manual edits to the exact initial state, knockout still does not work as expected.
My question is: what causes this behaviour?
And then, how does one work around it?
Turns out there is nothing magical happening here. The mistake I made was to only consider elements instead of all nodes. The knockout template binding keeps a record of all nodes when reordering, not just elements.
Before manually editing the DOM, the child nodes of the template binding are:
NodeList(6) [text, div, text, text, div, text].
After manually swapping the first two elements using parent.insertBefore(parent.children[1], parent.children[0]), this turns into:
NodeList(6) [text, div, div, text, text, text].
Repeating the action yields:
NodeList(6) [text, div, div, text, text, text].
Although this is identical to the initial state when only referring to elements, it is quite different when referring to all nodes.
The solution now becomes clear. One way to perform a proper manual swap is to replace
parent.insertBefore(parent.children[1], parent.children[0]);
with
let nexts = [parent.children[0].nextSibling, parent.children[1].nextSibling];
parent.insertBefore(parent.children[1], nexts[0]);
parent.insertBefore(parent.children[0], nexts[1]);
as seen in https://jsfiddle.net/k7u5wep9/2/.
Obviously more care has to be taken when there are no text nodes before/after, but the idea remains the same.
By manipulating the DOM, you have broken the binding made.
Do not manipulate directly the DOM. Knockout will not detect the changes made.
If you put a with: items around your foreach, it at least keeps working but requires double click if dom order != array order .. might get you on track atleast, maybe you can re-order the ko-array inside the dom function to keep their 'orders' in sync?
let vm = {
items: ko.observableArray(['item1', 'item2']),
reorder_array() {
vm.items([vm.items()[1], vm.items()[0]]);
},
reorder_dom() {
let parent = document.querySelector('#items');
parent.insertBefore(parent.children[1], parent.children[0]);
vm.reorder_array();
}
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="with: items">
<div id="items" data-bind="template: { foreach: $data }">
<div data-bind="text: $data"></div>
</div>
</div>
<button data-bind="click: reorder_array">Reorder array</button>
<button data-bind="click: reorder_dom">Reorder DOM</button>
<div>
Reorder the array twice, then reorder DOM twice. This should work as expected, and end up with the initial state. Then, try to reorder the array again. It should not work. Why?
</div>

Knockout template and unique ID within complex web app causing problems

Knockout templating system is great, however in a web app where there are several separated contexts ("views") that are loaded by ajax, one issue appears:
Templates rely on ID
This means that if my chance you have one template with the same name on a view that on another view loaded previously and still existing in the webapp context, knockout (because the browser does this) will take the first matching #templateId element.
On our webapp, we eliminated the ID of all our elements, and when it really needs to be used, it's an ID that is javascript determined to not have duplicates.
Some views can be loaded multiple times in the lifetime of the app, so
no, we can't say "simply check if the id is already used before making your html code" to our team members.
The only other thing we could do would be to check if a specific template is loaded, and if not load it in async, then apply bindings. But for simplicity purpose and the way our project is set up right now, we can't apply an js AMD-like dependency manager.
Questions
Is that possible to specify directly the DOM reference to the template directly?
data-bind="template:function(){ return $('yourSelectorToTheTemplate')[0]; }"
I've looked knockout code and it's weird because we have this:
templateDocument = templateDocument || document;
var elem = templateDocument.getElementById(template);
if (!elem)
throw new Error("Cannot find template with ID " + template);
return new ko.templateSources.domElement(elem);
This means that it really use the DOM element, so why being forced to give an ID for it if we already have the ID?
How do we retrieve dynamically applied IDtemplate, that is also calling another dynamically applied ID (template recursively calling itself for example)?
Setting ID from a binding handler may be wrong: it may set the ID after other data bound elements referred to it, but it would be simpler to have to.
The best solution found for the moment:
Place templates (script element) at the top of the html view.
Use a bindingHandler that does initialization (I called mine "init" as you can see in the example below) to set the ID of the script element
Store that ID inside the $root context so it can be reused by other elements
The result looks like this:
<script type="text/html" data-bind="init: function(){ $rawData.folderItemTemplate = functionThatSetsAndReturnsUniqueId($element, 'folderItemTemplate'); }">
<li>
Some item
<ul data-bind="template: { name: $rawData.folderItemTemplate, foreach: children }"></ul>
</li>
</script>
As you can see we can use this template binding with template: { name: $rawData.yourPropertyName, foreach:... }

Exclude DOM elements from knockout apply binding?

I want to target my knockout viewmodel to certain section of the dom as thus:
ko.applyBindings(MyViewModel,$('#Target')[0]);
However I do NOT want it to apply to all the doms below it. The reason for this is that the whole SPA thing isn't working very well - can't keep up with the jumbo sized viewmodel that results from including every potential interaction into one giant object. Hence, the page is composed of multiple partial views. I want each partials to instantiate its own ViewModel and provide interface for the parent to interact with.
Some sample dom
<div id="Target">
<!--Everything here should be included except-->
<div data-bind="DoNotBindBelowThis:true">
<!--Everything here should NOT be included by the first binding,
I will specifically fill in the binding with targetted
ApplyBind eg. ko.applyBindings(MyOtherViewModel, $('#MyOtherTarget')[0])
to fill the gaps-->
<div id="MyOtherTarget">
</div>
</div>
</div>
Again how can I exclude an entire dom tree below the div tagged with DoNotBindBelowThis from applyBindings?
Take a look at the blog post here: http://www.knockmeout.net/2012/05/quick-tip-skip-binding.html
Basically, you can create a custom binding like:
ko.bindingHandlers.DoNotBindBelowThis = {
init: function() {
return { controlsDescendantBindings: true };
}
};

backbone not rendering the $el but can reference element directly

This is killing me, being reading the examples on this site but can't figure out why it works like this.
I want to pass back values to my view, which has buttons that you can use to change the values.
If I use the following
this.$el.empty().html(view.el)
View.el contains the correct html, but those not render on the screen. If I use the following
$("#handicap").html( view.el);
The values get displayed on screen but the events no longer get picked up eventhough if I put an onclick function in the html code it kicks off.
Ideally I would like to get this.$el.empty().html(view.el) working. It has to do with context but can't see why.
I have created a jsbin here http://jsbin.com/iritex/1/edit
If I have to use $("#handicap").html( view.el), do I need to do something special to unbind events. I have tried undelegate everything but that didn't do the trick either.
thanks
A Backbone View's el property will always contain a reference to a valid DOM object. However, that DOM object may or may not be in your display tree. It's up to you to make sure it's in the display tree when you need it to be. This functionality lets Backbone maintain the state of it's View element without it being rendered to the screen. You can add and remove a view from the screen efficiently, for example.
There are a few ways to get your View's element into the display tree.
1) Associate the view with an existing DOM element on the page by passing in a jquery selector to the initializer as the "el" property.
var view = new MyView({el: '#MyElementSelector'});
2) Associate the view with an existing DOM element on the page by hardcoding the jQuery selector it into the view's "el" property.
var MyView = Backbone.View.extend({
el: '#MyElementSelector'
});
3) Render it to the page from within another view
var view = new MyView();
view.render();
this.$el.empty().html(view.el);
If you're interested, I show examples in a Backbone Demo I put together.
You need to put both views into the DOM. Wherever you create the view that above is this needs to be inserted into the DOM. If you do that, then the first line will work fine this.$el.empty().html(view.el).

Ember.js: How do I hook Ember.CollectionView after every child view is rendered?

This question demonstrates that overriding an Ember.View instance's didInsertElement allows you to execute some code after the view's element is in the DOM.
http://jsfiddle.net/gvUux/2/
Naturally, overriding didInsertElement on the child view class you add to an Ember.CollectionView will run the hook after each child view is rendered and inserted.
http://jsfiddle.net/BFUvK/1/
Two collection-oriented hooks on Ember.CollectionView, arrayDidChange and contentDidChange, execute after the underlying content has changed, but they execute before any rendering takes place. arrayDidChange is executed for every element added to the array, and contentDidChange wraps the content binding.
I would like to be able to hook around the rendering pipeline, something like willInsertCollection and didInsertCollection, to manipulate the DOM before and after all child elements are rendered - essentially, before and after filters around contentBinding.
Any ideas? I'm stumped.
If you want to want to do something before and/or after a view has been rendered you should use willInsertElement and/or didInsertElement respectively. In this case, since you want "to manipulate the DOM before and after all child elements are rendered" you should call those on your CollectionView.
I'm not quite sure what you mean by "before and after filters around contentBinding", so if this doesn't answer your question if you could clarify I'd be happy to help.
jsFiddle if needed
I wanted to apply a scroll animation to slide a list up after pushing new objects. The list was rendered using an ArrayController and the #each helper. Simply triggering an event on the controller which the view subscribed to after pushing objects was causing the animation to execute before the changes to the content were actually rendered. The following technique worked perfectly for me.
//excerpt from my loadMore method on the ArrayController
var self = this;
self.content.pushObjects(moreItems);
Ember.run.scheduleOnce('afterRender', this, function()
{
self.trigger('loadMoreComplete');
});

Categories

Resources