Backbone: dynamic views hierarchy with templates - 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.

Related

React JSX and Django reverse URL

I'm trying to build some menu using React and need some Django reverse urls in this menu. Is it possible to get django url tag inside JSX? How can this be used?
render: function() {
return <div>
<ul className={"myClassName"}>
<li>Menu item</li>
</ul>
</div>;
}
You could create a script tag in your page to inject the values from Django into an array.
<script>
var menuItems = [
{title: 'Menu Item', url: '{% url "my_reverse_url" %}'},
{title: 'Another Item', url: '{% url "another_reverse_url" %}'},
];
</script>
You could then pass the array into the menu through a property.
<MyMenu items={menuItems}></MyMenu>
Then loop over it to create the list items in your render method.
render: function(){
var createItem = function(itemText) {
return <li>{itemText}</li>;
};
return <ul>{this.props.items.map(createItem)}</ul>;
}
This will keep your component decoupled and reusable because the logic for creating the data and the logic for displaying the list items is kept separate.

Populating nested underscore templates in Backbone.js

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!

Ember.js How to properly filter a model in a component

I have the following auto complete component:
Initial idea from EmberCasts: Building an Autocomplete Widget Part 1
App.AutoCompleteComponent = Ember.Component.extend({
searchText: null,
searchResults: function() {
var model = this.get('model');
var searchText = this.get('searchText');
console.log(this.get('model')); // shows array
if (searchText){
console.log('searching for: ' + searchText); // shows up in console with searchText
var regex = new RegExp(searchText, 'i');
model = model.filterBy('name', function(name) {
console.log(name); // never got reached
return name.match(regex);
});
}
return model;
}.property('searchText')
});
My template:
{{auto-complete model=controllers.categories}}
<script type="text/x-handlebars"s data-template-name="components/auto-complete">
{{input type="text" value=searchText placeholder="Search..."}}
<ul>
{{#each searchResults}}
<li>{{this}}</li>
{{/each}}
</ul>
</script>
The problem is, that no model item get returned. At the initial state of the program all my categories are shown - I will fix that soon. But it shows me that the auto-complete component does work. The model does get returned at first.
I think the FilterBy does not what I expect it should do.
I have tried to change the FilterBy part to this and search exactly for the name:
model = model.filterBy('name', searchText);
But that did also not work. Any ideas?
you're second approach is the correct one with filterBy, if you want to pass a function you would use filter.
model = model.filterBy('name', searchText);
I bet name doesn't exist on your models, or something along those lines. If you need more help show us an example of the categories model.
http://emberjs.jsbin.com/oTIxAjI/1/edit
You'll want to use filter
http://emberjs.jsbin.com/oTIxAjI/4/edit

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/