Populating nested underscore templates in Backbone.js - templates

I am attempting to create an html page from a complex JSON object. I have already successfully parsed the JSON object into a Collection of Models, Where each Model has a collection of another Model etc..
I therefore have nested views to cater for this.
To create my html page, I have two templates like the following:
<script type="text/template" id="template1">
<h1><%=heading1%></h1>
<h2><%=heading2%></h2>
<ul id="template2-list"></ul>
</script>
<script type="text/template" id='template2'>
<p class = "heading"><%-otherheading%></p>
<div class="content" id="tab">
.....
</div>
</script>
As you can see, I have a template (template1) that contains a list of template2. How would I go about populating these templates from my Backbone nested views?
This is what I have tried:
var CollectionView = Backbone.View.extend({
type: "CollectionView", //For debugging purposes
el: "#container",
initialize: function () {
},
render: function () {
_.each(this.model.models, this.process, this);
return this;
},
process: function(obj)
{
var childItemView = new View1({model: obj});
childItemView.render();
this.$el.append(childItemView.el); //This works fine since I only have one Model in the highest level collection
}
})
var View1 = Backbone.View.extend({
type: "View1",
template: _.template($("#template1").html()),
tagName: "div",
className: "tableRow",
initialize:function () {
this.model.on("change", this.modelChanged, this);
},
render: function () {
var outputHtml = this.template(this.model.toJSON());
this.$el.html(outputHtml);
this.model.get('nestedModel').each(this.process, this);
return this;
},
process: function(obj) {
var childItemView2 = new View2({model: obj});
childItemView2.render();
childItemView2.el = '#template2-list';
$(this.el).append(childItemView2.el); //This still results in template2 being placed after the entire template 1
},
modelChanged: function(model, changes) {
console.log("modelChanged: " + this.model.get('title'));
}
});

If it's just populating underscore, then you should convert the collection to json(including the submodels collections), and you can add a for loop inside of the template. <% for(var x... %>.
The other option is, to use a library like marionette which has a composite view which can hold collection views, you can see an example for a treeView here: http://lostechies.com/derickbailey/2012/04/05/composite-views-tree-structures-tables-and-more/
it basically shows how to render collections inside collections.

There are lots of ways to do this.
Template inside template
Pass the entire collection and do all recursive iteration logic in the template itself by calling the child template inside parent template itself. Only one view is involved.
<script type="text/template" id="template1">
<h1><%=heading1%></h1>
<h2><%=heading2%></h2>
<ul id="template2-list">
<!-- Your iteration logic goes here -->
<%= _.template($("#template2").html())({model: model}) %>
</ul>
</script>
<script type="text/template" id='template2'>
<p class = "heading"><%-otherheading%></p>
<div class="content" id="tab"></div>
</script>
Better way is:
In the collection view, create a child view instance(you have done that)
Do the recursive iteration logic to read the collection models in the collection view(parentview) and call the child view to render the child collections.
If you want a complete solution, create a fiddle with json and html. Will help you to make it work.

I realised my mistake. Not sure if this is entirely correct, but I rendered the parent view first, then found the new list element (template2-list) and appended the rendered child view to that.
i.e.
render: function () {
var outputHtml = ...
this.$el.html(outputHtml); //render parent view
this.model.get('nestedModel').each(this.process, this);
...
},
process: function(obj) {
var childItemView2 = new View2({model: obj});
childItemView2.render();
this.$('#template2-list').append(childItemView2.el);
},
Thanks for all the help!

Related

Ember Dynamically Generated HTML

