UPDATE
Okay, what luck…this is now the default behavior as of Ember 1.5. Yay!
Original question
Something that really seems intuitive to me but isn't the case in the Ember router. I have a bunch of resources, the vast bulk of which have an index route where I want the model to be the same as the resource route's model--that is, the object selected by convention using the dynamic segment. E.g.:
this.resource("chargeRule", { path: "/chargeRule/:chargeRule_id" }, function(){});
I'm going to be using nesting, so I can't do everything in my chargeRule template, because that template is going to have little but {{outlet}} (kinda like this guy). So my index route is going to be my read-only view in most cases and I'll have an edit sub-route to update a model where needed.
What bugs me is, the default model for an index route (and any other sub-route) is: NOTHING! Tada! Null I think, though it could be empty string or something. So, I now have to create a bunch of tiny Ember.Route subclasses that look exactly like this:
App.ChargeRuleIndexRoute = Ember.Route.extend({
model: function() {
return this.modelFor('chargeRule');
}
});
Repeat ad-nauseum for each different model class/route. Really, then about double it because I want the same for /index and /edit routes, maybe more. Personally, I would have thought that the default model (since you can override it anyway) for any child route should have been the parent route's model instead of nothing, but I'd be interested in explanations as to why it's not.
But my question is, can anybody come up with a way for me to make the behavior I'm describing the default in my app? Something I can reopen? So I don't have a dozen or more boilerplate 5-line route objects?
Edit
I might should mention that I know my gripe might seem trivial, but the fact that I have to create so many route subclasses also implies that I should create individual files for each class…right? So that future developers can easily find where a given class is defined? I know that's not an absolute, but is a good practice, I think, and would require me to make as many tiny JS files as tiny classes…which is why I started thinking in terms of changing the default behavior. I'll take comments on that though.
UPDATE
See note at top of question, this is no longer necessary...
Based on #kingpin2k 's answer, I came up with this more generalized possibility:
App.ParentModelRoute = Ember.Route.extend({
model: function() {
return this.modelFor(this.get('routeName').replace(/\..*$/, ''));
}
});
App.ChargeRuleIndexRoute = App.ParentModelRoute.extend();
App.ChargeRuleEditRoute = App.ParentModelRoute.extend();
App.OtherIndexRoute = App.ParentModelRoute.extend();
.
.
.
Which certainly cuts down on the amount of code…but still requires me to explicitly declare a route class for each sub-route. It also may be a bit hacky (the routeName property isn't listed in the API docs?), though it's simple enough it should work pretty widely.
The reason is due to the fact that a route doesn't necessarily have to contain a model. Think of the case of a Albums/New route. It wouldn't actually have the albums resource as its model. For this reason there is no set rule for what the model should be for a model.
That being said, if you feel like lazily building up some sort of default route for your route and extending this, that's entirely possible and can be accomplished a few different ways.
App.DefaultChargeRuleRoute = Ember.Route.extend({
model: function() {
return this.modelFor('chargeRule');
}
});
App.ChargeRuleIndexRoute = App.DefaultChargeRuleRoute.extend();
App.ChargeRuleEditRoute = App.DefaultChargeRuleRoute.extend();
or if you wanted to make some generic model fetching route
App.ModelFetcherRoute = Ember.Mixin.create({
model: function(){
return this.modelFor(this.get('modelToFetch'));
}
});
App.ChargeRuleIndexRoute = Ember.Route.extend(App.ModelFetcherRoute, {
modelToFetch: 'chargeRule'
});
And this could follow the same pattern as the first example if you want to make it even smaller
App.DefaultChargeRuleRoute = Ember.Route.extend(App.ModelFetcherRoute, {
modelToFetch: 'chargeRule'
});
App.ChargeRuleIndexRoute = App.DefaultChargeRuleRoute.extend();
App.ChargeRuleEditRoute = App.DefaultChargeRuleRoute.extend();
Related
On my route im requesting via ember-data some records. Lets say the model-type is 'item'.
model: function(){
return this.get('store').find('item');
}
now ive got a component named 'my-foo' which should use the records to do something with the data. Therefore Im calling the component like that:
{{my-foo myItems=model}}
in my routes template. In the components js part, Im trying to get the myItems-field and iterate over them.
this.get('myItems').forEach(...);
Unfortunalety its not clear for me if the model i want to overgive to the component is an collection from records or just a single record (since on some routes the model is the result of store.find('item') on other store.find('item', 23424).
How can I check what kind of data arrives in the component.
(Also Im wondering what kind of object is it since im using ember-data. Is it a DS.recordarray or a promise or something else at this time?)
I can see two solutions to the problem:
Making component aware of the form that model receives
Checking and/or adjusting data type in component (in my opinion better default scenario)
As for making component aware - you could go with 2 approaches. Either differentiate in a way how your component take arguments, so there could be:
{{my-foo myItems=model}} - when you expect to receive multiple items
{{my-foo item=model}} - when you expect to receive single one
And then work accordingly further on, or - the second approach - is to actually split component (while extracting shared part to a different structure) so you would have my-foo for single items and my-foo-array for multiple.
Advantage of this approach is that you don't deal with what-if-multiple logic, that might grow to something unmanagable later on, yet usage of it is dependant on project requirements.
As for checking and/or adjusting - you already have data in, so could make assumption that your data is dirty and sanitize it using computed property. Below example, where single item is wrapped into an array.
export default Ember.Component.extend({
sanitizedItems: Ember.computed('items', function() {
var items = this.get('items');
if(!Array.isArray(items)) {
return [items];
} else {
return items;
}
})
});
Since you're using Ember.Data, depending on your setup, you might get a promise instead of object/array. In this case, you might want to resolve promise using this.get('items').then(function(items) { ... }) before doing sanitization, yet the idea behind is exactly the same.
You can check full example: Gist, Twiddle
Lets say I want that my page's title will change depending on a really simple field what is the Ember way of doing it?
I didn't really understand the bindings in Ember, do I have to create an object even if all I need is just 1 field?
Does Ember support two way bindings? if it does so how can I constrain the bindings to one-way only?
I think i'm a bit confused with Ember-data & regular Ember, when I use Ember-data do I need to care about bindings at all?
Thanks in advance :)
This is a little vague (or I just don't fully understand what you're asking), so I'll shotgun approach and we can narrow down as you ask more questions.
Preface: Ember Data is a client side record management library, Ember works completely fine without it.
Title
A page's title is a little tricky since it's kind of out of the scope of the viewable dom, but the best way to handle it would be with an observer. In the example below, as the title property changes inside of my application controller I'm setting the document.title.
App.ApplicationController = Em.Controller.extend({
title:undefined,
watchTitle: function(){
document.title = this.get('title');
}.observes('title')
})
Example: http://emberjs.jsbin.com/haducafu/1
Computed Properties
Ember does support one way bindings (though rarely do you need to care about bindings). More often you want to care about dependent properties. eg if property a has changed, property b should be updated etc. In the case below, b is a computed property that depends on a, if a changed, b is dirty, and ember should re-computed it.
App.ApplicationController = Em.Controller.extend({
a:'foo',
b: function(){
return 'Hello: ' + this.get('a');
}.property('a')
})
Example: http://jsbin.com/haducafu/2/edit
Simple Binding
Additionally Ember can do just simple bindings (you can actually skip defining name, since ember would define it the first time it uses it).
App.ApplicationController = Em.Controller.extend({
name:undefined
});
<h2>Hello {{name}}</h2>
Name: {{input value=name}}
Example: http://jsbin.com/haducafu/3/edit
One Way/Read Only:
One way will take the value from its host property, unless you set it, if you set it it stops following the dependent property and becomes its own (not modifying the dependent property).
Read only will take values form the host property, and if you try and set it it will blow chunks.
App.ApplicationController = Em.Controller.extend({
name:'billy',
oneWay: Em.computed.oneWay('name'),
readOnly: Em.computed.readOnly('name')
});
Try changing name first, they will all update, then change oneWay and it will diverge and never return, then change readOnly and it will throw errors.
Example: http://jsbin.com/haducafu/4/edit
Is there a standard way of handling errors when a 'findHasMany' call fails? Use case:
Model: App.User
{
DS.hasMany('comments', {'async': true});
}
Template
{{#each comment in comments}}
<p>{{comment.title}}</p>
{{/each}}
The issue is that when the lazy loading of comments fails, due to some server issue for example, I want to be able to respond to that error in the UI (by routing somewhere else, showing a popup about errors on the page, etc). At the moment the promise just rejects. I thought that Ember Data might have some hook on the ManyArray for cases like this, but it doesn't seem to, and the store seems to define precisely nothing as the action to carry out in such cases: https://github.com/emberjs/data/blob/v1.0.0-beta.8/packages/ember-data/lib/system/store.js#L1758 - the promise is given a 'resolve' method, but not a reject method.
My options seem to be either subclassing the store, and adding in some reject code there, or subclassing DS.PromiseArray and observing the 'isRejected' property. Any thoughts would be very welcome!
EDIT:
This issue seems to boil down to the fact that, when handling models defined in a route, Ember and Ember Data work well together (you can catch rejecting promises in an error action) there is no similar structure for async requests directly through a template. One solution might be to have an observer in the controller that observes something like 'model.isError', but a failing hasMany relationship does not trigger an error on the owning model. I suppose instead I can do 'comments.isRejected', but again, I would have to code that in for every controller that has a model with a hasMany relationship, in other words, all of them, which doesn't seem very satisfactory. If models had an observable enumerable property (like "hasManyIsError": {comments: false, posts: true}) then it would be easy to observe any of them with 'hasManyIsError.length
Assuming a var called user that has been fetched, you'd do this:
var itWorked = function(comments) { return comments; }
var itFailed = function(error) { return error; }
user.get("comments").then(itWorked, itFailed);
async: true means it'll get using a promise... so you can use then... you can't do that on a relationship that doesn't specify async: true.
[edit] sorry I just realised it might not be obvous that whatever you put in the itFailed function will eval when the request for comments fails, and likewise inversely for itWorked... :)
What is the "appropriate" way in Ember to send a parameter from one route to another? For instance, I have two routes defined as such:
this.resource('activities', { path: '/activities/:on_date' }, function() {
this.route('new');
});
when on the ActivitiesRoute the user is presented with a dropdown of possible activities. When they choose something it transitions to the ActivitiesNewRoute:
this.transitionToRoute('activities.new');
and I know there is a second parameter available in the transitionToRoute(route,model) method but it's meant for passing in a model and I'm assuming this shouldn't be repurposed for other parameter passing. In this case the dropdown choice is picking an Action model id and the model for ActivitiesNew is a Activity.
Here are my three guesses at ways that might work:
1) Make it a router parameter
I supposed I could change ActivitiesNew to include a "parameter" as part of the route:
this.route('new', { path: '/new/:my_parameter' });
I'm not sure I'd really like to have it becoming part of the URL path but if this was the prevailing convention then I'd live with that.
2) Get a handle, post transition
Immediately following the transitionToRoute call I could set a property of the new controller class. Not sure if the controller would be setup yet but I'm imagining something like:
this.transitionToRoute('activities.new');
this.get('target').controllerFor('activities.new').set('my_parameter', myValue);
3) Use model parameter
this.transitionToRoute('activities.new',myValue);
I suspect that this is a major no-no. I haven't looked into the Ember code to know if this could work but it seems against convention so this is my "bad option".
transitionTo & transitionToRoute return a "promise-like" object. The parameter this object is resolved with is the route, from which you can access controller and currentModel. So a nice clean way to pass information to a route to which you are transitioning is:
var my_param = ....;
this.transitionToRoute('activities.new').then(function(newRoute) {
newRoute.currentModel.set('someProperty', my_param);
//or
newRoute.controller.set('someProperty', my_param);
});
EDIT/RANT:
note that in most cases, you do want to use needs, and bind things between controllers. However, there are certainly instances when you have things that depend on the logic of a route transition -- eg., controllerB has state X if we came to routeA from routeB, but state Y if we came from routeC. In that case, my answer is valuable.
The primary value of stack overflow to the development community is not the immediate answers you get to questions you post, but the massive ever growing wealth of googleable development knowledge. When you "infer" from a user's question that they "should" be doing something other than what they are asking how to do, you may be right (or you may be just incapable of imagining their particular circumstance), but if you answer only with your recommendation/rule/aphorism/cargo-cult-dictum instead of answering the ACTUAL QUESTION, you diminish the value of everybody else's google searches. If you want to tell someone to do something other than what they're asking, do it in a comment, or in a footnote to an answer to the actual question.
You can use the needs API (Read about it here):
App.ActivitiesNewController = Ember.ObjectController.extend({
needs: ['activities']
// Bind the property you need
actionTemplateBinding: 'controllers.activities.actionTemplate'
});
So what you actually need is to pass a parameter between controllers, which is exactly what needs is for. Plus, binding the property with needs you ensure it is in sync at all times, instead of relying on setupController being called.
You could use query-params (http://guides.emberjs.com/v1.10.0/routing/query-params/), as follows:
this.transitionToRoute('activities.new', {queryParams: {my_param: 'my_value'});
In order to be able to receive my_param in the new controller, you would also need to define the following lines:
App.ActivitiesNewController = Ember.ObjectController.extend({
queryParams: ['my_param'],
my_param: ''
...
});
A drawback of this solution is that the value of my_param will be serialized in URL - so it would not be suitable for some sensitive information you may want to pass between routes.
I'll answer my question with what I've decided to go with for now but keep it open for a a few days to see if anyone comes back with a more experienced answer. My answer may very well be perfectly fine ... it works anyway.
I've gone with a variation of #2 from the question. The difference is that that rather than trying to set a property in the ActivitiesNew controller from Activities controller I do the the opposite:
In ActivitiesNewRoute:
App.ActivitiesNewRoute = Ember.Route.extend({
model: function(params) {
return this.store.createRecord('activity');
},
setupController: function(controller,model) {
controller.set('actionTemplate', this.controllerFor('activities').get('actionTemplate'));
}
});
Still interested in hearing from people if there's a better way of doing this.
Transition to route with params and set model
yourAction:->
model = 'your-model'
route = 'your.path.to.toute'
routeLoad = #transitionToRoute route,
id: model.get 'id'
routeLoad.then (route) ->
route.set 'controller.model', model
return
I have a route that has a dynamic segment:
this.resource('dog', {path: '/dog/:pet_id'});
For debugging purposes, I would like to linkTo dog with the specific dynamic segment of '666'. But
{{#linkTo 'dog' '666'}}Click to go to dog{{/linkTo}}
is giving me "undefined" instead of "666". Do you know why?
See it running on jsbin.
See the code on jsbin.
Your working jsbin: http://jsbin.com/iwiruw/346/edit
The linkTo helper does not accept strings as a parameter, but instead model from which to pick up the dynamic segments defined in your router map. If you don't have a model at hand leave the parameter out, and all you need to do is to hook into the serialize function of your DogRoute (if you don't have one defined just define it to instruct ember to use yours instead of the automatically defined) and return an object/hash containing the dynamic segments your route expects, this could be anything you want:
App.DogRoute = Ember.Route.extend({
serialize: function(model) {
return {pet_id: 666};
}
});
Hope it helps.
I cleaned up the code a little bit by removing unused bits and switching to the fixture adapter. Here's a working version without the need for a serialize method: http://jsbin.com/iwiruw/347
Ultimately, nothing needed to be changed in the base code beyond using a newer version of Ember and properly setting up the actual model classes and data.