rendering templates with #link-to and dynamic segments - ember.js

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.

Related

How do you create nested dynamic routes with ember.js where child route replaces parent's template?

On Ember 3.15 (Octane)
I'm trying to create the following routes in my application
/cars
/cars/:car_id
/cars/:car_id/:model_id
The first two are fine and I've been able to create them using ember-cli
ember g route cars
ember g route cars/car
The problem with the third one is that I need the model template to be shown under the cars template, ie. replace the car template. So if I put an {{outlet}} on the car template, the model template is rendered in it fine but obviously with that {{outlet}} nothing happens when I navigate to /cars/5/2
My router looks like this
Router.map(function() {
this.route('cars', function(){
this.route('car', { path: ':car_id'}, function() {
this.route('model', { path: ':model_id' });
});
});
});
I've tried using an index route ember g route cars/car/index but the problem with this that the params are not available in the index route and can only be accessed on the car route.
As plan b I've created the car and model routes as top level routes (based on this answer), but I'd like to achieve the above because of a few reasons
it seems more logical ie, structuring the app based on the nesting of the routes
I have many nested routes and creating all of them as first level routes will become hard to maintain
ember doesn't apply the active class correctly with this configuration. For example if I have a navbar with Cars as an link, I'd want it to have the active styling on all three pages. But this doesn't work anymore because the second route will be called something like car and the third one something like model.
there are some issues with the urls that are created by default using <LinkTo />.
So if I have something like this in my car template
<ul>
{{#each #model.models as |model|}}
<li><LinkTo #route="model" #model={{model}}> {{model.title}} </LinkTo></li>
{{/each}}
</ul>
The actual link works properly in that it takes me to the correct model page, however, the url is shown as /cars/undefined/undefined. (Although this is fixable by passing #models={{array #model.id model.id}}, it seems like a workaround at this point)
The solution is indeed to use the index route.
So have cars.car.index and cars.car.model. However you should use the cars.car route to load the common car model. Then you can access it in the cars.car.index route with this.modelFor('cars.car').
Then you can access the params.car_id in the cars.car route, use it to load the car, and then access this car from the cars.car.index and cars.car.model routes with modelFor.

how to inject a store into a component (when using localstorage adapter)

Ember docs say to define a store like this
MyApp.Store = DS.Store.extend();
If you are looking up records in components, this doc says you can inject the store into the component like this
// inject the store into all components
App.inject('component', 'store', 'store:main');
However, I am using the local storage adapter which I define like this
App.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'my-namespace'
});
Therefore, I don't know how to inject this into the component (where I need to look up a record) following the above instructions.
Following the instructions of this SO answer, I tried to inject the store into a component by passing it in like store=store and/or store=controller.store
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}} </li>
or
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=controller.store}} </li>
The goal was then to be able to do this in an action in the componeent
var todo = this.get('store');
console.log(todo, "the new store");
todo.set('notes', bufferedTitle);
console.log(todo, "todo with notes set");
todo.save();
However, todo.save(); always triggers
Uncaught TypeError: undefined is not a function
Notice that I logged the store? this is what it shows
Class {_backburner: Backburner, typeMaps: Object, recordArrayManager: Class, _pendingSave: Array[0], _pendingFetch: ember$data$lib$system$map$$Map…}
If i inspect it(by opening the tree, which isn't shown here), it does indeed show that notes were set via todo.set('notes', bufferedTitle); however, it doesn't have any of the other attributes of my model that I defined for the index route, and this object doesn't have a 'save' method. Therefore, it doesn't seem to be the actual store, but rather just some backburner object.
I got the same results trying this SO answer where it says to get the store of the targetObject
var todo = this.get('targetObject.store');
Note, I also tried this, i.e. setting the store to be the store of the item.
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=item.store}} </li>
It should be noted that if I set the store in the component, I can print the store on the page by doing {{store}} which gives me
<DS.Store:ember480>
but I can't do var todo = this.get('store'); in the action that handles the click even in the application code.
Question, using the localStorage adapter, how am I able to look up a record in a component (with the aim of then being able to alter the record and then save it again)
Note, if it's important, I define a model for the (index) route like this
App.Index = DS.Model.extend({
title: DS.attr('string'),
version (unfortunately I don't know what version of Ember data or the adapter I'm using)
Ember Inspector
1.7.0
Ember
1.9.1
Ember Data
<%= versionStamp %>
Handlebars
2.0.0
jQuery
1.10.2
Update in response to request for more info
The code that sets up the problem is very simple.
here's the router (with a bad name for the resource :)
App.Router.map(function(){
this.resource('index', { path: '/'});
}
Here's the route that gets the record to use in the Index route
App.IndexRoute = Ember.Route.extend({
model: function{
var resource = this.store.find('index');
return resource;
}
});
I have an Index Controller which does nothing in particular for the component (unless I should be defining methods on the Controller that get triggered by component events)
In the html, I do this with handlebars to pass data to the component
{{#each item in items}}
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}}
{{/each}}
Then, in components/my-component, I have a label that when clicked is supposed to trigger an action that will let me edit one of the attributes on the model
<label> {{action "editTodo" on="doubleClick">{{notes}}</label>
that click triggers this code in App.MyComponent, which triggers the error that prompted this question
var todo = this.get('store')
todo.set('notes', bufferedTitle);
todo.save()
IMHO injecting store into components is not the best idea... By design, components should be isolated and shouldn't have any knowledge about the store.
In the doc you've given, it's written: In general, looking up models directly in a component is an anti-pattern, and you should prefer to pass in any model you need in the template that included the component.
However, if you really need it for some reason, then why not just to pass the variable store to the component?
{{my-component store=store}}
Then, you can pass the store from your controller only in the components where you really need that.
Injecting the store in all your components will most likely lead you to the bad design (although it seems tempting at first).
Here's an updated answer for Ember 2:
Ember Data's store is now a Service, and we can easily inject it into all Components via an Initializer, e.g. app/initializers/inject-store-into-components:
export function initialize(application) {
application.inject('component', 'store', 'service:store');
}
export default {
name: 'inject-store-into-components',
initialize,
}
Then, in your Components, you can access the store with this.get('store'). The obviates the need to directly pass the store as an argument to Components, which requires a lot of boilerplate in your templates.
Whilst the accepted answer is sensible for simple applications it is perfectly acceptable to inject a store into a component if that component doesn't have a relationship with the url, like side bar content or a configurable widget on a dashboard.
In this situation you can use an initializer to inject the store into your component.
However, initializers can be a pain to mimic in testing. I have high hopes that the excellent Ember.inject API that is testing friendly will extend beyond services and accommodate stores. (Or that stores will simply become services).
According to this docThe preferred way to inject a store into a component is by setting a store variable to the record, for example
{{#each item in arrangedContent}}
<li> {{my-component store=item}} </li>
{{/each}}
Then in application code, you can do
var store = this.get('store');
store.set('todo', bufferedTitle);

Ember.js how to design different representations of Data (with TodoMVC as an example)?

I would like to know what's the best way of designing the display of different representations of the same data model in Ember.js. To ask my question, I'll use the TodoMVC of Ember.JS, which has 3 representations of todo-data:
any todo, i.e. the entire todo list (TodosIndexRoute)
todos that are still active and incomplete (TodosActiveRoute)
todos that have been completed (TodosCompletedRoute)
Currently, you can see each of the 3 by clicking on the words at the bottom of the list, directing to a different URL each time. Since currently each representation has a route, it makes sense that each representation gets its unique URL. The main page displays the entire todo list (1.).
A. What is the best ember.js design to make the main page display all 3 representations (i.e. under just one URL)?
B. How about the best design that displays all 3 on the main page as well as on separate pages?
Currently I only figured out a clumsy way and made this modified TodoMVC app that shows incomplete and completed lists at the bottom of the page.
In index.html, I added new named lists:
{{#each todosactive itemController="todo"}}
{{ title }},
{{/each}}
In the js router, I copied TodosActiveRoute and TodosCompletedRoute into TodoIndexRoute, which is code duplication, very bad.
Todos.TodosIndexRoute = Ember.Route.extend({
setupController: function () {
var todos = Todos.Todo.find();
this.controllerFor('todos').set('filteredTodos', todos);
var todos_active = Todos.Todo.filter(function (todo) {
if (!todo.get('isCompleted')) {
return true;
}
});
this.controllerFor('todos').set('todosactive', todos_active);
...
});
I feel like I'm missing an elegant way of doing this, but my current ember.js knowledge is very limited. Should I use {{partial}}, {{render}}, render, or something else?
I tried {{ partial }} and {{ render }}, but I can't get them to display any data .
Thanks for helping out.
A) Ember tries to work really closely with urls. This is a good thing since if you want to share a url, the view should be consistent. The url is a powerful tool and each unique url should link to the same unique page. Having one url that links to multiple views isn't great, and certainly not shareable. If you have some time listen to some talks by Tom Dale and Yehuda Katz for an interesting overview of ember and what they're trying to do.
B) You can include different views on one page. Have a look at the guides, most notably on rendering templates and using helpers for more information on including different views under one url.
A) To display all 3 representations in one view, we actually just need the basic model in the single route. The key is for the controller to give you flavors of that model. The other important thing is to use data binding in the handlebars template. You can see the running version here.
In the IndexRoute, add a model that gets the plain todos list:
Todos.TodosIndexRoute = Ember.Route.extend({
model: function(params) {
return Todos.Todo.find();
},
...
Make a TodosListView that doesn't need to have anything:
Todos.TodoListView = Ember.View.extend();
In the controller, add 2 computed properties that returns the desired arrays:
Todos.TodosController = Ember.ArrayController.extend({
...
todosActive: function() {
return this.filterProperty('isCompleted', false);
}.property('#each.isCompleted'),
todosCompleted: function() {
return this.filterProperty('isCompleted', true);
}.property('#each.isCompleted'),
...
Finally, in the HTML template:
<script type="text/x-handlebars" data-template-name="todos">
Active:
{{#view Todos.TodoListView lalaBinding="todosActive"}}
{{#each view.lala}}
{{title}},
{{/each}}
{{/view}}
Completed:
{{#view Todos.TodoListView dataBinding="todosCompleted"}}
{{#each view.data}}
{{title}},
{{/each}}
{{/view}}
</script>
note the dataBinding.
Thanks to the folks on #emberjs irc for helping out.

How can I get My previous route?

How can I get my previous router in my current controller.
App.MyController = Em.ObjectController.extend({
next:function() { // This is my action helper in HBS
this.transitionTo('nextPage');
},
back:function() { // This is my action helper in HBS
// Here I need to dynamically identify my previous route.
// How can I get my previous route.
}
});
After having inspected the router object again, I don't see any property in there that will allow you to grab the last route. In pre 4 there was a property for the last route, but it was a difficult property to work with.
My solution is therefore the same as it was in pre 4: I'd create my own mixin to handle the routes that you navigate into, and from that list of routes, you can get whatever route you're after: the current one, the last one, et cetera...
jsFiddle here: http://jsfiddle.net/sMtNG/
Mixin
The first thing to do is create the mixin that will allow us to push the routes into a HistoryController. We can do this by creating a setupController method which of course gets invoked every time you move into a route.
App.HistoryMixin = Ember.Mixin.create({
setupController: function() {
this.controllerFor('history').pushObject(this.get('routeName'));
}
});
We are pushing the route into the HistoryController.
History Controller
Since we're currently pushing the routeName into a non-existent HistoryController, we'll need to go ahead and create that, which is absolutely nothing special.
App.HistoryController = Ember.ArrayController.extend();
Index Controller
Since the HistoryController stores the list of routes we've navigated into, we'll need it accessible on other controllers, such as the IndexController, we'll therefore use needs to specify in which controller it should be accessible.
App.ApplicationController = Ember.Controller.extend({
needs: ['history']
});
Implement Mixin
We now have everything we need to keep a track of the routes, and so we'll specify that our routes need to implement this mixin.
App.CatRoute = Ember.Route.extend(App.HistoryMixin);
Template
Last but not least, now that we have a HistoryController which our IndexController can access, and the mixin pushes each accessed route into the HistoryController, we can use our application view to output a list of the routes, and specify the last route. Of course in your case you'll need the last route minus one, but there's no sense in me doing everything!
<h1>Routes History ({{controllers.history.length}})</h1>
<ul>
<li>Last Route: {{controllers.history.lastObject}}</li>
{{#each controllers.history}}
<li>{{this}}</li>
{{/each}}
</ul>
I hope this gets you onto the straight and narrow.
Current solutions feel like the tail wagging the dog.
Simple solution is to do window.history.back()
Ember doesn't keep track of the router history, since it would be redundant. All browser already handle this by default.

How do I setup an Ember View class to be appended to a particular container?

Ember.View has a nice method called .appendTo("#container") which would allow me to specify a container div for the view. However, when I use the router and .connectOutlet method, an instance of my view is created automatically based on convention and is added to the page body element by default. Is there a way to configure the class definition of the view so that upon creation it will be inside my desired #container. Here is my view:
Jimux.BuildsView = Em.View.extend({
templateName: 'builds',
appendTo: '#jimux-header', #this was just a guess and did not work. but does some propery like this exist for the view?
tagName: 'div',
listVisible: true,
...
Another way to ask this question is: how do I tell Ember router to append a view to a particular item in the dom? By default the router appends the view to the body.
And here is the router bit:
# Connect builds controller to builds view
router.get('applicationController').connectOutlet("builds","builds", Jimux.buildsController)
To clarify, I dont want to put my whole Ember app in a container. I have several views in my application, and most of them are fine directly in the body. But there are a couple like the one mentioned in this question, which I want to put inside "#application-header" div.
You can specify the root element for your application object.
window.App = Ember.Application.create({
rootElement: '#ember-app'
});
Edit:
Having re-read your question, I think you should look into named outlets, so you could do something like:
<div id="application-header">
{{outlet builds}}
</div>
{{outlet}}
well..after understanding your question, i remember having same trouble. Also, thing is i didn't find any way to do this even after going through the Ember code. But later i understood that its for good purpose only. I know you already might have come across with handlebars with which we can acheive this. If we give a view a ID to get appended, we are constraining the application and the whole use of ember becomes useless. Ok coming to you question, as far as i know, we can acheive that appending mustache templates in you div element of HTML.
<div id="jimux-header">
{{view Jimux.BuildsView}}
</div>
This way we can use the Jimux.BuildsView where ever you want and as many times possible. The Beauty of Ember you have to say...
Just add rootElement in the application object.
var App = Ember.Application.create({
rootElement: '#container'
});