I am new to Ember.js, and I am building a web application that is using Ember.js and Ember-Data for its front-end technology. I am trying to understand what would be the best practice for when you might have multiple ember-data bound components on a page that use an independent model.
Here is kind of what I'm trying to do:
https://gist.github.com/redrobot5050/6e775f4c5be221cd3c42
(There's a link on the page to editing it within jsbin this gist. For some reason, I can't get a 'Share' URL off the vanity URL.)
I have a template like so:
<script type="text/x-handlebars" data-template-name="index">
<p>Options for graphics quality: </p>
<ul>
{{#each item in model}}
<li>{{item.setting}}</li>
{{/each}}
</ul>
<p>Currently Selected: {{model.selectedValue}}</p>
<p>Draw Distance Options:</p>
<ul>
{{#each item in dropdown}}
<li>{{item.distance}}
{{/each}}
</ul>
<p>Currently Selected Distance: {{selectedDistance}}
</p>
{{outlet}}
<button {{action 'openModal' 'modal' model}}>Change Settings</button>
</script>
In this template, all the data binds correctly and appears in scope. However, when I attempt to modify it within its modal dialog box, only Quality is bound to its Ember.Select view. I have attempted to force the binding in the IndexController with a controller.set but it does not appear to be working.
My IndexController looks like this:
App.IndexRoute = Ember.Route.extend({
model: function() {
var qualityList = this.store.find('quality');
console.log('qualityList=' + qualityList);
return qualityList;
//return App.Quality.FIXTURES;
},
setupController : function(controller, model) {
controller.set('model', model);
var drawDistanceList = this.store.find('drawDistance');
console.log('distanceList=' + drawDistanceList );
controller.set('dropdown', drawDistanceList);
controller.set('selectedDistance', 1);
//this.controllerFor('modal').set('dropdown', drawDistanceList );
}
});
The JSBin really shows off what I am attempting to do: I want to load/bind each of the drop downs independently from the same controller. The JSBin does not work correctly because I'm not really sure how to do this, just yet. I am posting to stackExchange to see if someone can modify this JSBin and show me what I'm doing wrong, or if someone can point me in the right direction, design-wise on how to solve this problem?
(For example, I think a possible solution could be to create the dropdowns as components, and load the data through their controller or pass it in as properties from the parent controller, but I really want to know what is the "The Ember Way").
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
qualityList: this.store.find('quality'),
drawDistanceList: this.store.find('drawDistance')
});
},
setupController: function(controller, model) {
controller.set('model', model.qualityList);
controller.set('dropdown', model.drawDistanceList);
}
});
Documentation for Ember.RSVP.hash used to be here: http://emberjs.com/api/classes/Ember.RSVP.html#method_hash. I'm not sure why it has disappeared.
For the moment, you can find it at: http://web.archive.org/web/20140718075313/http://emberjs.com/api/classes/Ember.RSVP.html#method_hash
Related
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
So I want to use query parameters in the URL of my application. The Ember guide describes the solution: http://emberjs.com/guides/routing/query-params/
Unsuccessfully I tried it out in my ember-cli project, so I've set up a small test project without cli.
Route:
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
Template:
<script type="text/x-handlebars" id="index">
<ul>
{{#each item in model}}
<li {{action 'pickColour' item}}>{{item}}</li>
{{/each}}
</ul>
<div>Currently selected: {{selected}}</div>
</script>
Controller:
App.IndexController = Ember.ArrayController.extend({
queryParams: ['selected'],
selected: null,
actions: {
pickColour: function(colour) {
console.log("Colour " + colour + " selected");
this.set('selected', colour);
}
}
});
According to the Ember guide this should bind the selected field of the controller to the url parameters. But in this case no url parameters is set when I click a specific colour.
It should be so simple yet I can't make it work. Am I gloriously overlooking something?
Edit: SOLUTION
I missed the fact that for now it's only available in the beta. If you read this in the future, be aware that it might be available in the latest full release.
This is working just fine in version 1.9.0, so this question can probably be closed.
Working demo here
I have a template called sample and I am changing the property show in the controller based on a button press. Now I want the controller to reset the property every time the template is rendered. Currently even if I go to 'next' template and come back, the property show remains true if the button on sample was pressed. I want to change this behaviour and the property show should be false by default. I know I can do this by defining a view and using the didInsertElement hook but is that the only way to do this?? Ember.js website says that Views in Ember.js are typically only created for the following reasons:
When you need sophisticated handling of user events
When you want to create a re-usable component
and I am doing none of the above. Here is some sample code:
<script type="text/x-handlebars" data-template-name="sample">
{{#if show}}
Showing stuff
{{/if}}
<button {{action changeShow}}>Change</button>
{{#link-to 'next'}} Next {{/link-to}}
</script>
<script type="text/x-handlebars" data-template-name="next">
Hello
{{#link-to 'sample'}}Back{{/link-to}}
</script>
App.SampleController=Ember.Controllers.Extend{(
show:false,
actions:{
changeShow:function(){
this.controllerFor('sample').set('show',true);
}
}
)};
you can use didTransition action which will trigger automatically once the transition happened. didTransition action
App.SampleController=Ember.Controllers.Extend{(
show:false,
actions:{
didTransition:function(){
this.controllerFor('sample').set('show',false);
},
changeShow:function(){
this.controllerFor('sample').set('show',true);
}
}
)};
You can use the renderTemplate hook for the route you're doing this in, and change the controller variable in there.
http://emberjs.com/api/classes/Ember.Route.html#method_renderTemplate
I'd do something like this:
App.PostsRoute = Ember.Route.extend({
renderTemplate: function(controller, model) {
var favController = this.controllerFor('favoritePost');
favController.set("toggle", false)
this._super()
}
});
The task:
Open a form in a lightbox to create a new "event"; the opened form should be bookmarkable.
The road blocks:
There are examples of opening a lightbox using {{action}} tags, but could not find one that opened in its own route.
There are many examples using older versions of ember.js.
There is not a lot of documentation related to ember-data and REST (I know, I know...it isn't "production ready").
The problem:
The fields in the form were not being tied to a backing model so "null" was being posted to my servlet (a Spring controller).
My very first iteration was not too far off from the final outcome (jsfiddle). The thing that finally made it works swapping this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
controller.set("model", model);
},
...
});
...for this:
EP.EventsNewRoute = Ember.Route.extend({
...
setupController : function(controller, model) {
this.controllerFor("events-new").set("model", model);
},
...
});
The question:
Why does the setupController function need to call controllerFor in order to properly set up the model?
And finally, since I struggled to find a fully-functional example, I wanted to make this accessible (and hopefully discover improvements).
Here's the fiddle: http://jsfiddle.net/6thJ4/1/
Here are a few snippets.
HTML:
<script type="text/x-handlebars">
<div>
<ul>
{{#linkTo "events.new" tagName="li"}}
Add Event
{{/linkTo}}
</ul>
</div>
{{outlet events-new}}
</script>
<script type="text/x-handlebars" data-template-name="events-new">
<form>
<div>
<label>Event Name:</label>
{{view Ember.TextField valueBinding="name"}}
</div>
<div>
<label>Host Name:</label>
{{view Ember.TextField valueBinding="hostName"}}
</div>
</form>
</script>
JavaScript:
...
EP.Router.map(function() {
this.resource("events", function() {
this.route("new");
});
});
EP.EventsNewRoute = Ember.Route.extend({
model : function() {
return EP.Event.createRecord();
},
setupController : function(controller, model) {
//controller.set("model", model); // Doesn't work? Why not?
this.controllerFor("events-new").set("model", model); // What does this do differently?
},
...
});
EP.EventsNewController = Ember.ObjectController.extend({
save : function() {
this.get("content.transaction").commit(); // "content.store" would commit _everything modified_, we only have one element changed, so only "content.transaction" is necessary.
}
});
EP.EventsNewView = Ember.View.extend({
...
});
EP.Event = DS.Model.extend({
name : DS.attr("string"),
hostName : DS.attr("string")
});
Resources:
http://emberjs.com/guides/routing/setting-up-a-controller/
http://emberjs.com/guides/getting-started/toggle-all-todos/ (trying to mimic what I learned, but morph the add-new to a new route)
Writing a LightboxView causes problems / Integrating DOM Manipulating jQuery-Plugins makes actions unusable (lightbox "example")
Dependable views in Ember (another lightbox "example" but doesn't have routes for the lightbox opening)
Why does the setupController function need to call controllerFor in order to properly set up the model?
Ember makes URLs a very integral part of its conventions. This means that the state of your application is represented by the route it is on. You've grokked most of this correctly. But there are couple of subtle nuances, that I will clarify below.
First consider an app with the following URLs,
/posts - shows a list of blog posts.
/posts/1 - shows a single blog post.
And say clicking on a post in the list at /posts takes you to /posts/1.
Given this scenario, there 2 ways a user will get to see the post at /posts/1.
By going to /posts and clicking on the 1st post.
By typing in /posts/1, via bookmarks etc.
In both these cases, the PostRoute for /posts/1 will need the model corresponding to Post id 1.
Consider the direct typing scenario first. To provide a way to lookup the id=1 post model, you would use,
model: function(params) {
return App.Post.find(params.post_id);
}
Your template for post will get the model and it can render using it's properties.
Now consider the second scenario. Clicking on post with id=1 takes you to /posts/1. To do this your template would use linkTo like this.
{{#linkTo 'post' post}} {{post.title}} {{/linkTo}}
Here you are passing in the post model to the linkTo helper. It then serializes the data for the post into a URL, ie:- '/posts/1'. When you click on this link Ember realizes that it needs to render the PostRoute but it already has the post model. So it skips the model hook and directly calls setupController.
The default setupController is setup to simply assign the model on the controller. It's implemented to do something like,
setupController: function(controller, model) {
controller.set('model', model);
}
If you do not need to set custom properties on your controller, you don't need to override it. Note: if you are augmenting it with additional properties you still need to call _super to ensure that the default setupController behaviour executes.
setupController: function(controller, model) {
this._super.apply(this, arguments);
controller.set('customProp', 'foo');
}
One final caveat, If you are using linkTo and the route does not have dynamic segments, then the model hook is still called. This exception makes sense if you consider that you were linking to the /posts route. Then the model hook has to fire else Ember has no data to display the route.
Which brings us to the crux of your question. Nearly there, I promise!
In your example you are using linkTo to get to the EventsNewRoute. Further your EventsNewRoute does not have dynamic segments so Ember does call the model hook. And controller.set("model", model); does work in so much as setting the model on the controller.
The issue is to do with your use of renderTemplate. When you use render or {{render}} helper inside a template, you are effectively getting a different controller to the one you are using. This controller is different from the one you set the model on, hence the bug.
A workaround is to pass the controller in the options, which is why renderTemplate gets this controller as an argument.
renderTemplate: function(controller) {
this.render("events-new", {
outlet : "events-new", controller: controller
});
}
Here's an updated jsfiddle.
Final Note: Unrelated to this question, you are getting the warning,
WARNING: The immediate parent route ('application') did not render into the main outlet and the default 'into' option ('events') may not be expected
For that you need to read this answer. Warning, it's another wall of text! :)
The code works and here is the jsfiddle for the code. But, I don't like the approach I am using to get {[render}} to work. For now to be able to use {{render events}} in application-template, I need to set it in the application-route using setupController and then calling controllerFor('events'), otherwise it won't work. But I have already defined an EventsRoute with a model hook for setting up the controller content and will prefer for {{render helper}} to use the model set in the EventsRoute.
I was wondering if there is a better or more idiomatic way to do this in ember besides my present approach of going to ApplicationRoute.
Relevant code*
App.Router.map(function(){
this.resource('events');
});
The {{render 'events'}} in application template only works when i set the model via application route.
App.ApplicationRoute = Ember.Route.extend({
setupController: function(){
this.controllerFor('events').set('model', App.Event.find());
}
});
I would prefer to {{render 'events'}} to work with the content set here but it doesn't. But I am keeping for now to use in places where it might make sense to use {{#linkTo}}.
App.EventsRoute = Ember.Route.extend({
model: function(){
return App.Event.find();
},
setupController: function(controller, model){
controller.set('content', model);
}
});
The templates
<script type="text/x-handlebars" data-template-name="application">
{{render 'events'}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="events">
<button {{action 'yEmpty'}}> log event content is empty</button>
{{#each controller}}
<h3>{{title}}</h3>
<p>{{start}} - {{end}}</p>
{{/each}}
{{outlet}}
</script>
I'm unsure of what you're trying to do. Do you want events and appointments to be rendered at the same time on one page? If yes, then, I think, you need to use render (much like you do in your jsfiddle). If not, you could remove {{render 'events'}} and setupController hook on ApplicationRoute and redirect to events route.
I found this issue with Fixture Adapter and looks like it doesn't work with hasMany (therefore jsfiddle example won't work) but I assume you are using RESTAdapter.
UPDATE:
Another place to set the model is in init hook like so:
App.EventsController = Ember.ArrayController.extend({
init: function () {
this.set('model', App.Event.find());
}
});
Personally, I would left the code in setupController hook.