I'm quite new to Meteor and I'm having trouble trying to understand the "rendered" event on templates.
Assuming I have this two templates:
<template name="parent">
<div id="list">
{{#each childs}}
{{> child}}
{{/each}}
</div>
</template>
<template name="child">
<div class="item">
<!-- content -->
</div>
</template>
and these two events:
Template.parent.rendered = function () {
console.log('parent');
};
Template.child.rendered = function () {
console.log('child');
};
I always get this from console:
> parent
> child
> child
> child
So basically the parent template triggers "rendered" before the inner templates have finished rendering.
Because of that I'm unable to do execute any post operations to the DOM like jquery plugins.
e.g:
Template.parent.rendered = function () {
$('#list').myplugin();
};
Since this gets executed before inner templates are rendered it breaks the plugin.
Is there a workaround or a meteor event that I can use to safely now when a template is fully rendered, including it's inner templates?
My general advice for problems like this is you should look for a way to activate your plugin after rendering each child, rather than after rendering the parent - even if it means making extra calls to the plugin. If you can do that then it also solves the related problem of what happens when more children are added sometime later (of course this may not apply in your case).
It's hard to give a precise answer without knowing more details about what your plugin does but I can give an example from one of my applications:
I had to make sure all of the children were the same height as the tallest child. My initial reaction was that I had to somehow wait for all of the children to finish rendering and then adjust their heights once. The easier solution was just to resize all of them every time any of them were rendered. Sure it's O(N^2) comparisons, but it's still fast for a small list and works even when new children are added.
Note that if you absolutely had to call the plugin once and no new children could be added later, you could try an ugly hack to initialize the plugin only after all of them were rendered. For example:
Template.child.rendered = function () {
if ($('.child').length === Children.find().count()) {
$('#list').myplugin();
}
};
Related
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>
I am trying to figure out how to automatic trigger a click event on certain element after all data are loaded.
the code is like this in html file:
<div *ngFor="let location of locations; let i = index;" class="location-wrapper" (click)="onSelect($event, i); $event.stopPropagation();">
<div class="location">{{ location }}</div>
</div>
the onSelect() method is doing some expansion of something that related to current location.
What I am trying to achieve is that I want the very first element of the *ngFor can be automatically clicked to show the things that related to it every time I get to this page.
Or maybe we can achieve it using other similar approach?
I have tried several ways to do this,
like putting some code in window.on('load', function() { // blablabla });
or using ngAfterViewInit() and ngAfterViewChecked(), both not work well.
You can do this in at least 2 ways. The first one would be old-fashioned javascript click(). The second would be just using component logic, just create an variable like selectedLocation which would hold current index or Object that is currently expanded. Don't forget to add initial value to it to make it after load page visible.
Javascript dispatchEvent (not Angular friendly solution)
Simply we just need to grab our item and use click() function. That's it. To grab an element we can use basic javascript method document.getElementById(elementId)" or with template variable.
<div
[id]="'location_' + i" <!-- For biding with document.getElementById -->
#locationPanel <!-- For biding with template variable -->
*ngFor="let location of locations; let i = index;" class="location-wrapper" (click)="onSelect($event, i); $event.stopPropagation();">
<div class="location">{{ location }}</div>
</div>
With Id it would look like document.getElementById("location_0").click() this gonna dispatch click event on element.
For template variable in your component you need to add
#ViewChildren locationPanel: QueryList<any>;
openFirstLocation() {
if (this.locationPanel && this.locationPanel.first)
this.locationPanel.first.nativeElement.click();
}
And in afterViewInit just call this.openFirstLocation();
Please note that it's not Angular friendly because Angular does not like when you interfere directly with DOM. However as long we don't change anything in structures then everything should be fine, but we should avoid manipulating dom with plain javascript.
Please note that too about using #ViewChild and document.* methods.
Use this API as the last resort when direct access to DOM is needed. Use templating and data-binding provided by Angular instead. Alternatively you can take a look at Renderer2 which provides API that can safely be used even when direct access to native elements is not supported.
Relying on direct DOM access creates tight coupling between your application and rendering layers which will make it impossible to separate the two and deploy your application into a web worker.
From Angular docs link
In order to to get Polymer's data-binding without creating a custom element, I am using the "dom-bind" template helper. Later on, I am going to need to access the nodes inside the template so I can use masonry.js
to create a grid out of the data.
Here is the my template that is inside the main document:
<!-- Skills -->
<template is="dom-bind" class="careerSkills_consumer projects_consumer" id="resume-container">
<page-section id="resume">
<section-title>Skills and Projects</section-title>
<section-content>
<template is="dom-repeat" items="{{careerSkills}}">
<skill-category class="grid-item" title="{{item.header}}" skills="{{item.skills}}"></skill-category>
</template>
<project-showcase class="grid-item" projects="{{projects}}"></project-showcase>
</section-content>
</page-section>
</template>
The data itself is provided elsewhere and is irrelevant. The issue I am running into is that both dom-bind and dom-repeat seem to create local dom and put the result inside of it.
To create my grid, I need to access both the container for the grid, which will be the section-content element and the grid items, which are the skill-category elements inside the dom-repeat template.
If they all resided in the same document, I think could do (I am new to masonry, so this might not actually work):
document.addEventListener('WebComponentsReady', function () {
$('#resume section-content').masonry({
columnWidth: $('#resume skill-category')[0],
itemSelector: 'skill-category',
isFitWidth: true
});
});
But the queries don't seem to work because presumably the elements I need are hidden away from the main document in the shadow dom.
I was able to get access to the content inside #resume-container via:
Polymer.dom(document.querySelector('#resume-container')).node.content
However, I still can't get to the skill-category elements in the dom-repeat. This is getting kind of pedantic and I'm not even sure if it will work when masonry tries to do the positioning.
Is there a better way to go about this?
To be clear, this question is about how to properly gain reference to the content distributed inside of template helpers, but I would also appreciate any general advice to using polymer to do this sort of thing, where a custom element isn't exactly what I'm looking for since I'm only going to use the template in one spot and shadow dom is more hassle than help, but I need the data-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 };
}
};
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');
});