Ember - #each pass the instance of the model - ember.js

For a small webapp I'm trying to do the following:
I have a list of objects (achievement-model)that's being served through a json api
Router
export default Ember.Route.extend({
model:function(){
return this.store.find('achievement');
});
});
Model
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
});
Template
{{#each a in model}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description}}</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
The setup of the app is that there is a list of achievements. I want one list of achievements in a database. Every user that logs in can add with the button his own achievements to his profile. If a user logs in he should see the list of all the achievements but the one he already added to his profile should have a green background color and the button removed. I know this can be done with if-statements etc.
The problem however is, how do i pass the specific model to the controller so i can log this to the userprofile? I tried the following:
<button {{action 'addThis' a}}/>
and then in the controller
actions:
addThis: function(obj){
console.log(obj);
});
which logs the object, but somehow I can't acces it to get let's say the name or id to copy it to the user-profile.
I also don't know if this is the best approach for what I'm trying to achieve?
Edit
I think this has something to do with promises. I can see the data is logged in the above console.log. I just don't know how to target it. it's wrapped in _data. I tried the afterModel to wait untill everything's loaded, but that doesn't seem to work.

What you could is to use an ItemController, e.g. which handles each item in the ArrayController,
e.g.
{{#each a in model itemController="achievement"}}
<div>
<h4>{{a.name}}</h4>
<p>{{a.description</p>
<button {{action 'addThis'}}/>
</div>
{{/each}}
Since the itemController is "achievement", by naming convention, the controller becomes
App.AchievementController = Ember.ObjectController.extend({
init: function() {
var name = this.get('name');
var description = this.get('description');
}
});

Related

Emberjs: Accessing parent route model which is a Promise (master-detail)

I've got master-detail page layout as on image. I access this page through #/masters/:master_id app url.
Routes a defined as follows:
App.Router.map(function() {
this.resource('masters', { path: '/masters' }, function() {
this.route('detail', { path: '/:master_id' });
});
});
App.MastersRoute = Ember.Route.extend({
model: function() {
return App.DataStore.getData('/api/masters'); //returns Promise!
},
setupController: function(controller, model) {
controller.set("content", model);
}
});
App.MastersDetailRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor("masters").find(function(item) {
return item.get("id") == params.master_id;
});
}
});
Templates:
<script type="text/x-handlebars-template" data-template-name="masters">
<div id="masters-grid">
{{#each master in model}}
<div {{action "show" master}}>
{{master.name}}
</div>
{{/each}}
</div>
<div id="detail">
{{outlet}}
</div>
</script>
<script type="text/x-handlebars-template" data-template-name="masters/detail">
{{model.name}} <br />
{{model.age}} <br />
{{model.address}} <br />
</script>
When clicking through masters in the grid I want to show their details in Detail outlet and I do not want to reload all masters from API when changing the master selection.
I have a problem with MastersDetailRoute's model, because this.modelFor("masters") returns undefined. I think, it is caused by returning Promise in model hook. Any idea or workaround how to access one item from Masters model or controller in "child route" model hook?
I see a few things here.
when defining routes that have the same url as the route name theres no need to specify the path
the detail route should also be a resource as it is a route backed by a model
In the Masters route returning a promise is correct and supported natively by ember. The route wont be resolved until the promise is.
setup controller isn't required
its usually best to do the required api call to fetch the individual record in the detail route. This will only be used when loading the page for the first time (if f5 ing or coming from a bookmark)
in your masters template you can use id instead of typing data-template-name or better still look into use ember-cli/brocolli or grunt to precompile your templates
to prevent ember refetching your model when selecting a row use the handlebars helper link-to
{{#link-to 'masterDetail' master}}
{{master.name}}
{{/link-to}}
just to clarify, using link-to in this way passes the object specified in the second parameter as the model to the specified route (first parameter). In your case master will now be set as the model to the master detail route.
in masters detail theres no need to type "model" the default context (i.e. the value of "this") in your template is the controller, then if the property is not found on the controller it looks for it in the model.
Hope this helps

Creating a new record not pulling data from template fields

I am attempting to create a new record, however none of the data from the fields is being passed automatically, as I expected Ember to (from what I've read).
My template:
<form {{action save content on="submit"}}>
{{input value=name}}
<button type="submit"}}>Next</a>
From what I've read content is an alias for model and interchanging these makes no difference.
My route:
App.CampaignsNewRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.name);
}
},
model: function(controller) {
return this.store.createRecord('campaign');
}
});
And my controller:
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign Setup'
});
When I hit 'Next' it logs undefined. Logging just the campaign shows it's an Ember model, but without the name attribute. name is defined on the campaign model. Setting the input to {{input value=content.name}} places the name attribute within the model returned, but it's still undefined. Am I missing anything in this process? The EmberJS site doesn't show how to do this, from what I can find.
--
As a side note: I was originally using App.CampaignsNewController = Ember.Controller.extend as my model was returning a hash of promises, one of which is an array and Ember didn't like me using either array or object controller. I simplified it to the above to verify it wasn't that which was causing the issue. So any solution taking this into account would be wonderful.
Edit: I can access the template fields by doing this.get('controller').get('name') but surely that is not necessary? Changing my controller to a Ember.Controller.extend also stops that from working, would love to know why. Clarification on best practice here would still be wonderful!
Edit2: this.get('controller.content').get('name') works if the controller is simply an Ember.Controller as opposed to Ember.ObjectController and the template has {{input value=content.name}}. I'll work with but hopefully someone can clarify this is the correct way.
ObjectController is the way to go here. You would have it backed by one particular model, your new model, and you would add additional properties to the controller for use in the template.
Code
App.IndexRoute = Ember.Route.extend({
actions: {
save: function(campaign) {
console.log(campaign.get('color'));
}
},
model: function() {
return Ember.RSVP.hash({
record: this.store.createRecord('color'),
all: this.store.find('color')
});
},
setupController: function(controller, model){
this._super(controller, model.record);
controller.set('allColors', model.all);
}
});
App.IndexController = Em.ObjectController.extend({
});
Template
In the template any time you want to access anything on the model backing the template, you can just access it as if the model is the current scope.
{{name}}
if you want to access any of the properties that exist on the controller you would use the property name that it is on the controller.
{{allColors.length}}
Here's an example:
<form {{action save model on="submit"}}>
Color:{{input value=color}}<br/>
<button type="submit">Next</button>
</form>
<ul>
{{#each item in allColors}}
{{#unless item.isNew}}
<li>{{item.color}}</li>
{{/unless}}
{{/each}}
</ul>
One last tip, always use getters and setters ;)
Ember Data hides the properties, they don't live right on the object, so campaign.name will return undefined forever and ever. If you do campaign.get('name') you'll get a real response.
With the example: http://emberjs.jsbin.com/OxIDiVU/792/edit

rendering one item from a list in Ember

In my Ember app, I get a list of all the restaurants using an ajax call copied from Discourse co-founder's blog post http://eviltrout.com/2013/02/27/adding-to-discourse-part-1.html
App.Restaurant.reopenClass({
findAll: function() {
return $.getJSON("restaurants").then(
function(response) {
var links = Em.A();
response.restaurants.map(function (attrs) {
links.pushObject(App.Restaurant.create(attrs));
});
return links;
}
);
},
I have a Restaurants route set up which calls the findAll shown above and renders it into the application template
App.RestaurantsRoute = Ember.Route.extend({
model: function(params) {
return(App.Restaurant.findAll(params));
},
renderTemplate: function() {
this.render('restaurants', {into: 'application'});
}
});
The restaurants are displayed as a restaurants template like this with a link to each individual restaurant. I've also included the restaurant template
<script type="text/x-handlebars" id="restaurants">
<div class='span4'>
{{#each item in model}}
<li> {{#link-to 'restaurant' item}}
{{ item.name }}
{{/link-to }}</li>
{{/each}}
</ul>
</div>
<div class="span4 offset4">
{{ outlet}}
</div>
</script>
In the Ember router, I have a parent/child route set up like this
this.resource("restaurants", function(){
this.resource("restaurant", { path: ':restaurant_id'});
});
Therefore, I'm hoping that when I click on the link to a particular restaurant in the restaurants list, it'll insert this restaurant template into the outlet defined in the restaurantS (plural) template
<script type="text/x-handlebars" id="restaurant">
this text is getting rendered
{{ item }} //item nor item.name are getting rendered
</script>
This restaurant template is getting rendered, however, the data for the item is not getting displayed.
When I click {{#link-to 'restaurant' item}} in the list, item represents that restaurant.
In this setup, does Ember need to make another ajax call to retrieve that particular item, even though it's already been loaded from the findAll call?
In the event that I do need to query for the individual restaurant (again) I created a new route for the individual restaurant
App.RestaurantRoute = Ember.Route.extend({
model: function(params) {
console.log(params);
console.log('resto');
return App.Restaurant.findItem(params);
}
});
and a findItem on the Restaurant model
App.Restaurant.reopenClass({
findItem: function(){
console.log('is this getting called? No...');
return 'blah'
}
but none of those console.logs are getting called.
In the Ember starter video https://www.youtube.com/watch?v=1QHrlFlaXdI, when Tom Dale clicks on a blog post from the list, the post appears in the template defined for it without him having to do anything more than set up the routes (as I did) and the {{outlet}} within the posts template to receive the post.
Can you see why the same is not working for me in this situation?
When you navigate to the restaurant route, the item will be the model to this route.
So in your template, if you try
<script type="text/x-handlebars" id="restaurant">
this text is getting rendered
{{ model.name }}
</script>
You'll be able to see the name of the restaurant
And also the model hook is not called, and the further console.logs are not working,
because
Note: A route with a dynamic segment will only have its model hook called when it is entered via the URL. If the route is entered through a transition (e.g. when using the link-to Handlebars helper), then a model context is already provided and the hook is not executed. Routes without dynamic segments will always execute the model hook.
Hope everything will be clear now.

Ember.js arraycontroller call from view

I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.

text field filtering a list using ember + ember data

I'm new at using ember, but already familiar with it, basically following some tutorials here and there and reading the api docs. But tutorials don't go too deep into more complex topics.
These are the details: I already implemented a web page that shows a list of items. The following are the relevant code excerpts from different parts of the app.
// the data model, the view and the controller
App.Item = DS.Model.extend({
name: DS.attr('string')
});
App.ItemsController = Ember.ArrayController.extend();
App.ItemsView = Ember.View.extend({ templateName: 'items' })
// in the router's corresponding route
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('items', App.Item.find())
}
// in the handlebars template
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
The data for this list is loaded remotely via ember-data (notice the App.Item.find() call in the route's connectOutlet method above) and a handlebar template displays it, and dynamically updates the list as the data changes. Up to here this is basic ember.
Now I want to have a text field at the top of the list, and when the user types in this text field, the list should be updated, by filtering and showing only the items with a name that matches what the user is typing. The actual definition of what a matching name is, is irrelevant to this question. It could be those names that contain the typed string, or that start with it.
I know my next step is to include a textfield view on top of the list in the handlebars template:
<div class="search-bar">
{{view Ember.TextField}}
</div>
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
So my questions at this point are the following:
How do I refer to this text field in javascript code so I can attach a listener to it to detect when it changes?
And more importantly, what do I need to do inside this event listener so the list gets filtered?
I would like to know how to achieve it filtering data loaded locally, but also how to do it by loading the filtering data remotely everytime the user types.
I actually need to implement something slightly more complex than this, but knowing how to do this will help.
You can have a computed property on your controller that filters the content based on a text field.
App.ItemsController = Ember.ArrayController.extend({
// ...
searchedContent: function() {
var regexp = new RegExp(this.get('search'));
return this.get('content').filter(function(item) {
return regexp.test(item.get('name'));
});
}.property('search', 'content.#each.name')
});
Then you just bind your text field to controller.search. Example: http://www.emberplay.com#/workspace/2356272909
To filter data remotely you should have ember data load more items every time search changes. This can be done by sending an event to the router every time there is a change.