Templating engine that binds model attributes to view? - templates

When rendering a template through a Backbone view you will often end up with some code that looks something like this:
ShirtView = {
template: JST["/templates/shirt_template"],
el: ".shirt-element"
render: function() {
var html = this.template({color: this.model.color, size: this.model.size});
this.$el.html(html);
}
}
This is all well and good and your template will render with the attributes you wanted. But if this.model.color changes then it will not be reflected in the view. You can then use something like modelbinder to explicitly bind elements in the view to your model, but this means introducing extra code to your view.
What I am wondering is if there are any templating engines, like Moustache or Handlebars, that automatically updates the elements belonging to the fields in the attributes object as the model changes, without me having to specify it in the view?

As the comments have suggested, there are several libraries you can use for this ... but I'd like to suggest that you don't need to. I work on a Backbone-powered site with thousands (heck, probably tens or hundreds of thousands) of lines of code, and all we use is our own custom base class.
In essence, all you need to do is:
var TemplatedView = Backbone.View.extend({
render: function() {
this.renderTemplate();
}.
renderTemplate: function() {
this.$el.html(this.template(this.model.toJSON()));
}
});
then you can make any new view a templated one with just:
var ShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element"
});
or, if you have some custom render logic, you just need to call renderTemplate:
var AdvancedShirtView = TemplatedView.extend({
template: JST["/templates/shirt_template"],
el: ".shirt-element",
render: function() {
this.model.fold();
this.renderTemplate();
this.model.press();
}
});
Now we have a few enhancements beyond that (eg. if someone specifies a "rawTemplate" property on one of our views, our renderTemplate will compile it in to a proper template), but that's the beauty of rolling your own solution for something like this: you get exactly what you want.
Or you can use a library :-) But personally, for something that's both so simple and so integral to your site, I don't see why you'd want to.

Related

How do I call a controller function from a template in Ember?

