How to add dynamic dropdown to template without changing model? - ember.js

So I had a route: /tournaments/setup/:tournament_id in my ember app, and it displayed various fields from a model.
But then I find that I'll need other models on the same page. For example, suppose I want the list of all players, so that I can make a dropdown for selecting players to join a tournament.
I can pass an object:
this.transitionTo('tournaments.setup', {tournament: tournament, players: players});
and that works once I change the handlebars template to reflect the change of context.
But now the URL is wrong. The URL is /tournaments/setup/undefined. I'm sure there is a way to explicitly handle this also, but I don't know what it is ...
... and I feel like I'm losing the path. Is there a more conventional way to do what I'm doing?
Update: I've found that I can get the URL to work by adding an id:
this.transitionTo('tournaments.setup', {id: tournament.id, tournament: tournament, players: players});
but I'm still wondering if I'm doing this the right way.

What you want to do is load the models in your route and stick them in a controller to user them. I usually do this in the beforeModel hook. So in your App.TournamentsSetupRoute do something like this:
App.TournamentSetupRoute = Ember.Route.extend({
beforeModel: function() {
var self = this;
var players = this.store.find('player');
players.then(function() {
self.controllerFor('players').set('model',players);
});
}
});
So now, when ever this route is entered, the players will be fetched from your API and stored in a controller. In your controller, just add the players controller to needs so you can access it.
App.TournamentSetupController = Ember.ObjectController.extend({
needs: ['players']
});
You can access the players models anywhere in your controller by doing this.get('controllers.players.model); and iterate over it in your template like so:
<select>
{{#each player in controllers.players.model}}
<option>{{player.name}}</option>
{{/each}}
</select>
Hope that helps! I haven't tested the above code, so there may be a few typos, but it should help get ya there.

Related

rendering templates with #link-to and dynamic segments

I have a simple app to show a list of clients, and a detail page for each specific client.
So basically, two URLs:
/clients/
/clients/:slug/
I've been having some trouble rendering the results from my Client model into a template, specifically when using the #link-to helper with dynamic segments.
I've found two methods for doing so, and while I can get them both to work, I don't know which one is the best and why.
Here is some (simplified) code from my app to explain.
// app/models/client.js
export default DS.Model.extend({
name: DS.attr('string'),
slug: DS.attr('string')
});
(my actual model is larger, but these are the only relevant fields)
and here are my routes:
// app/router.js
Router.map(function() {
this.route('clients');
this.route('client', { path: 'clients/:slug' });
});
NOTE: I didn't nest the routes, because I didn't want to use the {{outlet}} nested template feature.
Here is the route for Clients, where I retrieve my list of clients
// app/routes/clients.js
export default Route.extend({
model: function() {
return this.get('store').findAll('client'); // fetch all Clients
}
});
Finally, here is the route to fetch info for a single Client:
// app/routes/client.js
export default Route.extend({
model: function(params) {
return this.store.query('client', {
filter: { slug: params.slug } // fetch one specific client, by slug
});
}
});
Everything works fine up to here, but my issue starts when displaying the model data in the templates.
There are two "ways", which one is correct??
OPTION A:
//app/templates/clients.hbs
// list all clients using #each
{{#each model as |client|}}
{{#link-to "client" client}} // pass the "client" object to generate my dynamic routes
{{client.name}}
{{/link-to}}
{{/each}}
Clicking on any of the generated links will render the client detail template, client.hbs
//app/templates/client.hbs
<h1>Client - {{model.name}}</h1>
In this example, I can use model.name to render my model object. That's fine, until I refresh the page! Then the info returned by model.name is obliterated. Same thing if I try to visit the URL directly. I have to go back to /clients and click on the link again to see my client's name.
I then looked for another way to display my data, where the model information would survive a page reload.
OPTION B:
After much reading I found a suggestion to use the specific client slug/id as param for #link-to
// app/templates/clients.hbs
// list all clients using #each
{{#each model as |client|}}
{{#link-to "client" client.slug}} // specifically use the actual slug/id, and not the object
{{client.name}}
{{/link-to}}
{{/each}}
But... by passing client.slug instead of client as parameter to #link-to, I can no longer use model.name to display my data. It simply returns nothing!
//app/templates/client.hbs
<h1>Client - {{model.name}}</h1> <-- model.name now returns nothing D:
However, using a loop DOES work for some reason:
//app/templates/client.hbs
{{#each model as |client|}}
<h1>Client - {{client.name}}</h1> <-- using loop even though I only have one record :(
{{/each}}
So option B works, and the information is displayed correctly after a page reload, or a direct visit from the URL.
But I'm now using a loop to display a single record! And I can't find an alternative solution that actually works.
To summarize:
option A feels like the correct way, but reloading the page obliterates the data.
option B actually works and returns the data, but I have to use a loop to iterate through a single record, which doesn't feel idiomatic / efficient.
I'm extremely confused, any clarification would be greatly appreciated.
Thanks in advance!
This is one of those fun 'magic' things that is supposed to simplify onboarding and make Ember seem easy - but really just caused the most confusion for what should be the most common use-case for the router.
At least it's in the guides now.
https://guides.emberjs.com/v2.12.0/routing/specifying-a-routes-model/#toc_dynamic-models
"Note: A route with a dynamic segment will always 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), and a model context is provided (second argument to link-to), then the hook is not executed. If an identifier (such as an id or slug) is provided instead then the model hook will be executed."
So, that's one thing...
But the other thing - is that you are getting back an array - because you are using query + filter - to create an array of records.
So, if you use queryRecord() - which is meant to get 1 record - then you'll get what you want. : )
I'd just add that {{outlets}} are cool. Here's how I normally do it - but t I can see that my route always loads all of the data this way... / which I normally want - but could see how in many cases - it wouldn't be desired.
And also - if you run into any more whacky params issues... investigate the underscore in the dynamic segment - because it's probably trolling you.

Refresh a rendered template when the model is returning store.queryRecord

I have a route that displays 5 categories. Each category is intended to only have one child. I created a child route and pass the selected category type id as queryparamter. In the child route model() I use the parameter with store.queryRecord() to query the backend to either return the record that matches that type OR nothing. This seems to work fine as long as a record exists. The problem I am running into is if I select a category that doesn’t have a child record. When nothing is returned from queryRecord the template continues to display the previous data. I can see the network request completing successfully and it returns an empty array. If I refresh the page the template correctly shows that there is no model data.
I’ve been struggling all day trying to find a way to refresh the template when the model no longer has a record. I have a feeling I am going at this backwards, I would be grateful for any pointers.
Parent:
export default Ember.Route.extend(AuthenticatedRouteMixin,{
user: Ember.inject.service('user'),
model() {
var user = this.get('user');
return this.store.findAll('strategic-priority',{ location: user.get('selectedLocationId'), year: user.get('selectedYearId') });
}
});
HBS
{{#each model as |strategic-priority|}}
{{#link-to 'priority-area.goal' (query-params priorityArea=strategic-priority.id) class="list-group-item"}} {{strategic-priority.label}} - {{strategic-priority.text}} {{/link-to}}
{{/each}}
Child:
export default Ember.Route.extend({
user: Ember.inject.service('user'),
queryParams: {
priorityArea: {
refreshModel: true,
replace: false,
}
},
model(params) {
Ember.Logger.debug(params); //I see this is in the console so I know this code is being called each time
var user = this.get('user');
return this.store.queryRecord('goal',{ location: user.get('selectedLocationId'), year: user.get('selectedYearId'),priority: params.priorityArea});
}
});
What you could try is to wrap the template that displays a category with {{#if hasModel}}. So something like
{{#if hasModel}}
... your template ...
{{/if}}
and then in the controller for your route
hasModel: Ember.computed.notEmpty('model')
I don’t know if this is the correct answer but my workaround was to change the model to use store.query rather than queryRecord. In my template I did a {{#each model as |xx|}} even though I only expected one record.
I was also able to avoid the situation in most cases by using hasMany. It is a bit cumbersome to gather the data on the backend (at least at my skill level with php and zf2) but in the end it seems to work pretty nice.
Thanks for advice.

How to pass parameters to an Ember Route

Please see: http://emberjs.jsbin.com/xakok/1/edit
How do I do the following? The categories display. When user clicks on a category, I want to bring up a list of links that belong to the category that was clicked. Ember seems to be by-passing my LinkRoute all together. Thanks
When you provide a model to the link-to helper it will skip the model hook (it built the url based on the model, and assumes that's the model that would be used for that route).
That being said you need to handle the case where you refresh the page instead of hitting the page using the link-to.
Solving the first, we can now assume the category model is being sent to the links route as its model. So we can update the template to iterate over the links on the category sent in. (you could also say each link in model.links, where the category is your model).
{{#each link in links}}
{{link.title}}<br/>
{{/each}}
But you need to be able to handle the case where we refresh the page as well. So we change the link route to mimic the behavior and return the same type of model the link-to is passing in.
App.LinkRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('category', params.category_id);
}
});
Lastly, using the fixture adapter, when you define a hasMany or belongsTo as 3 or [1,2,3] you need to specify those relationships as async.
App.Category = DS.Model.extend({
name: DS.attr('string'),
links: DS.hasMany('link', {async:true})
});
Example: http://emberjs.jsbin.com/fexelera/1/edit
Lastly, thanks for providing source and all that was necessary for your problem, the jsbin really helps people answer questions easily.

How to use itemControllerClass to achieve a singleton like experience with ember.js

I've got a fairly simple form but it should never carry any state with it. I started reading an older discussion about how you can use itemControllerClass to get a "singleton" class created each time you enter the route.
If I want to use this how would I plug this into the template and wire it up from the parent controller?
Here is what I'm guessing you would do from the javascript side
App.FooController = Ember.Controller.extend({
itemControllerClass: 'someclass_name'
});
The only "must have" is that I need to have access to the parent route params from the child singleton controller.
Any guidance would be excellent -thank you in advance!
Update
Just to be clear about my use case -this is not an ArrayController. I actually just have a Controller (as shown above). I don't need to proxy a model or Array of models. I'm looking for a way to point at a url (with a few params) and generate a new instance when the route is loaded (or the parent controller is shown).
I'm doing this because the template is a simple "blank form" that doesn't and shouldn't carry state with it. when the user saves the form and I transition to the index route everything that happened in that form can die with it (as I've cached the data in ember-data or finished my $.ajax / etc)
Anyone know how to accomplish this stateless behavior with a controller?
I'm betting you're talking about this discussion. It's one of my personal favorite discoveries related to Ember. The outcome of it was the itemController property of an ArrayController; I use it all the time. The basic gist of it is, when you're iterating over an array controller, you can change the backing controller within the loop. So, each iterating of the loop, it will provide a new controller of the type you specify. You can specify the itemController as either a property on the ArrayController, or as an option on the {{#each}} handlebars helper. So, you could do it like this:
App.FooController = Ember.ArrayController.extend({
itemController: 'someclass'
});
App.SomeclassController = Ember.ObjectController.extend({});
Or, like this:
{{#each something in controller itemController='someclass'}}
...
{{/each}}
Within the itemController, you can access the parent controller (FooController, in this case), like:
this.get('parentController');
Or, you can specify the dependency using needs, like you ordinarily would in a controller. So, as long as the params are available to the parentController, you should be able to access them on the child controller as well.
Update
After hearing more about the use case, where a controller's state needs to reset every time a transition happens to a particular route, It sounds like the right approach is to have a backing model for the controller. Then, you can create a new instance of the model on one of the route's hooks; likely either model or setupController.
From http://emberjs.com/api/classes/Ember.ArrayController.html
Sometimes you want to display computed properties within the body of an #each helper that depend on the underlying items in content, but are not present on those items. To do this, set itemController to the name of a controller (probably an ObjectController) that will wrap each individual item.
For example:
{{#each post in controller}}
<li>{{title}} ({{titleLength}} characters)</li>
{{/each}}
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
In some cases it is helpful to return a different itemController depending on the particular item. Subclasses can do this by overriding lookupItemController.
For example:
App.MyArrayController = Ember.ArrayController.extend({
lookupItemController: function( object ) {
if (object.get('isSpecial')) {
return "special"; // use App.SpecialController
} else {
return "regular"; // use App.RegularController
}
}
});
The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.

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.