Ember.js: Passing arguments to render helper - javascript

Say I have two objects in my application: 1) Topic, 2) Categories.
Topic has a route /topic/:topic_id
Categories doesn't have a URL route. It contains the list of all available categories that topics can be in.
On the page for Topic, I would like to show a list of all categories, while highlighting the category that the topic is a part of (the 'active' category)
Original Solution
I had originally solved this with a component as follows (a lot of code removed for brevity):
Component Template:
(Goes trough each category and lists them. If the current one is active, it says so.)
<script type="text/x-handlebars" id="components/category-list">
{{#each categories}}
{{#if this.active}} Active -> {{/if}}
{{this.name}}
<br />
{{/each}}
</script>
Component Object:
(Goes through the models via the CategoriesController, marks one category as the current active category for that topic via a "current" parameter passed in from the {{category-list}} component helper)
App.CategoryListComponent = Ember.Component.extend({
tagName: '',
didInsertElement: function () {
var categories = App.CategoriesController;
var current = this.get('current').get('id');
categories.get('content').forEach(function (c) {
if (c.id === current) {
c.active = true;
}
});
this.set('categories', categories);
}.observes('current')
});
Displaying the Category List:
In the Topic view ('category' is the property of Topic that says what category the topic is in):
{{category-list current=category}}
This all works, but I have a feeling that it is not the correct way to go about solving the problem. Components strike me as something that should be very reusable, and this really only is just an encapsulated part of one view.
Second Attempt
Now I am trying to use the category list as a view. So now instead of just a Component, I have a CategoriesRoute (not hooked up in the router though), a CategoriesController, and a CagetoriesView.
When the Topic resource loads up, it sets up the categories controller with the loaded model data:
App.TopicRoute = Ember.Route.extend({
model: function (params) {
return Em.RSVP.hash({
topic: this.store.find('topic', params.topic_id),
categories: this.store.find('category')
});
},
setupController: function (controller, context) {
this._super(controller, context.topic);
this.controllerFor('categories').set('model', context.categories);
}
});
The CategoriesController is just a standard array controller:
App.CategoriesController = Ember.ArrayController.extend({});
The CategoriesView is also very simple:
App.CategoriesView = Ember.View.extend({
templateName: 'categories',
tagName: ''
});
And finally the categories template:
<script type="text/x-handlebars" id="components/category-list">
{{#each categories}}
{{this.name}}
<br />
{{/each}}
</script>
I am rendering the list with the {{render}} helper in the Topic template:
{{render "categories" }}
The Problem:
The above seems to work fine in terms of showing the list of categories. However, I can't figure out how to tell the view which category is active (the category in which the topic is in).
I looked around on the internet, and it looks like the {{render}} helper used to allow for an optional options hash (which is how I solved the problem with a Component in my first attempt), but it seems to have been removed in some recent version of Ember.
I found a StackOverflow entry that mentioned making my own {{render}} helper, but it looks like that is to dynamically change models, or something of that nature. I'd like to keep the models backing for the categories view, I just need to be able to tell it which category is active via the Topic.category property.
Or, was my first attempt the better solution? I am very new to Ember, so I'm not sure what would be the best strategy here. My instinct tells me I should use my second attempt rather than the first, but I am not positive.
Any help is very appreciated.

You are right in saying that components must be re-usable everywhere and should not be tied to any particular controller. I would use a view for this. The way I would do is, I would have a
App.CategoriesController = Em.ArrayController.extend({
itemController: 'category',
//some other stuff here
});
for categories and then have an itemController called
App.CategoryController = Em.ObjectController.extend({
setActiveState: function() {
if (this.get('parentController.current.id') === this.get('content.id')) {
this.set('active', true);
}
}
});
and in the template, you could say
{{#each category in categories}}
{{category.name}}
{{/each}}

Related

Structuring ember.js code

I'm trying to figure out the "right" way to structure a part of my Ember app
A basic overview:
Let's say I have a Course model defined similarly:
var Course = DS.Model.extend({
videos: hasMany('video', { async: true }),
categories: hasMany('category', { async: true })
});
Each video has a category associated with it.
In my CourseView or CourseController, how am I supposed to get a list of the videos for each category?
First, I tried using a computed property, but any call like this.get('model').get('videos') returns a Promise – not what I want.
Next, I saw some posts about using that and then setting properties on the controller after the Promise resolves, so the View would update when the properties changed, but this involves a lot of almost-callback hell and I'm hoping there's a better way to approach the problem.
Is there any way I can "load" the videos/categories before the view gets rendered? Ideally I'd be able to do something like:
{{#each category in categories}}
// render category-related stuff and only videos in this category
{{/each}}
Would it be better to have all of the videos on the Category? Is there any way I can define/add properties to a model without having them be sent to the server? I guess this can be seen as a general question about API design as well – should videos be under categories? Essentially there'll be a list of Courses, each with its own set of categories. Every video must fall under one category. After typing it out this way it makes sense for videos to be under categories if all of them must be under some category...
Edit: Here's an implementation I just went with for the sake of time:
My Category model now looks something like this:
var Category = DS.Model.extend({
name: attr('string'),
description: attr('string'),
videos: hasMany('video', { async: true })
});
In the Course template:
...
{{#each category in model.categories}}
{{render "course/category" category}}
{{/each}}
...
In the course/category template:
<h2 class="sub-title">{{model.name}}</h2>
{{render "course/video-gallery" model.videos}}
where course/video-gallery is a something I built for a collection of Videos.
This works fine, but now there are a bunch of Video models everywhere; on the Course (all of them) and on each Category (all of them that belong to that Category).
I don't know your specific use-case, but if I was designing something similar, I would have my video class have a hasMany('categories') relationship, expressing the real world notion that a video can be "tagged" with multiple categories, then the models would look something like this:
var Course = DS.Model.extend({
videos: hasMany('video', { async: true }),
categories: hasMany('category', { async: true })
});
var Video = DS.Model.extend({
categories: hasMany('category'),
//rest of the attributes
});
var Category = DS.Model.extend({
name: DS.attr(),
description: DS.attr(),
//other attributes
});
now, you need a list of videos for each category, to break it down into a simpler problem, lets imagine you can get all the videos for one category. That's easy enough, all we need to do is get the list of videos and select those that have this category's id in their categories list. So the resulting code in ember, if made a computed property on the controller would look like this:
videosByCategory: function() {
var cats = this.get('categories'), //grab courses' categories.
videos = this.get('videos'); //grab the courses' videos
return videos.filter(function(video) {
var videoCategories = video.get('categories');
return Ember.EnumerableUtils.intersection(videoCategories,cats).length > 0;
});
}.property('videos'),
now, in the template, if you access this computed property like this:
{{#each video in videosByCategory}}
//whatever goes here
{{/each}}
Hope this helps. I would sideload the videos with the course, when downloading the course.

How do you render multiple templates with one route controller using iron-router?

URL to current version of my project
Here is the code for my project
Background:
I am making a blog using meteor and iron-router. I want to use a single controller for several different "category pages," which filter a list a blog articles in the yield region.
The Problem:
The article list does not get rerendered when the URL changes. I.e. the article list is not reactive. Interestingly, if I navigate back to the home page, the correct article list shows up.
The Question:
How do I make that article list change when I change between different routes on the category route controller?
Some example code:
Please note that the code for this whole project is available here.
Here is my Route Controller:
CategoryController = RouteController.extend({
action: function(){
this.render();
},
template: 'category',
data: function(){
return {category: this.params.category};
}
});
CategoryController.helpers({
articles: function(){
return Articles.find({category: this.params.category});
}
});
And here is the template it is rendering:
<template name='category'>
<div class="container">
<h2>{{category}}:</h2>
<ul>
{{#each articles}}
<li>
{{#linkTo route="article.show"}}
{{title}}
{{/linkTo}}
</li>
{{/each}}
</ul>
</div>
</template>
Resources/Updates:
Read this article on Meteor Reactivity and the Deps Package. Very interesting, but after trying some Deps.autoruns in different places, I don't think that this is the answer.
Currently trying to make different "category" routes inherit from the controller.
The article list does not change because the Template helper is not using a reactive data source. You may use the RouteController.getParams method to establish a reactive dependency on route parameters as shown below.
CategoryController.helpers({
articles: function(){
var controller = this;
var params = controller.getParams();
return Articles.find({category: params.category});
}
});
From Iron Router documentation:
Note: If you want to rerun a function when the hash changes you can do
this:
// get a handle for the controller.
// in a template helper this would be
// var controller = Iron.controller();
var controller = this;
// reactive getParams method which will invalidate the comp if any part of the params change
// including the hash.
var params = controller.getParams();
By default the router will follow normal browser behavior. If you
click a link with a hash frag it will scroll to an element with that
id. If you want to use controller.getParams() you can put that in
either your own autorun if you want to do something procedural, or in
a helper.

Ember.js not re-rendering template after Model loaded

I'm learning Ember.js using a Ruby on Rails API server. I've got the routes, template, and model all setup and working - but the template is never re-rendered once the data has been loaded from the API server. I'm not getting any error messages, and I know the customer is being loaded from looking at the Ember inspector.
Customer list is supposed to be displayed after start
Customer list is being loaded correctly from the API server:
Router
// javacripts/router.js
App.Router.map(function() {
this.resource('customers', { path: "/" });
});
Customers Route
// javascripts/routes/customer_routes.js
App.CustomersRoute = Ember.Route.extend({
model: function() {
return this.store.find('customer');
},
renderTemplate: function() {
this.render('customers/index');
}
});
Customer Model
// javascripts/models/customer.js
App.Customer = DS.Model.extend({
name: DS.attr('string')
});
Customer Index Template
// javacripts/templates/customers/index.js.handlebars
<ul>
<li>start</li>
{{#each customers}}
<li>{{name}}</li>
{{/each}}
</ul>
Store
// javacripts/store.js
App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
namespace: 'api/v1'
});
Instead of
{{#each customers}}
It should read either
{{#each controller}}
{{name}}
{{/each}}
or
{{#each customer in controller}}
{{customer.name}}
{{/each}}
I have recently posted two screencasts. One showing how to get started with a new application, and one showing how to setup Grunt:
https://www.youtube.com/watch?v=T-s34EVSE_0
https://www.youtube.com/watch?v=kPaHt_F3VcU
You might also get some use out of a talk I gave earlier this year as well, which goes through developing a simple application during the talk, including Ember Data.
https://www.youtube.com/watch?v=KH5RreHtaaQ
Your customers/index template is referencing a "customers" collection that doesn't exist.
Your route's model hook is returning an array of records, which makes Ember generate an Ember.ArrayController with its model set to your array of customers. It doesn't have a property called "customers", so the {{#each customers}} doesn't have anything to iterate over. If you change it to just {{#each}} (because this in this scope references the controller, which is array-like) or {{#each model}} (to explicitly access the model array of the ArrayController), it should work correctly.
Also, your renderTemplate hook in the Route is the default behavior, so you can just delete it.
Incidentally, I'd recommend just using an Ember JSBin or something while you're starting out and learning the basics, so when you need to ask for help, you can just link to the bin, and people have live code they can work with to help you out, with a minimum of effort. That low barrier to entry makes a big difference to people who are doing free work for internet points.

Emberjs: How to render a view with the view name in variable?

i would like to add some cards to a board, all cards are very different. so i want to create different view for each card that can bind different events and template. I set a 'type' property in the card model which to distinguish the cards.
the board template look like below:
{{#each card in cards}}
{{render card.type card class="card"}}
{{/each}}
However, the first argument for the render help can not be a variable, it can only be the card view name string.
anyone know how to achieve this?
As you already mentioned the built-in render helper only accepts a string to lookup the template to render, therefore one possible solution would be to write your own custom render helper, which then after getting the correct name with card.get('type') delegates the rendering to the built-in render helper. This could look something like this:
Ember.Handlebars.registerBoundHelper('renderCard', function(context, card, options) {
return Ember.Handlebars.helpers.render.call(context, card.get('type'), 'card', options);
});
After that, you could use it like this in your template:
{{#each card in cards}}
{{renderCard this card}}
{{/each}}
Edit
I've also added a basic jsbin that shows it working.
Edit 2
Edited the jsbin again, using object data to be rendered into the template, see here.
Edit 3
Lamentably the DS.FixtureAdapter does not support embedded records which is what you need to make it work. But you could configure you orignal DS.RESTAdapter like this:
App.Adapter = DS.RESTAdapter.extend();
App.Adapter.map('App.Dashboard',
cards: {embedded: 'always'}
);
App.Store = DS.Store.extend({
adapter: App.Adapter
});
This way the card records are always loaded with the parent record. I guess doing this change would make the call to card.get('type') in you handlebar helper return the value rather then undefined.
Hope it helps.
For Ember 1.9 solution above doesn't work. Use another way to register helper:
Ember.Handlebars.registerHelper 'renderDynamic', (context, model, options)->
contextString = options.hash.contextString
model = options.data.view.getStream(model).value()
return Ember.Handlebars.helpers.render(model.get('type'), contextString, options)
And in the template:
= hb 'renderDynamic "this" currentModel contextString="curremtModel"
or in handlebars:
{{renderDynamic "this" currentModel contextString="curremtModel"}}

Limit the results of {{#each a in b}}

I have a Model called Course that has an array variable called users. Is there a way to just limit the results of, let say, IDs from users rather than the {{#each}} rendering every single id?
Such that
<script type="text/x-handlebars" data-template-name="course">
{{#each user in users 5}}
{{user.name}}
{{/each}}
</script>
Will show the first 5 IDs rather than every single id stored in users?
Here is the Ember-data model that I am using
App.Course = DS.Model.extend({
//Attributes
name: DS.attr('string'),
description: DS.attr('string'),
//Relations
users: DS.hasMany('App.User'),
});
I tried multiple times at creating a registerHelper, but where it always seems to go wrong is when an 'a in b' is present.
Any help or ideas would be appreciated. Thank you very much!
Rather than modifying the #each helper you could take a different approach to show only a limited amount of items when looping over the array with the #each helper.
See this for a possible implementation:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [
Ember.Object.create({id:1, name:'Red'}),
Ember.Object.create({id:2, name:'Green'}),
Ember.Object.create({id:3, name:'Blue'})
];
}
});
App.IndexController = Ember.ArrayController.extend({
limitedContent: function() {
return this.get('content').splice(0, 2);
}.property('content')
});
The .property('content') defines a binding to the controller's content and takes care that the #each helper rerenders when the content changes.
And then in your template you loop over the limitedContent rather then the content:
<script type="text/x-handlebars" data-template-name="course">
{{#each color in limitedContent}}
{{color.name}}
{{/each}}
</script>
And here a working jsbin that show the concept mentioned.
Hope it helps
I was using the method in the accepted method, but ran into an issue with it.
The problem lay in that using splice in ember modifies the underlying data:
filtered: function() {
return this.get('content').splice(0,2);
}.property('content'),
splice removes the elements from the underlying content.
A better method is to use Ember's built-in slice function, changing the above code into
filtered: function() {
return this.get('content').slice(0,2);
}.property('content'),
And that's it, now it doesn't modify the underlying array, because slice returns a new array and leaves the underlying data untouched.
JSBin showing that splice modifies underlying data:
http://emberjs.jsbin.com/ridixawimugo/1/
JSBin with the fixed solution:
http://emberjs.jsbin.com/voceye/1/

Categories