Custom Dynamic Segment (NOT ID) - EmberJS - ember.js

I want to have a dynamic segment path in Ember without using the :id attribute
As per the Ember Guides, I'm using the serialize method to achieve this.
Heres my Router:
App.Router.map(function() {
this.resource("orders", function(){
this.resource('order', { path: ':order_sequence'}, function(){
this.route('edit');
})
});
});
And my Route:
var OrderRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('order', params.order_sequence)
},
serialize: function(model) {
return { order_sequence: model.get('sequence') };
}
});
module.exports = OrderRoute;
However, my URL's still behave using the id attribute in the path instead of the sequence attribute..
Any ideas?

Is your browser caching something, because that's correct. Are you passing in the id instead of the sequence/model in any of your transitionTo/transitionToRoute/link-to?
Oh, you aren't talking about the slug in the url, nor the route, you are talking about the id of your model. You need to create a serializer for that particular model and override the primary key
App.OrderSerializer = DS.RESTSerializer.extend({
primaryKey: 'sequence'
});
The Fixture Adapter has a constraint on defining the id, but you can lazily get around it by extending the fixture adapter and overriding a single method
App.OrderAdapter = DS.FixtureAdapter.extend({
fixturesForType: function(type) {
if (type.FIXTURES) {
var fixtures = Ember.A(type.FIXTURES);
return fixtures.map(function(fixture){
// aka we massasge the data a bit here so the fixture adapter won't whine so much
fixture.id = fixture.sequence;
var fixtureIdType = typeof fixture.id;
if(fixtureIdType !== "number" && fixtureIdType !== "string"){
throw new Error(fmt('the id property must be defined as a number or string for fixture %#', [fixture]));
}
fixture.id = fixture.id + '';
return fixture;
});
}
return null;
},
});

Related

Ember Data rewrite URL

I have the following setup:
App.Router.map(function() {
this.route('tab', { 'path' : 'tab/:which' });
});
App.ApplicationStore = DS.Store.extend({});
App.ApplicationAdapter = DS.RESTAdapter.extend({
host: '../api'
});
App.TabAdapter = DS.RESTAdapter.extend({
find: function(store, type, id) {
alert("I doesn't get invoked");
return this._super(store, type, id);
}
});
App.TabRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('emails', {tab: "inbox"});
}
});
And when visiting the route #/tab/inbox i wanna rewrite the URL for the endpoint from
http://localhost/ba/api/emails?tab=inboxinto
http://localhost/ba/api/emails/inbox. Therefore i'm overriding the find()-method on the TabAdapter, but when this.store.find('emails', {tab: "inbox"}); runs, it doesn't enter my overridden method(and my test-alert doesn't get invoked).
Why does my overridden find()-method not get invoked?
you're overriding the wrong find method. You're finding by query, not id and should be overriding that method
findQuery: function(store, type, query) {
// Do your thing here
return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query });
}
And you're using a TabAdapter which would be specific to models of type tab not of type email(s). You should be creating a Email(s)Adapter. The general convention is for a model to be singular btw.
See also: How do you create a custom adapter for ember.js?

How to avoid too many empty records?

