Ember clone model for new record - ember.js

I want to make a clone of a model currently being edited.
I've found a couple of ways that almost work. But neither are perfect.
1) model.get('data.attributes') gets all the attributes except for relationships in camelCase form, generates a new record fine but the relationships are missing of course.
2) model.serialize() generates a JSON object, with all attributes including relationships. But createRecord will not handle it well since the object is not camelCased (attributes with underscores like first_name will not be handled)
After my clone has been created I want to transaction.createRecord(App.Document, myNewModelObject) change/set a couple of attributes and finally commit(). Anyone have some insight in how to do this?

Now we have a add-on to copy models
ember-cli-copyable
With this add on, just add the Copyable mix-in to the target model which is to be copied and use the copy method
Example from the add-on site
import Copyable from 'ember-cli-copyable';
Account = DS.Model.extend( Copyable, {
name: DS.attr('string'),
playlists: DS.hasMany('playList'),
favoriteSong: DS.belongsTo('song')
});
PlayList = DS.Model.extend( Copyable, {
name: DS.attr('string'),
songs: DS.hasMany('song'),
});
//notice how Song does not extend Copyable
Song = DS.Model.extend({
name: DS.attr('string'),
artist: DS.belongsTo('artist'),
});
//now the model can be copied as below
this.get('currentAccount.id') // => 1
this.get('currentAccount.name') // => 'lazybensch'
this.get('currentAccount.playlists.length') // => 5
this.get('currentAccount.playlists.firstObject.id') // => 1
this.get('currentAccount.favoriteSong.id') // => 1
this.get('currentAccount').copy().then(function(copy) {
copy.get('id') // => 2 (differs from currentAccount)
copy.get('name') // => 'lazybensch'
copy.get('playlists.length') // => 5
copy.get('playlists.firstObject.id') // => 6 (differs from currentAccount)
copy.get('favoriteSong.id') // => 1 (the same object as in currentAccount.favoriteSong)
});

How about using toJSON() method instead of serialize() like this
js
transaction.createRecord(App.Document, model.toJSON());

Most simple way I found:
function cloneModel(model) {
const root = model._internalModel.modelName;
const store = model.get('store');
let attrs = model.toJSON();
attrs.id = `clone-${attrs.id}`;
store.pushPayload({
[root]: attrs
});
return store.peekRecord(root, attrs.id);
}

You can use https://github.com/offirgolan/ember-data-copyable. Been using this package for some time now, and it is very much reliable. Some of its features :
Shallow & deep copy an Ember Data model
Shallow & deep copy model relationships
Handles cyclical relationships
Handles custom transforms to create true copies
Overwrite, ignore attributes, and copy objects by reference
Intelligent failure and cleanup
Uses ember-concurrency to allow cancelling a copy task

Here is the simple way to clone your Ember Model with relationships. working fine.
Create a Copyable mixin like,
import Ember from 'ember';
export default Ember.Mixin.create(Ember.Copyable, {
copy(deepClone) {
var model = this, attrs = model.toJSON(), class_type = model.constructor;
var root = Ember.String.decamelize(class_type.toString().split(':')[1]);
if(deepClone) {
this.eachRelationship(function(key, relationship){
if (relationship.kind == 'belongsTo') {
attrs[key] = model.get(key).copy(true);
} else if(relationship.kind == 'hasMany' && Ember.isArray(attrs[key])) {
attrs[key].splice(0);
model.get(key).forEach(function(obj) {
attrs[key].addObject(obj.copy(true));
});
}
});
}
return this.store.createRecord(root, attrs);
}
});
Add the mixin in your model,
Note: If you want to clone your child model then, you need to include the mixin in child model as well
USAGE:
With relationship : YOURMODEL.copy(true)
Without relationship : YOURMODEL.copy()

This will also solve my problem
Account = DS.Model.extend({
name: DS.attr('string'),
playlists: DS.hasMany('playList'),
favoriteSong: DS.belongsTo('song')
});
Duplicate = Ember.Object.extend({});
TemporaryRoute = Ember.Route.extend({
model : function(){
var model = this.store.findAll('account');
var json = model.toJSON();
var duplicateModel = Duplicate.create(json);
this.set('duplicateModel', duplicateModel);
return model;
}
});

