Nested views in Ember.js - javascript

I have a container view which, among other things, displays a list of objects, like so:
{{#each}}
<div {{bind-attr class="author.first_name task"}}></div>
{{/each}}
I would like to hook a Javascript function everytime a DOM element is added to this list. I've tried doing:
didInsertElement: function() { ... }
But this hook apparently runs only the first time the view is initialized. I figured that maybe the hook doesn't run because the view is actually inserted once, and what's inserted more than once are just the nested element.
So should I use a nested view?
I tried something along these lines:
{{#each}}
{{#view App.SingleItemView}}
<div {{bind-attr class="author.first_name task"}}></div>
{{/view}}
{{/each}}
But in this case, though it works somehow, it doesn't get passed the necessary data that would render the properties such as author.first_name.

render will give you a new scope and is really easy to assign the content as well
<ul>
{{#each item in controller}}
{{render 'ind' item}}
{{/each}}
</ul>
http://emberjs.jsbin.com/alAKubo/1/edit

Related

How to execute a callback after an #each is done?

I'm having trouble with a callback after the #each has finished. I have a template named "content":
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
{{else}}
Loading...
{{/if}}
</template>
At first I wait for a subscription, when this is available, I iterate through my Collection with {{#each}} and append the div. What I need is a sort of callback for when the for-each loop is done (in other words DOM ready).
Template.content.onRendered()
-> triggers to early
I also tried appending an image after the {{each}} and fire a function in its onload like this:
<img style="height:0;width:0" src="*mysource*" onload="callback()">
-> did work sometimes but not reliable somehow
Is there a way to get this callback? I do not fear to change the structure of this template, if that brings the solution.
There's no easy way to get notified when a Spacebars {{#each}} block has done rendering into the DOM every item getting iterated over.
The best solution is to use another reactive computation (Tracker.autorun) to observe your (reactive) current data.
Everytime your current data (which is likely a cursor) is modified, you can run arbitrary code after every other reactive computations are done performing whatever their job is, using Tracker.afterFlush.
The {{#each}} block is one of those computations, whose role is to listen to the reactive data source you give it as argument and rerender its Template.contentBlock as many times as items fetched from the source being iterated over, with the current item as current data context.
By listening to the exact same reactive data source as the {{#each}} block helper and running your code AFTER it has finished its own reactive computation, you can get the actual requested behavior without relying on some weird tricks.
Here is the full implementation of this pattern :
JS
Template.content.helpers({
currentData: function(){
return Template.currentData();
}
});
Template.content.onRendered(function(){
this.autorun(function(){
var cursor = Template.currentData();
// we need to register a dependency on the number of documents returned by the
// cursor to actually make this computation rerun everytime the count is altered
var count = cursor.count();
//
Tracker.afterFlush(function(){
// assert that every items have been rendered
console.log(this.$("[data-cid]") == count);
}.bind(this));
}.bind(this));
});
I had a similar problem and after a lot of searching found the following solution. I tried using Tracker, onRendered and other tricks, none of them worked. This could be considered more of a hack, but works. Unfortunately can't remember where I found this solution initially.
Start with your template, but add an template tag after your each.
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
{{doneTrigger}}
{{else}}
Loading...
{{/if}}
</template>
Then define a helper that returns null.
Template.content.helpers({
doneTrigger: function() {
Meteor.defer(function() {
// do what you need to do
});
return null;
}
});
You can read more about Meteor.defer() here, but it is equivalent to using a 0 millisecond setTimeout.
You can also use sub-templates and count the number of sub-templates rendered. If this number is the number of items in the collection, then all are rendered.
<template name="content">
{{#if Template.subscriptionsReady}}
{{#each currentData}}
{{> showData}}
{{/each}}
{{else}}
Loading...
{{/if}}
</template>
<template name="currentData">
<div data-cid="{{this._id}}"></div>
</template>
With that, initialize a reactive variable and track it:
var renderedCount = new ReactiveVar(0);
Tracker.autorun(function checkIfAllRendered() {
if(renderedCount.get() === currentData.count() && renderedCount.get() !== 0) {
//allDataRendered();
}
});
When the currentData template is rendered, increment it, and decrement it when it is destroyed.
Template.currentData.onRendered(function() {
renderedCount.set(++renderedCount.curValue);
});
Template.currentData.onDestroyed(function() {
renderedCount.set(--renderedCount.curValue);
});
A possibly simpler approach to consider - create a template around your #each block and then get an onRendered event afterwards:
html:
<template name="content">
{{#if Template.subscriptionsReady}}
{{> eachBlock}}
{{else}}
Loading...
{{/if}}
</template>
<template name="eachBlock">
{{#each currentData}}
<div data-cid="{{this._id}}"></div>
{{/each}}
</template>
js:
Template.eachBlock.onRendered(function(){
console.log("My each block should now be fully rendered!");
});

Am I doing it right? {{#each}}

I have a question because I'm curious if I do it right.
As I'm reading all tutorials about Ember.js every one is typing something like that:
{{#each friend in model}}
{{friend.firstName}} {{friend.lastName}}
{{/each}}
And in my app with route for example: app/templates/works/index.hbs I have:
{{#each}}
{{#link-to 'works.work' this.id class="trigger"}}
<h3 class="title">{{name}}</h3>
<p class="description">{{description}}</p>
<img {{bind-attr src=image}}>
{{/link-to}}
{{/each}}
But that still working just fine, so thats just convention or I am doing something wrong all this time?
You should go with the first form, non-context switching form of {{each}}, as the second form is deprecated. See here.
The first form is less confusing as you don't need to think of the context for your properties.

EmberJS: can't insert an element into the DOM that has already been inserted

Edit: stripped-down JSBin link
I'm running into a problem where when I create a new record, I get an error saying "uncaught exception: You can't insert an element into the DOM that has already been inserted".
An excerpt from index.html:
<ul id="flowList">
{{#each}}
{{#if isTemplate}}
{{view App.FlowView}}
{{/if}}
{{/each}}
</ul>
<ul id='levelsList'>
<ul id='level1'>
{{#each}}
{{#if isLevelOne}}
{{view App.FlowView}}
{{/if}}
{{/each}}
</ul>
</ul>
I preloaded a Flow object with isLevelOne false, and isTemplate true. It has a button which creates a new flow such that the computed property isLevelOne is true, and the property isTemplate is false -- I verified this in the Ember Inspector. However, when this happens, I get the an uncaught exception: You can't insert an element into the DOM that has already been inserted, although the page seems to render alright. When I try to refresh the page (with the new record saved into the store) it then fails to render the page and throws an Error: Something you did caused a view to re-render after it rendered but before it was inserted into the DOM. Neither of these errors make any sense to me -- any help would be appreciated.
Edit: FlowView template included below
<script type="text/x-handlebars" data-template-name='flow-view'>
<li {{bind-attr id=title class=":flow isTemplate"}}>
{{#if isTemplate}}
<button {{action "insertIntoSandbox" this}}>&plus;</button>
{{/if}}
<label>{{title}}</label>
{{#unless isDefault}}
<button {{action "removeFlow" this}} {{bind-attr class="isTemplate"}}>−</button>
{{/unless}}
</li>
</script>
Edit: PortkeyController added below
App.PortkeyController = Ember.ArrayController.extend({
actions: {
/**
* #elem is the flow associated with the button -- can use this to duplicate
*/
insertIntoSandbox: function(elem) {
var _store = this.store;
var newFlowJSON = elem.toJSON();
console.log(newFlowJSON); // isDefault=true, isTemplate=true, params=[] from element
delete newFlowJSON.id; // might be unnecessary?
newFlowJSON.title = 'gg';
newFlowJSON.level = 1;
newFlowJSON.isLevelOne = false;
var newFlow = _store.createRecord('flow', newFlowJSON);
newFlow.save();
},
});
I suspected as much, Ember usually goes haywire with invalid HTML when attempting to remove/add elements. (I'm not sure if it's Ember, or the browser helping by injecting the closing tag for you, and then Ember not removing the browser helped tag)
The FlowView is missing the closing LI tag.

Handlebars.js - Getting parent context within an each loop, an if statement and a child object

I understand how to transverse the data source within Handlebars but I have stumbled across a situation I cannot work out.
Using "../" you can reach the parent template scope but when iterating through the child of an object it seems to return the object and not the child.
{{#each content.items}}
{{#if prop}}
<p>{{prop}} + {{../../variable}}</p>
{{/if}}
{{/each}}
The above code snippet works fine if you iterate through an object called 'content' but as soon as you iterate through it's child, 'content.items' it no longer returns the right scope.
Here is a fiddle which demonstrates the issue. http://jsfiddle.net/sidonaldson/MDdn2/
Can anyone shed any light on what is wrong?
It turns out that my original thought was wrong. I've only used Handlebars.js inside the context of Ember.js. Ember provides some extra helpers that aren't available in plain Handlebars, so that wasn't an option. But I did seem to figure out your issue. Check this fiddle.
<p>IN CONTENT</p>
{{#with content}}
{{#each items}}
{{#if prop}}
<p>{{prop}} + {{../../variable}}</p>
{{/if}}
{{/each}}
{{/with}}
<p>OUTSIDE CONTENT</p>
{{#each items}}
{{#if prop}}
<p>{{prop}} + {{../../variable}}</p>
{{/if}}
{{/each}}
I'm not sure why it didn't work in the first place, but using the with helper, then the each helper seemed to work. Hopefully I've come close to what you wanted.

Render Multiple View/Controller Pairs

I am currently rendering a list of views:
<ul>
{{#each newsItem in controller}}
{{view App.NewsItemView contentBinding="newsItem" class="news-item" }}
{{/each}}
</ul>
But I would like to inject a NewsItemController into each view.
I've tried using render, but this only seems to support a single view, giving the exception:
Uncaught Error: Handlebars error: Could not find property 'control'
on object .
I've found a brief mention of using control instead, but this no longer seems to be included.
So how can I render multiple versions of the same view, injecting a separate controller into each one?
{{render}} should be fixed in current master (if you build it from Github). You should be able to use it multiple times if you pass a model:
<ul>
{{#each controller}}
{{render "newsItem" this}}
{{/each}}
</ul>
{{control}} is still there but hidden behind a flag (because it's still experimental). To use it you need to do : ENV.EXPERIMENTAL_CONTROL_HELPER = true before including the ember.js file. If you can avoid using it, it would be better.
However I think the simplest approach would be to use itemController:
<ul>
{{#each controller itemController="newsItem"}}
{{view App.NewsItemView class="news-item" }}
{{/each}}
</ul>
I think you can combine them to make it simpler (I haven't tried it yet):
<ul>
{{each controller itemController="newsItem" itemViewClass="App.NewsItemView"}}
</ul>

Categories

Resources