Ember : 1.5.0-beta.2
Ember Data : 1.0.0-beta.7
I have the following router:
App.Router.map(function() {
this.resource('posts', function() {
this.route('new');
});
});
My PostsNewRoute creates a new record in the model hook:
App.PostsNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('post');
}
});
Since I don't want transient record to be visible, I filter them out in my PostsRoute:
App.PostsRoute = Ember.Route.extend({
model: function() {
this.store.find('post');
return this.store.filter('post', function(post) {
return !post.get('isNew');
});
}
});
This works as expected, but every transition to posts.new add a new record to the store, which is something I would like to avoid. So, instead of calling createRecord every time the model hook is called, I filter the store for an empty record and return this if there is one found:
App.PostsNewRoute = Ember.Route.extend({
model: function() {
var route = this;
return this.store.filter('post', function(post) {
return post.get('isNew');
}).then(function(result) {
return result.get('firstObject') || route.store.createRecord('post');
);
});
This gives me at the most one empty record.
My question: is there a better way to avoid my store being populated with (many) empty records ?
UPDATE:
Instead of filtering on the isNew attribute, I can probably use currentModel:
model: function() {
this.get('currentModel') || this.store.createRecord('post');
};
You can use this addon https://github.com/dockyard/ember-data-route to clean up when you leave a /new route. It hooks into the willTransition action hook that gets called on the route whenever a transition occurs.
The source code is a short read: https://github.com/dockyard/ember-data-route/blob/master/addon/mixins/data-route.js.
The alternative would be to not create a new record in the model hook, but according to a comment of yours it doesn't seem to be an option.

getting parameter value from nested routes

I have my router set like :
this.resource('analytics', {path: '/analytics'}, function(){
this.resource('analyticsRuns', {path: ':exerciseRunId/analyticsRuns'},function(){
this.resource('analyticsRun',{path: ':runId'});
});
});
I jump to 'analyticsRuns' route using :
this.transitionToRoute('analyticsRuns',{"exerciseRunId":this.get('selectedExerciseRun.id')});
And my AnalyticsRunsIndexRoute is defined as :
AS.AnalyticsRunsIndexRoute = Ember.Route.extend({
model : function(params) {
var store = this.get('store');
//console.log(params); //returns empty object
//var exerciseRunId = AS.Analytics.get('exerciseRunId');
exerciseRunId = 577;
if(!(exerciseRunId)){
this.transitionTo('analytics');
}
store.find('analyticsRun',{'exerciseRunId':exerciseRunId});
return store.filter('analyticsRun', function(analyticRun){
return analyticRun.get('exerciseRunId') == exerciseRunId;
});
},
setupController : function(controller,model){
this._super(controller,model);
this.controllerFor('analysisTemplates').set('model',controller.get('store').find('analysisTemplate'));
}
});
I was wondering if I could access ":exerciseRunId" value in the AnalyticsRunsIndexRoute. Currently there isnothing set when I check the params arguments passed to this routes' model. On refresh however, the parameter becomes available to the AnalyticsRunRoute but only on refresh. So do I have to play with stateManagement to get the parameter value? or is there simpler way to access it. Thanks.
SOLUTION :
Again lots of thanks to Jeremy for walking through this. Here is how I have set up things now :
I defied routes like :
AS.AnalyticsRunsRoute = Ember.Route.extend({
model : function(params) {
return params;
}
});
AS.AnalyticsRunsIndexRoute = Ember.Route.extend({
model : function(params) {
var parentModel = this.modelFor('analyticsRuns');
var exerciseRunId = AS.Analytics.get('exerciseRunId')||parentModel.exerciseRunId;
var store = this.get('store');
if(!(exerciseRunId)){
this.transitionTo('analytics');
}
store.find('analyticsRun',{'exerciseRunId':exerciseRunId});
return store.filter('analyticsRun', function(analyticRun){
return analyticRun.get('exerciseRunId') == exerciseRunId;
});
},
setupController : function(controller,model){
this._super(controller,model);
this.controllerFor('analysisTemplates').set('model',controller.get('store').find('analysisTemplate'));
}
});
When calling transitionToRoute you should be passing a live object.
this.transitionToRoute('analyticsRuns',this.get('selectedExerciseRun'));
When you transition from route to route the model hook is skipped so it's important that you pass live objects either in transitionToRoute or in a link-to.
[UPDATE] in response to a comment:
If selectedExcerciseRun is not a live object, then you'd need to instantiate a live object before transitioning. Something like this :
var runId = this.get('selectedExerciseRun.id');
var promise = store.find('analyticsRun',{'exerciseRunId':runId});
promise.then(function(analyticsRun){
this.transitionToRoute('analyticsRun',analyticsRun);
});

Delete associated model with ember-data

I have two models:
App.User = DS.Model.create({
comments: DS.hasMany('App.Comment')
});
App.Comment = DS.Model.create({
user: DS.belongsTo('App.User')
});
When a user is deleted, it also will delete all its comments on the backend, so I should delete them from the client-side identity map.
I'm listing all the comments on the system from another place, so after deleting a user it would just crash.
Is there any way to specify this kind of dependency on the association? Thanks!
I use a mixin when I want to implement this behaviour. My models are defined as follows:
App.Post = DS.Model.extend(App.DeletesDependentRelationships, {
dependentRelationships: ['comments'],
comments: DS.hasMany('App.Comment'),
author: DS.belongsTo('App.User')
});
App.User = DS.Model.extend();
App.Comment = DS.Model.extend({
post: DS.belongsTo('App.Post')
});
The mixin itself:
App.DeletesDependentRelationships = Ember.Mixin.create({
// an array of relationship names to delete
dependentRelationships: null,
// set to 'delete' or 'unload' depending on whether or not you want
// to actually send the deletions to the server
deleteMethod: 'unload',
deleteRecord: function() {
var transaction = this.get('store').transaction();
transaction.add(this);
this.deleteDependentRelationships(transaction);
this._super();
},
deleteDependentRelationships: function(transaction) {
var self = this;
var klass = Ember.get(this.constructor.toString());
var fields = Ember.get(klass, 'fields');
this.get('dependentRelationships').forEach(function(name) {
var relationshipType = fields.get(name);
switch(relationshipType) {
case 'belongsTo': return self.deleteBelongsToRelationship(name, transaction);
case 'hasMany': return self.deleteHasManyRelationship(name, transaction);
}
});
},
deleteBelongsToRelationship: function(name, transaction) {
var record = this.get(name);
if (record) this.deleteOrUnloadRecord(record, transaction);
},
deleteHasManyRelationship: function(key, transaction) {
var self = this;
// deleting from a RecordArray doesn't play well with forEach,
// so convert to a normal array first
this.get(key).toArray().forEach(function(record) {
self.deleteOrUnloadRecord(record, transaction);
});
},
deleteOrUnloadRecord: function(record, transaction) {
var deleteMethod = this.get('deleteMethod');
if (deleteMethod === 'delete') {
transaction.add(record);
record.deleteRecord();
}
else if (deleteMethod === 'unload') {
var store = this.get('store');
store.unloadRecord(record);
}
}
});
Note that you can specify via deleteMethod whether or not you want to send the DELETE requests to your API. If your back-end is configured to delete dependent records automatically, then you will want to use the default.
Here's a jsfiddle that shows it in action.
A quick-and-dirty way would be to add the following to your user model
destroyRecord: ->
#get('comments').invoke('unloadRecord')
#_super()
I adapted the answer of #ahmacleod to work with ember-cli 2.13.1 and ember-data 2.13.0. I had an issue with nested relationships and the fact that after deleting an entity from the database its id was reused. This lead to conflicts with remnants in the ember-data model.
import Ember from 'ember';
export default Ember.Mixin.create({
dependentRelationships: null,
destroyRecord: function() {
this.deleteDependentRelationships();
return this._super()
.then(function (model) {
model.unloadRecord();
return model;
});
},
unloadRecord: function() {
this.deleteDependentRelationships();
this._super();
},
deleteDependentRelationships: function() {
var self = this;
var fields = Ember.get(this.constructor, 'fields');
this.get('dependentRelationships').forEach(function(name) {
self.deleteRelationship(name);
});
},
deleteRelationship (name) {
var self = this;
self.get(name).then(function (records) {
if (!records) {
return;
}
var reset = [];
if (!Ember.isArray(records)) {
records = [records];
reset = null;
}
records.forEach(function(record) {
if (record) {
record.unloadRecord();
}
});
self.set(name, reset);
});
},
});
Eventually, I had to set the relationship to [] (hasMany) or null (belongsTo). Else I would have run into the following error message:
Assertion Failed: You cannot update the id index of an InternalModel once set. Attempted to update <id>.
Maybe this is helpful for somebody else.

Can a nested ember.js route use a different model and still retain controller context?

I have a basic person object
PersonApp.Person = DS.Model.extend({
username: DS.attr('string')
});
I have a route to find all people
PersonApp.Router.map(function(match) {
this.resource("person", { path: "/" }, function() {
this.route("page", { path: "/page/:page_id" });
this.route("search", { path: "/search/:page_term" });
});
});
In my route I'm looking at the params coming in
PersonApp.PersonRoute = Ember.Route.extend({
selectedPage: 1,
filterBy: '',
model: function(params) {
if (get(params, 'page_id') !== undefined) {
this.selectedPage = get(params, 'page_id');
} else {
this.selectedPage = 1;
}
if (get(params, 'page_term') !== undefined) {
this.filterBy = get(params, 'page_term');
} else {
this.filterBy = '';
}
console.log(this.selectedPage);
console.log(this.filterBy);
return PersonApp.Person.find();
}
});
My nested routes are using a different model (not person directly) as they contain data that isn't persisted (and really only let me flip a bit on the controller)
Yet when I manually put something on the url or click a link that does a full blown transition the "params" coming into my model hook above are always empty.
Here is the basic page model I'm using (w/ search support)
PersonApp.Page = Ember.Object.extend({
term: ''
});
When a user does a search I have a view that invokes transitionTo
PersonApp.SearchField = Ember.TextField.extend({
keyUp: function(e) {
var model = PersonApp.Page.create({term: this.get('value')});
this.get('controller.target').transitionTo('person.search', model);
}
});
Any way I can pass this "page" model to a nested view and still retain the basic "person" controller context (ie- so I can manipulate the view around this array of model objects)