Here's an updated answer, it still doesn't handle hasMany relationships.
cloneBelongsTo: function(fromModel, toModel) {
var relationships;
relationships = Em.get(fromModel.constructor, 'relationships');
return relationships.forEach(function(relationshipType) {
var _relType;
_relType = relationships.get(relationshipType);
return _relType.forEach(function(relationship) {
var name, relModel;
relModel = Em.get(fromModel, relationship.name);
if (relationship.kind === 'belongsTo' && relModel !== null) {
name = relationship.name;
return toModel.set(name, fromModel.get(name));
}
});
});
}
And here's how I use it:
// create a JSON representation of the old model
var newModel = oldModel.toJSON();
// set the properties you want to alter
newModel.public = false;
// create a new record
newDocument = store.createRecord('document', newModel);
// call the cloneBelongsTo method after the record is created
cloneBelongsTo(model, newDocument);
// finally save the new model
newDocument.save();

Related

Ember: Return a value or set a model property from Ember promise

Update - more information below
If I have a promise, is it possible to return a value from it?
let itemData = [];
model.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
console.log(item.get('name')); // Product 1, Product 2 etc
itemData.pushObject(item);
});
},reject => {
console.log('error '+reject);
});
If I try and return the itemData array after the promise has resolved I get undefined.
Alternatively (and preferably) I'd like to be able to set a model property when the promise has resolved:
// component code
itemData:null,
init() {
let model = this.get('data');
model.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
this.set('itemData',item);
});
},reject => {
console.log('error');
});
}
The reason for all of this is that I need to sort the product items which I can only access via the promise (in this example). Having set the itemData property I was intending to do something like:
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc']
More information:
In my product route, product.items.item I have a pagination component
{{pagination-item-trad data=model}}
The model hook in the route product.items.item is
model(params) {
let itemModel = this.store.findRecord('product',params.id);
let mainModel = this.modelFor('product.items');
return Ember.RSVP.hash({
data:itemModel,
mainData:mainModel
});
}
The mainModel will include the category model for that particular product item.
Since the product-category model has a many-to-many relationship with products, I need to access the product data in my component using a promise, which was not a problem until I needed to sort the product data. What I am trying to do is obtain the product information from the promise (itemData below) and then use that in the computed property. So the question is how I can extract the data from the promise for use elsewhere in the code? Is there a better way to achieve this? I hope this is clearer!
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc']
The component in more detail:
import Ember from 'ember';
const {computed} = Ember;
export default Ember.Component.extend({
itemData:null, // i would like to set this within the promise
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc'],
init() {
let allData = this.get('data');
let mainModel = allData.mainData;
var self = this;
let itemData = [];
mainModel.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
console.log(item.get('name')); // prints Product 1 etc etc
itemData.pushObject(item);
});
self.set('itemData',itemData); // I can't do this
},reject => {
console.log('error '+reject);
});
}
// rest of code omitted for brevity
});
Your scope is wrong inside your forEach, this no longer points to your component. You can either use another fat arrow or maintain a reference to the component scope using a variable.
Additionally, I doubt you are meaning to iterate and overwrite itemData on each iteration.

Ember return length of a model created today

I am trying to do this: I have a model called 'trip', and inside trip, an attribute called 'createdToday', which returns the date when a trip is created. What I want is to return a list of trips that were made today.
Here is my trip model:
import DS from 'ember-data';
export default DS.Model.extend({
driver: DS.belongsTo('driver', {
async: true,
inverse: 'trip'
}),
..... etc .......
createdAt: DS.attr('string', {
defaultValue() {
return new Date();
}
}),
isBookedToday: function(trip) {
var today = new Date().toDateString();
return (today === trip.get('createdAt').toDateString);
},
getTripsToday: Ember.computed('trip.#each.createdAt', function() {
var tripsToday = this.get('trip');
return tripsToday.filterBy('isBookedToday', true).get('length');
})
});
In my isBookedToday, I'm trying to see if an individual trip's created time is the same as todays time, and in getTripsToday, I am trying to loop through all the trips and filtering by isBookedToday.
And in my .hbs file, I'm saying: {{trips.getTripsToday}}, which won't render anything, so something's wrong.
I guess I am most confused at Ember's #each and exactly how it works.
Thanks for any feedback.
First you have to understand that your Trip Model instances represents a single Trip! Its absolutely not the right place to put a function that gives you a filtered list of trips!
Next isBookedToday is a normal function not a Computed Property. So you can't filterBy on it.
You may want to implement a isBookedToday on your trip, but you definitely have to filter your trips on the same place where you fetch them! Probably in a model() hook or a Computed Property on a component or a controller.
So you could do but don't need to do in your models/trip.js:
...
isBookedToday: Ember.computed('createdAt', {
get() {
let now = new Date();
let created = get(this, 'createdAt');
return now.getFullYear() === created.getFullYear() &&
now.getMonth() === created.getMonth() &&
now.getDate() === created.getDate();
}
})
...
And then in your model hook:
model() {
return this.store.findAll('trip').then(trips => trips.filterBy('isBookedToday'));
}
Or in a Computed Property in a controller or a component:
tripsToday: Ember.computed('trips.#each.isBookedToday', {
return get(this, 'trips').filterBy('isBookedToday');
})
Be careful. This will result in confusing things if you leave the page open overnight! when your date changes the Computed Properties will not recompute automatically!

