I am using Ember-Data and one of my properties is a dictionary data structure. I'd like any update to this dictionary to be converted into an action which sets the parent Model into a "dirty" state.
So here's the config:
Model
export default DS.Model.extend({
// standard types
foo: DS.attr('string'),
bar: DS.attr('number'),
baz: DS.attr('boolean'),
// dictionary (aka, flexible set of name value pairs)
dictionary: DS.attr('object')
});
Transform
export default DS.Transform.extend({
deserialize: function(serialized) {
return Ember.Object.create(serialized);
},
serialize: function(deserialized) {
return deserialized;
}
});
This works and let's assume for a moment that the "dictionary" property is defined as:
{
one: { prop1: foo, prop2: bar, prop3: baz },
two: 2,
three: "howdy",
many: [{},{},{}]
}
This means that an Ember Object has four properties. These properties can be a string, a number, an array, or an object. What I'd like is to have some way of identifying any changes to this underlying basket of attributes so I can propagate that to the Model and have it adjust its state to "dirty".
TL;DR - Working JS Bin example
In order to accomplish this you have to do the following:
1. Deserialize the raw object and all its nested deep properties to Ember Objects so they could be Observable
2. Add observers to your model for all existing keys dynamically on every change of the raw object reference, because it can change its content and scheme.
3. Remove these dynamic observers on every raw object reference change and assign the new ones
4. All dynamic properties changes will set timestamp property so that controllers could listen to it
This is a "Deep" transform I wrote in order to accomplish (1):
// app/transforms/deep.js
export
default DS.Transform.extend({
deserializeRecursively: function(toTraverse) {
var hash;
if (Ember.isArray(toTraverse)) {
return Ember.A(toTraverse.map(function(item) {
return this.deserializeRecursively(item);
}, this));
} else if (!Ember.$.isPlainObject(toTraverse)) {
return toTraverse;
} else {
hash = this.generatePlainObject(Ember.keys(toTraverse), Ember.keys(toTraverse).map(function(key) {
return this.deserializeRecursively(Ember.get(toTraverse, key));
}, this));
return Ember.Object.create(hash);
}
},
deserialize: function(serialized) {
return this.deserializeRecursively(serialized);
},
serialize: function(deserialized) {
return deserialized;
},
generatePlainObject: function(keys, values) {
var ret = {};
keys.forEach(function(key, i) {
ret[key] = values[i];
});
return ret;
}
});
This is a mixin for Models with deep raw objects which accomplish (2) & (3) & (4)
// app/mixins/dynamic-observable.js
export
default Ember.Mixin.create({
propertiesToAnalyze: [],
registerRecursively: function(toTraverse, path, propsToObserve) {
if (Ember.isArray(toTraverse)) {
propsToObserve.addObject(path + '.#each');
if (toTraverse.length > 0) {
this.registerRecursively(toTraverse[0], path + '.#each', propsToObserve);
}
} else if (!(toTraverse instanceof Ember.Object)) {
propsToObserve.addObject(path);
} else {
Ember.keys(toTraverse).forEach(function(propertyName) {
this.registerRecursively(Ember.get(toTraverse, propertyName), path + '.' + propertyName, propsToObserve);
}, this);
}
},
addDynamicObserver: function(propertyNameToAnalyze) {
var propertyToAnalyze = this.get(propertyNameToAnalyze),
propsToObserve = Ember.A([]),
currentDynamicProps = this.get('currentDynamicProps'),
propsToRemove = currentDynamicProps.filter(function(prop) {
return new RegExp('^' + prop + '.').test(prop);
});
propsToRemove.forEach(function(prop) {
Ember.removeObserver(prop, this, dynamicPropertiesObserver)
}, this);
currentDynamicProps.removeObjects(propsToRemove);
this.registerRecursively(propertyToAnalyze, propertyNameToAnalyze, propsToObserve);
propsToObserve.forEach(function(prop) {
Ember.addObserver(this, prop, this, 'dynamicPropertiesObserver');
}, this);
currentDynamicProps.addObjects(propsToObserve);
},
dynamicPropertiesObserver: function(sender, key, value, rev) {
this.set('dynamicPropertyTimestamp', new Date().getTime())
},
addDynamicObservers: function() {
this.get('propertiesToAnalyze').forEach(this.addDynamicObserver, this);
},
init: function() {
this._super();
this.get('propertiesToAnalyze').forEach(function(prop) {
Ember.addObserver(this, prop, this, Ember.run.bind(this, this.addDynamicObserver, prop));
}, this);
},
dynamicPropertyTimestamp: null,
currentDynamicProps: Ember.A([])
});
This is how you use the mixin on a model:
// app/models/some-object.js
import DynamicObservable from 'app/mixins/dynamic-observable';
export
default DS.Model.extend(DynamicObservable, {
dictionary: DS.attr('deep'),
propertiesToAnalyze: ['dictionary']
});
Finally, this is an array controller which its model is an array of some-object models
export
default Ember.ArrayController.extend({
message: '',
observeDictionaries: function() {
this.set('message', 'A dictionary has been changed. change time: ' + new Date().getTime());
}.observes('#each.dynamicPropertyTimestamp')
});
Related
I want to modify the attributes in model.
models/example.js:
export default DS.Model.extend({
value: DS.attr('number'),
modified_value: function() {
/*Some Calculations*/
return modified_value
}.property('value'),
})
above code works fine, but it creates another extra attribute in model. So to avoid this,I want to move it to the RESTSerializer. How to do this ?
Create a new file (example.js) in directory transforms.
export default DS.Transform.extend({
deserialize: function(serialized) {
return serialized;
// if (serialized) {
// return JSON.stringify(serialized);
// } else {
// return '';
// }
},
serialize: function(deserialized) {
return deserialized;
// if (deserialized) {
// return JSON.stringify(deserialized);
// } else {
// return [];
// }
}
});
In your model file example change the attr to DS.attr('example')
export default DS.Model.extend({
value: DS.attr('example'),
I need to access an ember controllers context from within a regular object. Currently I am saving a reference to the controller context in the init() method that seems a little crappy.
let self = this //saving the context here
export default Ember.controller.extend({
init() {
this._super(...arguments);
self = this;
},
settings: {
crud: {
read: {
enabled: true,
default() {
return self.get('blah.blah'); //Need to access the controller context
}
}
}
}
});
So I need access to the controller self.get('blah.blah'). Is there a better way to do this?
Use a computed property closure
export default Ember.controller.extend({
settings: Ember.computed(function() {
const controller = this;
return {
crud: {
read: {
enabled: true,
defaults() {
return controller.get('blah.blah');
}
}
}
};
})
});
Accessing object
this.get('settings').crud.read.defaults(); // "blah.blah"
I have a simple rsvp helper that lets me wrap an ajax call as a simple promise
var PromiseMixin = Ember.Object.create({
promise: function(url, type, hash) {
return new Ember.RSVP.Promise(function(resolve, reject) {
hash.success = function(json) {
return Ember.run(null, resolve, json);
};
hash.error = function(json) {
if (json && json.then) {
json.then = null;
}
return Ember.run(null, reject, json);
};
$.ajax(hash);
});
}
});
This works great and is then-able like you'd expect. The problem is when I have code that needs another promise that wraps this low level one first.
example
In my ember controller I might do this
Appointment.remove(this.store, appointment).then(function() {
router.transitionTo('appointments');
}, function() {
self.set('formErrors', 'The appointment could not be deleted');
});
In my Appointment model I'm doing this for the "remove"
remove: function(store, appointment) {
return this.xhr('/api/appointments/99/', 'DELETE').then(function() {
store.remove(appointment);
//but how I do return as a promise?
}, function() {
//and how can I return/bubble up the failure from the xhr I just sent over?
});
},
xhr: function(url, type, hash) {
hash = hash || {};
hash.url = url;
hash.type = type;
hash.dataType = "json";
return PromiseMixin.promise(url, type, hash);
}
Current my controller always falls into the "fail" state (even when my ajax method returns a 204 and is successful). How can I do a "chained promise" return from this remove method in my model to enable controllers to invoke it as a "thenable" like I have above?
Couldn't you do something like this?
remove: function(store, appointment) {
var self= this;
return new Ember.RSVP.Promise(function(resolve,reject) {
self.xhr('/api/appointments/99/', 'DELETE').then(function(arg) {
store.remove(appointment);
resolve(arg);
}, function(err) {
reject(err);
});
});
},
I'm trying to create a User.current() in my application, which pulls data from my server using $.getJSON('/users/current', function(data) { ... });. I am using the Singleton method that Discourse uses, which does the following:
Dashboard.Singleton = Ember.Mixin.create({
// See https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/mixins/singleton.js
current: function() {
if (!this._current) {
this._current = this.createCurrent();
}
return this._current;
},
createCurrent: function() {
return this.create({});
}
});
And in my User singleton model, I've rewritten createCurrent as follows:
Dashboard.User.reopenClass(Dashboard.Singleton, {
createCurrent: function() {
return Ember.Deferred.promise(function(p) {
return p.resolve($.getJSON('/users/current').then(function(data) {
return Dashboard.User.create(data);
}));
});
}
});
User is a normal Ember object model:
Dashboard.User = Ember.Object.extend({
});
This does request the data from the server, but the function is not setting User.current() correctly - when I inspect it, User.current() has none of the properties that should be set, such as name.
How can I return and set the current user using Ember's deferred and promises?
That's cause you're returning the promise in place of the user.
Why don't you create the user, then fill in the properties later.
Or use the Promise Proxy pattern that Ember Data uses (the promise can be used as the object once resolved)
DS.PromiseObject = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin);
function promiseObject(promise) {
return DS.PromiseObject.create({ promise: promise });
}
Since $.getJSON('/users/current') returns a promise, might as well use that.
createCurrent: function() {
return $.getJSON('/users/current').then(function(data) {
return Dashboard.User.create(data);
});
}
Then you need to keep in mind that createCurrent returns a promise, not the object itself so you will need to:
current: function() {
if (!this._current) {
var that = this;
this.fetching = true;
this.createCurrent().then(function(val) {
that.fetching = false;
that._current = val;
});
}
return this._current;
},
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.