Let's say I have a template which iterates over a collection of items, and I want to call a function with each item which is specific to the controller, and not a model-level concern:
{{#each people as |person|}}
icon name: {{findIconFor(person)}}
{{/each}}
I'd like to define findIconFor in the controller, because this is something specific to this particular view.
export default Ember.Controller.extend({
findIconFor: function(person) {
// figure out which icon to use
}
);
But that doesn't work. The template fails to compile. Parse error: Expecting 'STRING', 'NUMBER', 'ID', 'DATA', got 'INVALID'
What is the "ember way" to do this?
As i spent almost entire day on a similar problem here is my solution.
Because Ember for some reason just doesn't allow you to run a controller functions directly from the template (which is ridiculous and ties your hands in some very stupid ways and i don't know who on earth decided this is a good idea ...) the thing that makes most sense to me is to create an universal custom helper, that allows you to run functions from the template :) The catch here is that you should always pass the current scope (the "this" variable) to that helper.
So the helper could be something like this:
export default Ember.Helper.helper(function([scope, fn]) {
let args = arguments[0].slice(2);
let res = fn.apply(scope, args);
return res;
});
Then, you can make a function inside your controller, that you want to run, for example:
testFn: function(element){
return element.get('name');
}
and then in your template you just call it with the custom helper:
{{#each items as |element|}}
{{{custom-helper this testFn element}}}
{{/each}}
The first two arguments to the helper should always be "this" and the name of the function, that you want to run, and then you can pass as many extra arguments as you wish.
Edit: Anyway, every time when you think you need to do this, you should think if it will not be better to create a new component instead (it will be in 90% of the cases)
I'd use a computed property in the controller:
iconPeople: Ember.computed('people.#each', function(){
var that = this;
return this.get('people').map(function(person){
return {
'person': person,
'icon': that.findIconFor(person)
};
});
})
Now you could get the icon from {{person.icon}} and the name from {{person.person.name}}. You might want to improve on that (and the code is untested), but that's the general idea.
If the icon is something associated with a person, then since the person is represented by a model, it is best to implement it as a computed property on the person model. What is your intent in trying to put it into the controller?
// person.js
export default DS.Model.extend({
icon: function() { return "person-icon-" + this.get('name'); }.property('name')
..
};
Then assuming that people is an array of person:
{{#each people as |person|}}
icon name: {{person.icon}}
{{/each}}
The alternative provided by #jnfingerle works (I assume you figured out that he is proposing that you loop over iconPeople), but it seems like a lot of extra work to go to to create a new array containing objects. Does the icon depend on anything known only to the controller? If not, as I said, why should the logic to compute it be in the controller?
Where to put things is a a matter of philosophy and preference. Some people like bare-bones models that contain nothing more than fields coming down from the server; other people compute state and intermediate results in the model. Some people puts lots of stuff in controllers, whereas others prefer light-weight controllers with more logic in "services". Personally, I'm on the side of heavier models, lighter controllers, and services. I'm not claiming that business logic, or heavy data transformations, or view preparations should go in the model, of course. But remember, the model represents an object. If there's some interesting facet to the object, whether it come down from the server or be computed somehow, to me it makes a lot of sense to put that in the model.
Remember also that controllers are part of a tightly-coupled route/controller/view nexus. If there's some model-specific thing that you compute in one controller, you might have to then add it to some other controller that happens to be handling the same model. Then before you know it you're writing controller mixins that share logic across controllers that shouldn't have been in them in the first place.
Anyway, you say your icon comes from an "unrelated data store". That sounds asynchronous. To me, that hints that maybe it's a sub-model called PersonIcon which is a belongsTo in the person model. You can make that work with the right mix of adapters and serializers for that model. The nice thing about that approach is that all the asynchronicity in retrieving the icon is going to be handled semi-magically, either when the person model is created, or when you actually need the icon (if you say async: true).
But perhaps you're not using Ember Data, or don't want to go to all that trouble. In that case, you could consider adorning the person with the icon in the route's model hook, making use of Ember's ability to handle asynchronous model resolution, by doing something like the following:
model: function() {
return this.store.find('person') .
then(function(people) {
return Ember.RSVP.Promise.all(people.map(getIcon)) .
then(function(icons) {
people.forEach(function(person, i) {
person.set('icon') = icons[i];
});
return people;
})
;
})
;
}
where getIcon is something like
function getIcon(person) {
return new Ember.RSVP.Promise(function(resolve, reject) {
$.ajax('http://icon-maker.com?' + person.get('name'), resolve);
});
}
Or, if it is cleaner, you could break the icon stuff out into an afterModel hook:
model: function() { return this.store.find('person'); },
afterModel: function(model) {
return Ember.RSVP.Promise.all(model.map(getIcon)) .
then(function(icons) {
model.forEach(function(person, i) {
person.set('icon') = icons[i];
});
})
;
}
Now Ember will wait for the entire promise to resolve, including getting the people and their icons and sticking the icons on the people, before proceeding.
HTH.

The Ember Way for Setting a Controller Property Based on this.store.find()

I am setting up a page where my user can add an orgLanguage, and I'd like to show a special message if this is the first orgLanguage being added. I'm able to get my code working, but it sure looks ugly, and I'm wondering if there's a better way to handle this?
First, here's my Handelbars template:
Handlebars Template (Simplified):
{{#if isFirstOrgLanguage}}
...display some content
{{/if}}
That variable is defined on my controller as follows.
Controller (Simplified):
export default Ember.ObjectController.extend({
isFirstOrgLanguage: function() {
// the 'orgLanguages' controller property is set in the route
var orgLanguagesPromiseArray = this.get('orgLanguages');
return orgLanguagesPromiseArray.then( function() {
var orgLanguagesRecordArray = orgLanguagesPromiseArray.get('content');
var orgLanguagesArray = orgLanguagesRecordArray.get('content');
return orgLanguagesArray ? orgLanguagesArray.length === 1 : true;
});
}.property('orgLanguages')
}
I've named my variables the data type that I receive. You'll note that this is a computed property that depends on a controller property set on my route, shown below.
Route (Simplified):
setupController: function (controller, model) {
this._super(controller, model);
controller.set('orgLanguages', this.store.find('org-language') );
},
Finally, I'd like to call some basic jQuery on this Handlebars template if isFirstOrgLanguage is true, so I set up my view as follows.
View:
export default Ember.View.extend({
didInsertElement: function() {
this.get('controller').get('isFirstOrgLanguage').then( function( isFirstOrgLanguage ) {
console.log('isFirstOrgLanguage', isFirstOrgLanguage);
});
}
});
This seems like a crazy amount of promises and async management just to answer the question "is there exactly 1 orgLanguage defined"? Although the above works, is there a simpler way, or perhaps "The Ember Way" to do this?
Update:
In doing some additional research, it seems this has been a topic for some debate. Here are relevant discussions I've seen on this. If I settle on a pattern I like, I'll post it as as an answer, but would welcome other suggestions.
http://discuss.emberjs.com/t/dashboard-type-views/5187/24
http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4
I wanted to post how I eventually solved this.
First, it became clear that there are recommended solutions to this pattern, but no "one true way". See http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4.
What I wound up using was this:
Route:
...
afterModel: function() {
var _this = this;
Ember.RSVP.hash({
languages: this.store.find('language'),
orgLanguages: this.store.find('org-language')
}).then( function( hash ) {
_this.set('controller.languages', hash.languages );
_this.set('controller.orgLanguages', hash.orgLanguages );
});
},
...
The key insights here are:
This is done after the page's model loads. This may or may not make sense depending on your context.
Some people like to wrap each model in its own controller, but I didn't have clean mappings to controllers like that, so I directly set these property values.
It's generally bad practice to set computed properties that are promises, so if you have to deal with promises (which with any use of this.store.find() you do, then it's best to resolve the promise in the route and then pass the "concrete" property to your controller. But keep in mind that your template will be rendering these values when they eventually resolve! So, again there is some room for debate.
I think the general takeaway is that Ember is giving you lots of options to get this done, with plenty of possibilities to use depending on your needs.

Emberjs. Building a Model with Ember.Object

Forgive me as I am new to Ember. I have, to me , a fairly complex json object that I am working with.
https://gist.github.com/bungdaddy/11152304
My attempt is to build a Model with Ember.Object, use reopenClass for several methods that will return 'sections' of the JSON object that I can use in my handlebars template.
var Prequalification = Ember.Object.extend();
Prequalification.reopenClass({
template: function(){
return $.getJSON("http://myurl/prequalification")
.then(function(response){
var prequalification = [];
var template = response.collection.template.data
template.forEach(function(data){
prequalification.push(Prequalification.create(data));
});
console.log(template);
return prequalification;
});
},
businessType: function(){
//Here is where I would like to build a method that will pull from template, the particula JSON that I need and return below.
return ["Me","Myself","I"];
}//I wish to further extend from here, other methods that I may need to fulfill model requirements.
});
I most likely will need an ArrayController to manage these models. Thing I have read so much, compiled so many different variations that I am quite lost. Any clarity in all this would be a great help to me. I can handle the simple JSON objects, it's the complex models that are kicking my ^&&^$^(*^#

Ember JS Router not filtering when I change the controller from default

My router in it's entirety:
Books.Router.map(function () {
this.resource('books', { path: '/' }, function () {
this.route('search', { path: 'search/:keyword' });
});
});
Books.BooksRoute = Ember.Route.extend({
model: function(){
return this.store.find('book');
},
actions: {
postAlert: function (msg, classes) {
var postAlert = $('#alert');
postAlert.html(msg).toggleClass(classes).toggle(1000);
setTimeout(function () {
postAlert.toggle(1000, function () {
postAlert.toggleClass(classes)
});
}, 3000);
}
}
});
Books.BooksIndexRoute = Ember.Route.extend({
model: function () {
return this.modelFor('books');
},
renderTemplate: function () {
this.render({ controller: 'books' });
}
});
Books.BooksSearchRoute = Ember.Route.extend({
model: function (params) {
return this.store.filter('book', function (book) {
return book.get('titleSlug').indexOf(params.keyword) > -1;
})
},
renderTemplate: function (controller) {
this.render('books/index', { controller: controller });
}
});
Now let's focus on the last bit of the router, the BooksSearchRoute. When I leave my router as it is right now and go to the route localhost/#/search/the_adventures_of_huckleberry_finn then I will see the books/index template populated with the model where the titleSlug contained the dynamic segment which is great, exactly what I expect.
Now when I try to use an action defined in my books controller from that URL I get an error that nothing handled the action. In response to that I switched the renderTemplate line so that it uses 'books' as the controller instead of the default controller.
renderTemplate: function () {
this.render('books/index', { controller: 'books' });
}
That change allows me to access the actions in the books controller that I need. However after making the change the filter does not appear to work anymore as all of the books in the libray are displayed rather than just thouse matching the search term. Can someone please explain to me what is happening here?
That is actually the expected behaviour.
Explanation
When a Route is hit, it obtains the model and passes that to the Controller associated with that route. It determines which one using the Ember naming conventions.
Override template
What you have done here is override renderTemplate here to specify that a different template should be used, than the one that the naming conventions tell it to use.
That works fine in this case because the model for the BooksIndexRoute and the model for the BooksSearchRoute are compatible - they are both arrays of Books.
Override template AND controller
The next thing that you did was to override renderTemplate here to specify that a different template should be used, and it should use a different controller too, BooksController, according to the naming convention.
Of course, BooksController doesn't know that you have done this, and will use the model that it is aware of, the model returned by its own Route, which is this case was this.store.find('book').
... and since that model is not filtered, the template renders the full set of Book models.
Suggested solution
You can probably continue along this path, where you override the template and controller, and refactor the required actions such that they are available on both controllers. However, I would not suggest this, as it goes against the grain of how Ember was designed, plus it will involve quite a lot of spaghetti code.
The canonical solution would involve using the routes and controllers that you already have, but do not override renderTemplate in BooksIndexRoute.
Instead, extract the code that renders your list of books into a separate template and put it into a folder called partials, then invoke that partial from both the templates:
from books (or books\index as the case may be), as well as
from books\search
The syntax looks like this
I do not know what your templates look like, but if you post them, I can show you how to do so.

Ember.JS + Require.JS — Rendering specific view depending on the route

I'm trying out Ember.JS and I'm having a really tough time using it with Require.JS so far, even with a (pretty) basic example.
First of all, I'd like to say that Require.JS is supposed to (I think) improve two weak points I see in Ember.JS :
Organizing the app, especially in separate js files
Not loading unnecessary code
I'm basically trying to display an app with header/content/footer. So, when I'm creating my App I'm binding a ApplicationController and an ApplicationView, and the view handles the template. This works great in displaying (pretty easily) the header and the footer.
Then, I'm trying to render a template for the index (for example), and I would like to dynamically load IndexView/IndexController (for example) and bind it with a route. That's where I'm having a tough time.
I found an easy way to do this by setting IndexView directly as App.IndexView, but the problem with this solution is that if I load IndexView, I'm also loading the index template file content (using text.js plugin). That would be okay for my example, BUT since I'm trying to build a complex website, that would mean loading all the templates when loading the website, which is exactly what Require.JS was trying to avoid.
Where am I wrong here? How do I dynamically load the template depending on the routing?
EDIT: It's not really needed to declare a placeholder in the main html document as it's injected using view.append().
I've been struggling with the same thing, and I finally came up with a way to split over router, controller, views and templates loading them dynamically.
This is my main "embermain.js" file:
window.MyRanks = Ember.Application.create();
MyRanks.Router.map(
function() {
this.route('about');
}
);
MyRanks.AboutRoute = Ember.Route.extend({
setupController: function(controller, model) {
require(['app/controller/AboutController'], function(controller) {
});
}
});
Here is my AboutController:
require(
['app/view/AboutView'],
function (view) {
var controller = MyRanks.AboutController = Ember.Controller.extend({
});
return controller;
});
Here is my AboutView:
define(
['text!app/templates/about.html'],
function (template) {
var view = Ember.View.create({
template: Ember.Handlebars.compile(template),
templateName: 'about',
variable: 'my value',
didInsertElement: function() {
console.log( "Yes the view was included");
}
});
view.append();
return view;
}
);
And here is the template about.html
This is the template {{view.variable}}
Hope it helps! :)