Accessing the store from model function in routes

I want to be able to set my model dynamically depending on a record in the store - mainly because I want to control whether or not a form submission should be a POST or PUT.
export default Ember.Route.extend({
model: function() {
let bankAccount = this.store.get('bankAccount').objectAt(0);
if (bankAccount.is_connected) {
bankAccount = this.store.createRecord('bankAccount', { account_label: 'new' });
}
return Ember.RSVP.hash({
appState: this.get('appStates').currentAppState(),
user: this.store.findAll('user').thenGetFirst(),
bankAccount: bankAccount,
});
},
});
The issue is that let bankAccount = this.store.get('bankAccount').objectAt(0); return null when I refresh the page.
However, if I run App.store.get('bankAccount').objectAt(0) in my browser console, it returns the correct record.
What am I doing incorrectly?
The store has no get method do you mean to use findAll to get the accounts?
export default Ember.Route.extend({
model() {
// A better way to do this would be to `findRecord` with an id
return this.store.findAll('bank-account').then(bankAccounts => {
let bankAccount = bankAccounts.get('firstObject');
// Properties are usually camel cased this seems weird
// Can you add your model definition?
if (bankAccount.get('is_connected')) {
bankAccount = this.store.createRecord('bank-account', { account_label: 'new' });
}
});
return Ember.RSVP.hash({
appState: this.get('appStates').currentAppState(),
// A better way to do this would be to `findRecord` with an id
user: this.store.findAll('user').thenGetFirst(),
bankAccount: bankAccount,
});
},
});
On a side note your code needs some major cleanup and rethinking, especially if you are going to be dealing with people's financial data.

How to filter records using hasMany association

