I have an actual quite simple situation:
A route to add a new item. In the corresponding controller I pre define a mockup model of my new item:
item: Ember.Object.create({
date: moment(),
amountTotal: '',
netto: '',
//...more properties
}),
This needs to be an Ember-Object, not a plain js-Object, because otherwise other things would break.
When I try to safe that newly created item:
actions: {
addItem: function() {
let expense = this.store.createRecord('expense', this.get('item'));
},
//....
}
I get the error
Assertion Failed: Cannot clone an Ember.Object that does not implement Ember.Copyable
So my Question is:
How can I create an Object that implements Ember.Copyable?
Or is there any way around this?
Yes, I've read the two other questions about that.
The first gives a soulution where I would initially create a record in the store. This has the usual downsides to it (already populating in lists, ..).
I've also tried all ways I could think of to get around that like
item: Ember.Copyable.create({...})
// or
let newItem = Ember.copy(this.get('item'));
let expense = this.store.createRecord('expense', newItem);
// and many more
Finally:
If there is a way to mock up a new Item (best with the definitions of the model) without creating a record, this would be the absolute best...
You can try specifying default value for all the model properties, and then simply you don't need to provide argument for createRecord method.
Like the below, models/expense.js and you can simply say this.store.createRecord('expense') this will come up with all the default values.
export default Model.extend({
name: attr('string',{ defaultValue: 'Sample Name'}),
date: attr('date',{
defaultValue(){
//You can write some code and the return the result.
//if you have included moment, you can use that.
return Date();
}
}),
amount: attr('number',{ defaultValue: 10}),
totalAmount: Ember.computed('amount',function(){
return this.get('amount')*10;
})
});
Using JSON.stringify and JSON.parse like the below,
this.store.createRecord('expense', JSON.parse(JSON.stringify(this.get('item'))))
Created twiddle for reference.
Related
I have a small problem, I created a package to manage forms, everything worked correctly until I needed to create a dynamic form based on an API response.
Here is the package: https://github.com/nulvem/js-form
It can be installed with npm:
npm install #nulvem/js-form
Here is my Vue component calling the Form class:
<script>
import {Form} from '#nulvem/js-form'
export default {
data() {
return {
form: new Form({
template: {
value: null,
validation: {
rules: 'required',
messages: {
required: 'You must select the type of report'
}
}
}
})
}
},
mounted() {
this.form.template = 'random-id'
// These data will be obtained from an API
let fields = [
'start_date',
'end_date',
'wallets',
]
fields.forEach((field) => {
let fieldData = {
value: null,
validation: {
rules: 'required',
messages: {
required: `You must enter the field ${field.description}`
}
}
}
this.form.$addField(field, fieldData)
})
// Here we can only see the getter/setter of the template property that was passed when instantiating the Form class
console.log(this.form)
}
}
</script>
The problem is: The fields I passed to the form during the creation of new Form ({...}) instance are reactive, since when I add new fields by calling the this.form.$AddField() function the fields are not getting reactive.
The funny thing is that the construct of the Form class calls a function called this.form.$AddFields() that calls the this.form.$AddField() function several times.
Why it works inside the constructor and don't when called separated? How to fix this issue?
Any idea what might be going on? This is driving me crazy, I've been trying to find the problem for several hours and I can't find it.
Edit
As a quick fix we make a change in the package where it is possible to pass the Form instance as a third parameter to the $addFields function, so the attributes are reactive, however we would like this to work without having to pass this parameter.
VueJS lose the reactivity of properties that are added to your data after the render. You can read more about this, here: https://br.vuejs.org/v2/guide/reactivity.html.
A quick way to try to solve this problem is adding a this.$forceUpdate() everytime that you add a new property, but it also can bring another problems depending of your context.
Or you can try using Vue.set() method, instead of your $addField method. You can read more in the same link above.
remove the context and change this line for reactivity:
this.$initialValues[field] = fieldDeclaration.value;
=>>>
this.$initialValues[field] = fieldDeclaration.value;
this.$initialValues = Object.assign({}, this.$initialValues);
So, I have an object inside a ReactiveDict that I want to update:
dataSet = new ReactiveDict('dataSet');
dataSet.set({
defaultInfo: {
title: 'Default'
}
});
updateTitle(title) {
// not sure what to put here to update the default title
dataSet.set({ })
}
Thoughts? I'm using ES6.
So, I was able to create a solution, though not exactly what I had in mind.
I wasn't able to find a way to update the object, only replace it with a new object:
updateTitle(title) {dataSet.set({
defaultInfo: _.extend(dataSet.get('defaultInfo'), {title: title})
})}
This is using underscoreJS for the _.extend
Like millions of other apps, we have a model that optionally belongs to another.
This model has a computed property that depends on the existence of that parent, like...
// some-model
parent: belongsTo('some-model'),
hasParent: Ember.computed('parent.id', function() {
return Ember.isPresent(this.get('parent.id');
}),
(We use a CP instead of computed.alias because the association can appear/disappear and alias does not observe changes)
All well and good, it works for us. I went to add a unit test...
test('hasParent', function(assert) {
const someModel = this.subject({ id: 1, name: 'Mr. Model, Sr' });
assert.equal(someModel.get('hasParent'), false,
'returns false when no parent ID');
const childModel = this.subject({ name: 'Little Model, Jr.' });
childModel.set('parent.content', someModel);
assert.equal(childModel.get('hasParent'), true,
'returns true when has parent with ID');
childModel.set('parent.content', null);
assert.equal(childModel.get('hasParent'), false,
'returns false when no parent ID');
});
Setting with parent.content seems really hacky, and I wanted to know if there is a better, more standard way of assigning a model to another. Ember docs are... sparse.
Regarding the comments posted for the question, it is better to make use of store while testing models. Hence the following twiddle is probably a better way to implement the given testing scenario. I am putting it as an answer if it is needed by anyone else in the future.
Sounds like a simple enough thing to do yet is causing me all sorts of grief.
I have a simple server model which has a few nested objects,
export default DS.Model.extend({
type: DS.attr('string'),
attributes: DS.attr(),
tasks: DS.attr()
});
I can create a new record in the route using
export default Ember.Route.extend({
model() {
return this.store.createRecord('server');
},
actions: {
create(server) {
server.save().then(() => this.transitionTo('servers'));
}
}
});
and in the related .hbs I'm setting a few properties of attributes and tasks using value=model.attributes.name from a form for example.
This all works fine. I however want to add a few more properties from the route during create such as default values.
Using server.set('attributes.size', 'large'); doesn't work as Ember doesn't know about size yet as it's a new record.
I can use setProperties but this seems to wipe out every other value
server.setProperties({
attributes: {
size: "large"
},
tasks: {
create: true
}
});
size is now correctly set, however name is now null because I didn't specify it in the setProperties...
What's the proper way to go about this? Surely I don't need to map out all the properties in setProperties? That seems wasteful and very error prone.
Something I've thought is should attributes just be its own model and have a relationship with Server? Even though this is always a 1-to-1 and 1-to-1 relationship?
I would recommend using ember-data-model-fragments addon as a solution in this case.
https://github.com/lytics/ember-data-model-fragments
Other option using a separate model for attributes and setting up a 1-to-1 relation. Both would be belongsTo, however it is depend on your database and API also, so you have to align your backend system to match with this new structure.
When saving a model, Backbone determines whether to send an HTTP POST or PUT request by whether or not the model's ID attribute is set. If there is an ID, the model is considered to already exist.
For my application, this logic is incorrect because I must allow the user to specify an ID (as I interact with a poorly designed legacy system).
How should I handle this problem? I still would like to use PUT if the model is changed.
I am considering the following options:
Override isNew, which is the Backbone method that simply checks if an ID is present.
Override sync.
Determine if the concept of cid would somehow solve the problem.
One solution is to address the symptoms rather than the cause. Consider adding to your model a new create method:
var FooModel = Backbone.Model.extend({
urlRoot: '/api/foo',
create: function () {
return this.save(null, {
type: 'post', // make it a POST rather than PUT
url: this.urlRoot // send the request to /api/foo rather than /api/foo/:id
});
}
});
This is the solution I use, but I don't consider it ideal because the view logic/caller now needs to call create rather than save when creating (which is rather easy to do). This extended API bothers me for my use-case (despite working and being rather small), but perhaps it'll work for yours.
I'd love to see some additional answers to this question.
So I went down the path of trying to change up isNew.
I came up with new criteria that would answer whether a model is new:
Was the model created via a fetch from a collection? Then it's definitely not new.
Was the model created with an ID attribute? This is a choice I made for my case, see disadvantages below for the effect of doing this, but I wanted to make new Model({ id: 1, name: 'bob' }) not be considered new, while setting the ID later on (new Model({ name:
bob'}).set('id', 1)) would be.
Was the model ever synced? If the model was successfully synced at any point, it's definitely not new because the server knows about it.
Here's what this looks like:
var UserDefinedIDModel = Backbone.Model.extend({
// Properties
_wasCreatedWithID: false,
_wasConstructedByFetch: false,
_wasSynced: false,
// Backbone Overrides
idAttribute: 'some_id',
urlRoot: '/api/foo',
constructor: function (obj, options) {
this._wasCreatedWithID = !!obj[this.idAttribute];
this._wasConstructedByFetch = options && options.xhr && options.xhr.status === 200;
// Preserve default constructor
return Backbone.Model.prototype.constructor.apply(this, arguments);
},
initialize: function () {
this.on('sync', this.onSync.bind(this));
},
isNew: function () {
// We definitely know it's not new
if (this._wasSynced || this._wasConstructedByFetch) return false;
// It might be new based on this. Take your pick as to whether its new or not.
return !this._wasCreatedWithID;
},
// Backbone Events
onSync: function () {
this._wasSynced = true;
}
});
Advantages over the other answers
No logic outside of the backbone model for handling this odd usecase.
No server-side changes to support this
No new pseudo properties
Disadvantages
This is a lot of code when you could just create a new create method as per my other answer.
Currently myCollection.create({ some_id: 'something' }); issues a PUT. I think if you need support for this you'll have to do myCollection.create({ some_id: 'something' }, { url: '/api/foo', type: 'post' }); You can remove the _wasCreatedWithoutID check to fix this, but then any construction of a new model that derives its data from an existing one will be treated as new (in my case, this is undesirable).
Here's another solution :
In your model define an idAttribute that don't exists in your server model/table/... and that wouldn't be displayed to the DOM.
So let's suppose that the JSON that you send to the server is as follows :
{
'id': 1,
'name': 'My name',
'description': 'a description'
}
Your model should look like :
var MyModel = Backbone.Model.extend({
idAttribute: 'fakeId'
});
Now, when you create a new model and try to save it to the server, no one would initialize the fakeId and it would be considered a new object (POST).
When you fetch your model from the server you have to set the fakeId in your model, and your server must duplicate the id in the fakeId like this your model will be considered as an existing (PUT)