I have a requirement where I need to add html after the DOM has been rendered.
I was wondering if it is possible to manipulate the DOM after creation and dynamically add html and also specifying an associated ember action.
E.g. The intension of what I want to achieve:
$('.add').on("click", function(e){
e.preventDefault();
i += 1;
var content = "<div class=\"item dodgerBlue\"><h1>"+i+"</h1></div>";
var content + "{{action "owlItemClicked" titleModel.id titleModel.index titleModel on="click" }}"
owl.data('owlCarousel').addItem(content);
});
Specifically I want to add another Item to my carousel:
http://owlgraphic.com/owlcarousel/demos/manipulations.html
I'm not sure that the triple-stash, which just includes content without escaping it, will work with an {{action}}. In any case, it looks to me that you'd be better off simply defining the html within an each block and letting Ember handle the content addition.
{{#each model as |titleModel index|}}
<div class=\"item dodgerBlue\">
<h1>{{index}}</h1>
</div>
{{action "owlItemClicked" titleModel.id titleModel.index titleModel on="click" }}
{{/each}}
I noticed you have a titleModel.index property, so maybe you don't need the index in each block and can use the model's property instead.
That would be the Ember way to do it. However, it looks like this OwlCarousel widget wants to have the html passed directly. But there's also a reinit method, so maybe that would be sufficient to tell it that new content has been added through Ember. Something like the following:
carouselOptions: {
// ...
},
didInsertElement: function() {
Ember.run.scheduleOnce('afterRender', this, function() {
var $c = Ember.$('#the-carousel');
if ($c.length) {
$c.owlCarousel( this.get('carouselOptions') );
}
});
},
actions: {
addCarouselItem: function(e){
e.preventDefault();
// Add a new item to your array.
// Not shown because i have no idea of your code.
// Ember will handle DOM insert.
// Then reinit carousel.
$("#the-carousel").data('owlCarousel').reinit( this.get('carouselOptions') );
},
owlItemClicked: function(e) {
// ...
}
}
You can insert dynamically created HTML using the triple-stache in your template.
// controller.js
dynamicHtml: Ember.computed({
get() {
return `<div>Hello World!</div>`;
}
})
...
{{! template.hbs }}
{{{dynamicHtml}}}

EmberJs: render a template within a handlebars-helper into its parent element

Because I have the name of a View inside a variable I'm using a handlebars helper to get it rendered.
So, I have an object as follows (all simplified of course)
config = Ember.Object.create({ view: "MyTplView"}) ;
Next, I have the following template
<script type="text/x-handlebars" data-template-name="index">
<div id="container">
{{helperViewRenderer config}}
</div>
</script>
And the Handlebars helper
Ember.Handlebars.registerBoundHelper('helperViewRenderer', function(value, options) {
var template = App[value.view].create() ;
template.appendTo("#container") ;
}) ;
The problem is that I try to insert the template into the element with id "container", which doesn't exist at that moment. What are the possibilities to fix this. For example, is it possible to get a HTML-string from the template and return this instead ?
I have occasionally found it valuable to define a child view class inside of a parent view class instead of as a global. You can pass any path -- global or local -- to the view helper, and the current view is available as view in the current context. Thus,
App.ParentView = Ember.View.extend({
template: Ember.Handlebars.compile("The parent's child: {{view view.ChildView}}."),
ChildView: Ember.View.extend({
template: Ember.Handlebars.compile("-- child view --")
})
});
Ember tries to go the route of creating 'outlets' through the use of the new router.
If you want to use the appendTo method then obviously you have to append your template to an existing HTML element. In that way, you can't get a HTML string from a template (unless you know it is part of a parent template). However if you're not too bothered about where you're inserting your template, then you can use a straight append which will add the template to the end of the body.
I think in your case it would be better to get familiar with the router, as you seem to be wanting to render different templates inside the same main template. Therefore you can easily separate these out by using the URL as a variable, and injecting the relevant view into the outlet.
App.Router.map(function () {
this.route("index", { path: "/" });
this.route("first", { path: "/view1" });
this.route("another", { path: "/view2" });
});
App.FirstRoute = Em.Route.extend({
renderTemplate: function () {
this.render('view1', { outlet: 'main' });
}
});
App.AnotherRoute = Em.Route.extend({
renderTemplate: function () {
this.render('view2', { outlet: 'main' });
}
});
View:
<script type="text/x-handlebars" data-template-name="index">
<div id="container">
{{outlet main}}
</div>
</script>
This way you don't need any handlebars helper passing in variables and it's all neatly wrapped up in Ember conventions.

Backbone: dynamic views hierarchy with templates

I have three different view templates: "post", "comment", "add new comment". The main template is "post". I need to find out how to place "comments" and "add new comment" templates into theyr divs in the "post" template. Or any other methods to make this structure:
- post
- comments
- add new post form
- post
...
It is similar to facebook wall
Javascript for Backbone:
// Post View
var PostView = Backbone.View.extend({
template: $("#post").html(),
...
render: function () {
var tmpl = _.template(this.template);
var thisPost = this.model.toJSON();
this.$el.html(tmpl(thisPost));
}
});
var postView = new PostView();
postView.render();
// Comments List
var CommentsListView = Backbone.View.extend({
el: '#comments', // how to place it to #comments div in "post" template? This line doesn't work
...
addNewCommentForm: function (post_id) {
var tmpl = _.template($("#addCommentTemplate").html());
this.$('#addNewComment').append(tmpl()); // How to place it to #addNewComment div in "post" template? This line doesn't work
}
});
HTML:
<script id="post" type="text/template">
<%= text %>
<div id='comments'>...</div>
<div id='addNewComment'>...</div>
</script>
There are a number of things wrong with your code. The first being that you aren't actually putting the PostView.el into the DOM. Through PostView.render(), you are populating PostView.$el and subsequently PostView.el, but you're not actually putting it into the page (DOM). Additionally, by setting the el on CommentsListView, you're not really doing anything there. If you wanted to set the el to an existing element, then you would do something like this: el: $('#comments'). Or if you want to render the CommentsListView dynamically and inject it into the DOM, then you would want to just make the element have an id of 'comments' by doing defining the id property like so: id: 'comments'. Those are just the two most obvious problems with the code. I got a semi-working example running here: http://codepen.io/jayd3e/pen/hAEDv.

What's the best idiom for creating an EmberJS view that can show all its child-views, or just one?

Say I have a model App.Page, an ArrayController of App.Page, and an App.PageView to render each App.Page.
I'm trying to figure out how to best implement App.MyPagesView so it works like so:
if App.showAllPages is true: I want MyPagesView to contain an App.PageView(s) for displaying each of the App.Page in App.pages
Else: I want MyPagesView only show one App.PageView, bound to App.pages.currentPage.
The most straightforward implementation that occurs to me is using a template like so:
// MyPagesViewApproach1
{{#unless App.showAllPages}}
{{view App.PageView pageBinding="pages.currentPage"}}
{{else}}
{{#each pages}}
{{view App.PageView pageBinding="this"}}
{{/each}}
{{/unless}}
But won't this create new views for the existing models every time the user toggles showAllPages on and off? Also, I get emberJS warnings about performance issues when I try to use this template.
The PageView(s) could be quite complex and expensive to render. I'd really like to create a PageView once for each Page, and just remove/hide the irrelevant PageViews from the DOM when they're not in use.
App = Ember.Application.create({
showAllPages: false,
pages: Ember.ArrayController.create({
content: []
currentPage: null
}),
ready: function () {
this.pages.pushObject(App.Page.create({title: 'Page One'});
this.pages.pushObject(App.Page.create({title: 'Some Other Page'});
this.pages.pushObject(App.Page.create({title: 'Grrreatest Page Evar'});
this.pagesController.set('currentPage',
this.pagesController.get('firstObject'));
}
});
App.Page = Ember.Object.extend({
title: null
// etc, etc...
});
App.PageView = Ember.View.extend({
templateName: 'page',
page: null // should be bound to an App.Page
});
App.MyPagesView_Approach1 = Ember.View.extend({
pagesBinding: 'Elicitation.pages'
// ???
});
App.MyPagesView_Approach2 = Ember.ContainerView.extend({
// ???
});
And my HTML:
<script type="text/x-handlebars" data-template-name="page">
The title of this page is {{ page.title }}
</script>
{{view App.MyPagesView }}
To recap, what's the proper EmberJS-y way to implement MyPagesView so it responds to App.showAllPages without re-creating all the views each time its toggled?
Should it be some sort of ContainerView? Or should I use the unless/else template shown at the top of the question? Or something entirely different? I feel like a really simple solution exists in EmberJS, but its elluding me.
Here's the best I've come up with, encapsulated as a re-usable View class called "CurrentCollectionView". I'm using CollectionView, and using view.set('isVisible') to hide/show appropriate child views. Basically use it like a CollectionView, but you can set currentContent to hide all but one element of content, or use showAllContent to override currentContent.
App.CurrentCollectionView = Ember.CollectionView.extend({
showAllContent: false,
currentContent: null,
currentContentChanged: function () {
console.log("Elicitation.PagesView.currentContentChanged()");
var showAllContent = this.get('showAllContent');
if (Ember.none(showAllContent) || !showAllContent) {
var contents = this.get('content');
var currentContent = this.get('currentContent');
this.get('childViews').forEach(function (view, i) {
var isVisible = contents.objectAt(i) == currentContent;
view.set('isVisible', isVisible);
});
} else {
this.get('childViews').forEach(function (view) {
view.set('isVisible', true);
});
}
}.observes('currentContent', 'showAllContent', 'childViews')
});
An example of using CurrentCollectionView to implement MyPagesView:
App.MyPagesView = App.CurrentCollectionView.extend({
itemViewClass: App.PageView,
contentBinding: 'App.pages',
currentContentBinding: 'App.pages.currentPage',
showAllContentBinding: 'App.showAllPages',
});
or as using it inline as a template:
{{view App.CurrentCollectionView itemViewClass="App.PageView" contentBinding="App.pages" currentContentBinding="App.pages.currentPage" showAllContentBinding="App.showAllPages"}}
Hope somebody else finds this useful and/or can improve on it (please!)

Create Ember View from a jQuery object

I'm looking into integrating Ember with an existing Rails application, to take advantage of Ember's bindings, events (didInsertElement, etc.) ...
Now I don't want to transfer my erb views to handlebars, but instead I want to create Ember View objects and attach them to various elements already in the DOM. For example, I might have
<html>
<body>
<div class="header">
</div>
<div class="content">
</div>
<div class="footer">
</div>
</body>
</html>
and (on DOM ready) create a View for each element:
App.HeaderView = Ember.View.create({
// capture $('.header') to this
// console.log(this.$().attr('class')) should then output `header`
});
use appendTo() on a view: App.HeaderView.appendTo('.header') see http://jsfiddle.net/yFke9/
UPDATE
I think this is currently not possible. Please proof me wrong! You could create a workaround for this, although this is a hack, see http://jsfiddle.net/jFTk5/. The workaround basically adds the view via append() and inside the didInsertElement callback it replaces the specific element via jQuery's replaceWith.
App.HeaderView = Ember.View.create({
template: Ember.Handlebars.compile('hello from HeaderView'),
classNames: ['header'],
didInsertElement: function() {
Ember.$('.header').replaceWith(this.$());
}
}).append();
If you're going with this solution you could write a Mixin which handles this for you, see http://jsfiddle.net/KFcgA/.
App.ReplaceWith = Ember.Mixin.create({
didInsertElement: function(){
var el = this.get('elementToReplace');
Ember.$(el).replaceWith(this.$());
}
});
App.HeaderView = Ember.View.create(App.ReplaceWith, {
template: Ember.Handlebars.compile('Hello from HeaderView'),
classNames: ['header'],
elementToReplace: '.header'
}).append();
Ok the following works but I haven't fully tested it.
Inspired by #pangratz's pull request I extend Ember.View with the following method for
Ember.View = Ember.Object.extend(
/** #scope Ember.View.prototype */ {
// ........
wrap: function(target) {
this._insertElementLater(function() {
// Set all attributes name/values from target
var target_attrs = {};
var $this = this.$();
for (var attr, i=0, attrs=$(target)[0].attributes, l=attrs.length; i<l; i++){
attr = attrs.item(i)
var attrName = attr.nodeName;
var attrValue = attr.nodeValue;
if(attrName === 'id') continue;
$this.attr(attrName, attrValue);
}
// Set HTML from target
$this.html($(target).html());
Ember.$(target).replaceWith($this);
});
return this;
},
// ........
});
Basically it copies the html content of the target element as well as its attributes. Then by just doing
App.HeaderView = Ember.View.create().wrap('.header');
the .header element (that is already in the DOM) is now in App.HeaderView.
See http://jsfiddle.net/KFcgA/4/