Trying to figure out what should be a simple solution: filterBy records using an array of hasMany ids.
I have a model Language, which hasMany Providers, and vice versa. From a collection of Providers, I need to filterBy('language', [1,2]) or even filterBy('language', 2).
(I can't do a language.get('providers') since I'd getting the collection of Providers from another association.)
// models.language.js
var Language = DS.Model.extend({
title: DS.attr('string'),
providers: DS.hasMany('provider')
});
// models/provider.js
var Provider = DS.Model.extend({
title: DS.attr('string'),
region: DS.belongsTo('region', { async: true }),
languages: DS.hasMany('language', {async: true})
});
// In this region-component.js I have the model title,
// and then an each block listing all providers
// When the language filter changes, I need it to filter down
// just to those providers who have the language association.
providers: function() {
return this.get('model').get('providers').filterBy('languages', this.get('language'));
}.property('model')
filterBy filters based on a single property, but you can just use filter itself, passing it a selection function:
filteredProviders: function() {
let providers = this.get('providers'); // some providers to select from
let languages = this.get('languages'); // array of languages to select
// Does a provider have one of the languages we're looking for?
function providerHasLanguage(provider) {
return provider.get('languages') .
some(language => languages.indexOf(language) !== -1;
}
return providers . filter(providerHasLanguage);
}.property('providers', 'languages')
So some redditors helped me out and I worked it from there. Seems there is no native solution but to iterate through each array and render a new array from matching objects.
// app/utils/where.js
let where = function(sourceArray, property, language_id) {
let destArray = [];
sourceArray.forEach(function(obj) {
obj.get(property).forEach(function(property_object) {
let id = parseInt(property_object.get('id'));
if (language_id === id) {
destArray.push(obj);
}
});
});
return destArray;
}
export default where;
And its usage:
// in a component
import where from 'path';
let filteredProviders = where(providers,
'languages', language_id);
Minor modifications would be required to accommodate filtering from an array of multiple language_ids.

Ember: create a DS.belongsTo *outside* of a Model

I'm wanting to ideally create a DS.belongsTo / BelongsToRelationship in my own class (which is an Ember.Object, but not a DS.Model), or alternatively recreate the functionality to let me hold a reference to a record in my own class. I don't know if it's possible to use a DS.belongsTo outside of a DS.Model, or if so, how to set it up.
Background:
I have an ember-cli app using ember-data + ember-fire + firebase. One of my models has an attribute which is an object holding "type specific" information for the record. I transform this object into my own class based on the type it describes, and some times that type will have references to other records in the database. In these cases I would like to have a DS.belongsTo property set up in my typeSpecific class that I could link to in the same way as linking to a relationship in a regular model.
Alternative:
After a lot of searching and not finding any information on how to do this I made my own class which got me most of the way there. I've just noticed that although I can change the record it references on the client side and have it update, if I change it on the server-side I don't get updates coming through, so it's back to the drawing board.
If anyone is able to tell me how to make this alternative approach work that would serve the purpose too. The idea with this class is that I pass it a model name and id, and it should create the model reference and then keep model and id in sync if either side changes, and also pass through updates if anything on the model it's connected to gets changed just like a regular relationship would.
export default Ember.Object.extend({
id: null,
table: undefined,
model: undefined,
store: undefined,
init: function() {
this._super();
if(this.id && !this.model) {
this.updateModel();
}
else if(this.model && !this.id) {
this.updateId();
}
},
updateModel: function() {
var self = this;
if( this.get('id') ) {
this.store.find(this.get('table'), this.get('id')).then( function(model) {
self.set('model', model);
});
}
else {
self.set('model', undefined);
}
}.observes('id','table'),
updateId: function() {
if(this.get('model')) {
this.set('id', this.get('model.id'));
}
else {
this.set('id', null);
}
}.observes('model'),
});
Edit: code to manipulate the object above:
//Creating a reference:
this.set('target', ModelPointer.create({store:this.get('store'), table:this.get('targetTable'), id:targetId}));
//or:
this.set('target', ModelPointer.create({store:store, table:'myTable'}));
...
this.set('target.id', '42');
I believe that at the moment if I change either the id or model on the client the other will update automatically, eg:
//either:
this.set('target.id', '43');
//or:
this.store.find('myTable','43').then( function(newModel) {
self.set('target.model', newModel);
});
The problem is that if I log into Firebase and change myTable['42'].name='Fred' then the value showing on my web page which is linked to target.model.name doesn't update to 'Fred'. I suspect that if I set target.model.name to 'Fred' on the client side and save it wouldn't update the value on the server either(?)
The cleanest solution I've come up is to not store the id separately (left to the model itself). I've verified that changes I make in Firebase propagate to the displayed entry just fine.
With this solution setting the referenced model can be done with either its id or simply the model instance itself. See the controller code for examples.
Firstly, for reference, some test data for Firebase:
{
"testModels": {
"1": {
"name": "Model one"
},
"2": {
"name": "The second model"
},
"3": {
"name": "Third is the charm"
}
}
}
Thus its model app/models/test-model.js just needs the name in there.
Here is my belongsTo-like proxy class, I put mine under app/utils/proxy-class.js but it should probably be a Mixin:
import Ember from 'ember';
export default Ember.Object.extend({
remote: null, // reference to the remote DS.Model
store: null, // reference to the actual store
storeModel: null, // name of the model in the store
_watchRemote: function() {
var self = this;
if ( typeof self.get('remote') === 'object' ) {
// do nothing, already an object
if ( ! Ember.isPresent( self.get('store') ) ) {
// but set the store from the model
self.set( 'store', self.get('remote.store') );
}
} else if ( typeof self.get('remote') === 'string' ||
typeof self.get('remote') === 'number' ) {
// it's an id, so fetch the model
self._fetchModel( self.get('remote') );
}
}.observes('remote').on('init'), // on change, and during object init
_fetchModel: function( id ) {
var self = this;
self.store.find( self.get('storeModel'), id ).then(
function( model ) {
self.set( 'remote', model );
}, function ( err ) {
console.error( "couldn't read from the store:", err );
});
},
});
I created this controller, and used the browser console to change the model on the fly to test that model changes are picked up:
import Ember from 'ember';
import proxyClass from '../utils/proxy-class';
export default Ember.Controller.extend({
model: {
remoteFromId: null,
remoteFromModel: null,
},
init: function() {
var self = this;
self.set( 'model.remoteFromId',
proxyClass.create({
remote: 1,
store: self.get('store'),
storeModel: 'test-model',
})
);
self.get('store').find( 'test-model', 2 )
.then( function( model ) {
self.set( 'model.remoteFromModel',
proxyClass.create({
remote: model,
storeModel: 'test-model',
// no store provided here: set from the model
})
);
});
}
});
And the template for the controller:
<p>remoteFromId: {{model.remoteFromId.remote.id}}
{{model.remoteFromId.remote.name}}</p>
<p>remoteFromModel: {{model.remoteFromModel.remote.id}}
{{model.remoteFromModel.remote.name}}</p>