Ember.js update model attribute - ember.js

I do not have the best understanding of how ember.js works. I am currently working on updating an attribute called features (an array of strings) that each owner has using a multi select checkbox. So far everything seems to be working except for the updated features attribute is not saving. When I click the checkbox it updates the selected computed property in the multi-select-checkboxes component. I thought if I was passing model.owner.features as selected to the component it would directly update the model when the component changes.
(function() {
const { shroud } = Sw.Lib.Decorators;
Sw.FranchisorNewAnalyticsConnectionsUsersRoute = Ember.Route.extend({
currentFranchisorService: Ember.inject.service('currentFranchisor'),
#shroud
model({ account_id }) {
console.log("load model in route")
return Ember.RSVP.hash({
account: this.get('store').find('account', account_id),
owners: Sw.AccountOwner.fetch(account_id),
newAccountOwner: Sw.AccountOwner.NewAccountOwner.create({ accountID: account_id }),
});
},
actions: {
#shroud
accountOwnersChanged() {
this.refresh();
},
close() {
this.transitionTo('franchisor.newAnalytics.connections');
},
},
});
})();
users controller:
(function() {
const { shroud, on, observer } = Sw.Lib.Decorators;
Sw.FranchisorNewAnalyticsConnectionsUsersController = Ember.Controller.extend(Sw.FranchisorControllerMixin, {
isAddingUser: false,
adminOptions: [{
label: 'Employee Advocacy',
value: 'employee advocacy'
}, {
label: 'Second Feature',
value: 'other'
}, {
label: 'Can edit users?',
value: 'edit_users'
}],
});
})();
users.handlebars
{{#each model.owners as |owner|}}
<tr
<td>
{{owner.name}}
</td>
<td>
{{owner.email}}
</td>
<td>{{if owner.confirmedAt 'Yes' 'No'}}</td>
<td>
{{log 'owner.features' owner.features}}
{{multi-select-checkboxes
options=adminOptions
selected=owner.features
owner=owner
model=model
}}
</td>
<td>
<button class="btn light-red-button"
{{action "remove" owner}}>
Remove
</button>
</td>
</tr>
{{/each}}
multi-select-checkboxes.handlebar:
{{#each checkboxes as |checkbox|}}
<p>
<label>
{{input type='checkbox' checked=checkbox.isChecked}}
{{checkbox.label}}
</label>
</p>
{{/each}}
multi_select_checkboxes.jsx:
// Each available option becomes an instance of a "MultiSelectCheckbox" object.
var MultiSelectCheckbox = Ember.Object.extend({
label: 'label',
value: 'value',
isChecked: false,
changeValue: function () { },
onIsCheckedChanged: Ember.observer('isChecked', function () {
var fn = (this.get('isChecked') === true) ? 'pushObject' : 'removeObject';
this.get('changeValue').call(this, fn, this.get('value'));
})
});
Sw.MultiSelectCheckboxesComponent = Ember.Component.extend({
labelProperty: 'label',
valueProperty: 'value',
// The list of available options.
options: [],
// The collection of selected options. This should be a property on
// a model. It should be a simple array of strings.
selected: [],
owner: null,
model: null,
checkboxes: Ember.computed('options', function () {
console.log("CHECKBOXES", this.get('model'))
var _this = this;
var labelProperty = this.get('labelProperty');
var valueProperty = this.get('valueProperty');
var selected = this.get('selected');
var model = this.get('model');
return this.get('options').map(function (opt) {
var label = opt[labelProperty];
var value = opt[valueProperty];
var isChecked = selected.contains(value);
return MultiSelectCheckbox.create({
label: label,
value: value,
isChecked: isChecked,
model: model,
changeValue: function (fn, value, model) {
_this.get('selected')[fn](value);
console.log("model in middle", this.get('model'))
this.get('model').save();
}
});
});
}),
actions: {
amountChanged() {
const model = this.get(this, 'model');
this.sendAction('amountChanged', model);
}
}
});

Seems like you have a pretty decent understanding to me! I think your implementation is just missing a thing here or there and might be a tad more complex than it has to be.
Here's a Twiddle that does what you're asking for. I named the property on the model attrs to avoid possible confusion as attributes comes into play with some model methods such as rollbackAttributes() or changedAttributes().
Some things to note:
You don't need to specify a changeValue function when creating your list of checkbox objects. The onIsCheckedChanged observer should be responsible for updating the model's attribute. Just pass the model or its attribute you want to update (the array of strings) into each checkbox during the mapping of the options in the multi select checkbox:
return Checkbox.create({
label: option.label,
value: option.value,
attributes: this.get('owner.attrs') // this array will be updated by the Checkbox
});
If the model you retrieve doesn't have any data in this array, Ember Data will leave the attribute as undefined so doing an update directly on the array will cause an error (e.g., cannot read property 'pushObject' of undefined) so be sure the property is set to an array before this component lets a user update the value.
The observer will fire synchronously so it might be worthwhile to wrap it in a Ember.run.once() (not sure what else you will be doing with this component / model so I note this for completeness).
If you want to save the changes on the model automatically you'll need to call .save() on the model after making the update. In this case I would pass the model in to each checkbox and call save after making the change.

Related

Ember-rails: function returning 'undefined' for my computed value

Both functions here return 'undefined'. I can't figure out what's the problem.. It seems so straight-forward??
In the controller I set some properties to present the user with an empty textfield, to ensure they type in their own data.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
//quantitySubtract: function() {
//return this.get('quantity') -= this.get('quantity_property');
//}.property('quantity', 'quantity_property')
quantitySubtract: Ember.computed('quantity', 'quantity_property', function() {
return this.get('quantity') - this.get('quantity_property');
});
});
Inn the route, both the employeeName and location is being set...
Amber.ProductsUpdateRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
},
//This defines the actions that we want to expose to the template
actions: {
update: function() {
var product = this.get('currentModel');
var self = this; //ensures access to the transitionTo method inside the success (Promises) function
/* The first parameter to 'then' is the success handler where it transitions
to the list of products, and the second parameter is our failure handler:
A function that does nothing. */
product.set('employeeName', this.get('controller.employee_name_property'))
product.set('location', this.get('controller.location_property'))
product.set('quantity', this.get('controller.quantitySubtract()'))
product.save().then(
function() { self.transitionTo('products') },
function() { }
);
}
}
});
Nothing speciel in the handlebar
<h1>Produkt Forbrug</h1>
<form {{action "update" on="submit"}}>
...
<div>
<label>
Antal<br>
{{input type="text" value=quantity_property}}
</label>
{{#each error in errors.quantity}}
<p class="error">{{error.message}}</p>
{{/each}}
</div>
<button type="update">Save</button>
</form>
get rid of the ()
product.set('quantity', this.get('controller.quantitySubtract'))
And this way was fine:
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
Update:
Seeing your route, that controller wouldn't be applied to that route, it is just using a generic Ember.ObjectController.
Amber.ProductController would go to the Amber.ProductRoute
Amber.ProductUpdateController would go to the Amber.ProductUpdateRoute
If you want to reuse the controller for both routes just extend the product controller like so.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
});
Amber.ProductUpdateController = Amber.ProductController.extend();
I ended up skipping the function and instead do this:
product.set('quantity',
this.get('controller.quantity') - this.get('controller.quantity_property'))
I still dont understand why I could not use that function.. I also tried to rename the controller.. but that was not the issue.. as mentioned before the other two values to fetches to the controller...
Anyways, thanks for trying to help me!

Observer in DS.Model for an attribute fires when even the attribute is just used in template

I've written an array controller with pagination function.
When I switch to another pages for the first time, there's no problem.
But if I reviist the page I visited before, and observer for an attribute that is used is template is triggered.
(in this case, published)
When I remove {{#unless published}}...{{/unless}} from template, the observer isn't triggered anymore when I revisit the page where I've already visited.
I don't think I've done weird thing on my controllers....
(When pagination button is clicked, it simply changes controllers's page)
(I've written observer for title in model class to test whether this issue is limited to published property, and observer for title also behaves like observer for published. So this issue doesn't seem to limited to published property )
I'm using
Ember : 1.7.1+pre.f095a455
Ember Data : 1.0.0-beta.9
Handlebars : 1.3.0
jQuery : 1.11.1
and I tried beta and canary version of ember, but this issue remains same.
Here is my Route
MuteAdmin.IndexRoute = Ember.Route.extend({
model: function(params, transition, queryParams) {
var search = params.search || '';
var page = params.page || 1;
return this.store.find(this.get('articleModelClassName'), {
search: search,
page: page
});
},
setupController: function(controller, model) {
controller.set('model', model);
var will_paginate_meta = model.get("meta.will_paginate");
controller.set('totalPages', will_paginate_meta.total_pages);
controller.set('previousPage', will_paginate_meta.previous_page);
controller.set('nextPage', will_paginate_meta.next_page);
}
});
and here is my controller
MuteAdmin.IndexController = Ember.ArrayController.extend(MuteAdmin.Modelable, {
queryParams: ['page', 'search'],
page: 1,
totalPages: null,
pageChanged: function() {
this.store.find(this.get('articleModelClassName'), {
search: this.get('search'),
page: this.get('page')
}).then(function(model) {
this.set('model', model);
var will_paginate_meta = model.get("meta.will_paginate");
this.set('totalPages', will_paginate_meta.total_pages);
this.set('previousPage', will_paginate_meta.previous_page);
this.set('nextPage', will_paginate_meta.next_page);
}.bind(this));
}.observes('page'),
actions: {
doSearch: function() {
this.store.find(this.get('articleModelClassName'), {
search: this.get('search'),
page: 1
}).then(function(model) {
this.set('model', model);
var will_paginate_meta = model.get("meta.will_paginate");
this.set('totalPages', will_paginate_meta.total_pages);
this.set('previousPage', will_paginate_meta.previous_page);
this.set('nextPage', will_paginate_meta.next_page);
this.set('page', will_paginate_meta.current_page);
}.bind(this));
}
}
});
and here is my template
{{#each controller}}
<tr>
<td>{{link-to title "edit" this}} {{#unless published}}<small class="text-muted">비공개</small>{{/unless}}</td>
<td>{{author.name}}</td>
<td>{{category.title}}</td>
<td>시간 지정</td>
<td>{{viewCount}}</td>
</tr>
{{/each}}
and here is my model which has observers
MuteAdmin.Article = DS.Model.extend({
title: DS.attr( 'string' ),
body: DS.attr( 'string' ),
category: DS.belongsTo('category'),
author: DS.belongsTo('user'),
viewCount: DS.attr('number'),
published: DS.attr('boolean', { defaultValue: true }),
publishScheduled: DS.attr('boolean', { defaultValue: false }),
publishScheduleTime: DS.attr('date'),
publishedChanged: function() {
if (this.get('published') == true) {
this.set('publishScheduled', false);
}
console.log('published changed! ' + this.toString());
}.observes('published'),
});
Never mind, I know what it is. Your making a call to the server for the records that already exist. The results are merging into the pre-existing records in the store causing the model to invalidate and observer to fire.
http://emberjs.jsbin.com/OxIDiVU/1043/edit

handlebars, checked checkbox if the permission is true by an object array

I am working with backbone and handlebars for a month and I have this problem.
I have this object that I get from my model:
Object {list: Object, permissions: Object}
list: Object
0: Object
id: "1"
name: "cms"
__proto__: Object
1: Object
2: Object
3: Object
permissions: Object
analytics: true
categories: false
cms: true
coupons: false
Now with handlebars I'm trying to find out how I can checked the box when the relative permission value is true like this:
{{#each list}}
<tr>
<th><input type="checkbox" id="{{id}}"
{{#each permission}}
{{#ifCond this {{name}} }}//that's where my current error is when I try to precompile this template,the syntax is wrong
checked
{{/ifCond}}
{{/each}}
/>{{name}}
</th>
</tr>
{{/each}}
ifCond is a function that I builded in handlebars.js:
Handlebars.registerHelper('ifCond', function(v1, v2, options) {
if(v1 === v2) {
return options.fn(this);
}
return options.inverse(this);
});
Hope someone can help me out!!
Thank you!
This is how you would have written your template if there was no helper in the first place.
Start from this point
Without Helper
In the template you were trying to iterate over objects, which is not the right approach.
You should leave that to the template helper to take care of that passing the correct context.
---- This is again wrong ( something in these lines)
| compare="name"
{{#ifCond this {{name}} }}
^
^ ----- Context
|
Name of helper
under which it is
registered
When you pass a value to compare=name the value will be replaces by that key value of that object.
But remember that you are already inside each loop that iterates over list object.
So this will point to that object. So you need to go back to the parent object 1 step using ../ which will give you access to the permissions object.
{{#ifCond this compare=name }}
Will map to the arguments of the helper.
'ifCond', function(v1, v2, options) {
v1 will be this context
v2 will map to the handlebar options
options will be undefined as no 3rd argument was passed in the helper
So the template will be of this form
{{#ifCond this obj=../this compare=this.name}}
this - context of each (current object in loop)
obj= will be dumped into the options.hash object. It is the main object.
compare will be the name attribute inside this context
maps to
'ifCond', function (context, options)
So your template structure will look like below
Template
<script type="text/template" id="handlebar-template">
{{#each list}}
<tr>
<th>
<span>
<input type="checkbox" id="{{id}}"
{{#ifCond this obj=../this compare=this.name}}
checked
{{/ifCond}} />
</span>
<span class="name">{{name}}</span>
</th>
</tr>
{{/each}}
</script>
Helper
Handlebars.registerHelper('ifCond', function (context, options) {
var permissions = options.hash.obj.permissions,
name = options.hash.compare;
if (permissions[name]){
return options.fn(this);
}
return options.inverse(this);
});
Full Code
// The object in question
var obj = {
"list": [{
"id": 1,
"name": "cms"
}, {
"id": 2,
"name": "analytics"
}, {
"id": 3,
"name": "coupons"
}, {
"id": 4,
"name": "categories"
}],
"permissions": {
"analytics": true,
"categories": false,
"cms": true,
"coupons": false
}
};
// Model for the Object
var HandlebarModel = Backbone.Model.extend({});
var handlebarModel = new HandlebarModel(obj);
// View that will render the template inside the table
var HandlebarView = Backbone.View.extend({
el: $('#table'),
initialize: function () {
var source = $('#handlebar-template').html();
this.template = Handlebars.compile(source);
},
render: function () {
var $elem = $('tbody', this.$el);
$elem.empty();
$elem.append(this.template(this.model.toJSON()));
}
});
Handlebars.registerHelper('ifCond', function (context, options) {
console.log("this context " + this);
console.log("name -" + options.hash.compare);
console.log("Main Object - " + options.hash.obj);
var permissions = options.hash.obj.permissions,
name = options.hash.compare;
if (permissions[name]){
return options.fn(this);
}
return options.inverse(this);
});
var handlebarView = new HandlebarView({
model: handlebarModel
});
handlebarView.render();
Check Fiddle

Collection of objects of multiple models as the iterable content in a template in Ember.js

I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});

Item specific actions

Against my controller, I have an item with an array of items which I want to display a list of and have an action to remove an item.
Have taken most of the code from a similar question item-specific actions in ember.js collection views. Majority of this works, the address display works and the items render, however the action does not seem to have the correct context and therefore does not perform the remove action.
Controller:
App.EditAddressesController = Em.ArrayController.extend({
temporaryUser: {
firstName: 'Ben',
lastName: 'MacGowan',
addresses: [{
number: '24',
city: 'London'
etc...
}, {
number: '23',
city: 'London'
etc...
}]
}
});
The temporaryUser is an EmberObject (based off a User model), and each item within the addresses array is another EmberObject (based off an Address model) - this has just been simplified for purposes of displaying code.
Parent view:
{{#each address in temporaryUser.addresses}}
{{#view App.AddressView addressBinding="this"}}
{{{address.display}}}
<a {{action deleteAddress target="view"}} class="delete">Delete</a>
{{/view}}
{{/each}}
App.AddressView:
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function() {
var address = this.get('address'),
controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
$.each(currentAddresses, function(i) {
if(currentAddresses[i] == address) {
currentAddresses.splice(i, 1);
}
});
}
else {
//Throw error saying user must have at least 1 address
}
}
});
Found out it was an issue with my each:
{{#each address in temporaryUser.addresses}}
should have been:
{{#each temporaryUser.addresses}}
And my AddressView should use removeObject (like the original code sample)
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function() {
var address = this.get('address'),
controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
currentAddresses.removeObject(address);
}
else {
//Throw error saying user must have at least 1 address
}
}
});
I think your code could become more readable if you approach it that way:
1- Modify your action helper to pass the address with it as context. I modified your Handlebars a bit to reflect the way i am approaching stuff like this and works for me. I think this is the "Ember way" of doing this kind of stuff, since a the properties in a template are always a lookup against the context property of the corresponding view:
{{#each address in temporaryUser.addresses}}
{{#view App.AddressView contextBinding="address"}}
{{{display}}}
<!-- this now refers to the context of your view which should be the address -->
<a {{action deleteAddress target="view" this}} class="delete">Delete</a>
{{/view}}
{{/each}}
2 - Modify the handler in your view to accept the address as a parameter:
App.AddressView = Ember.View.extend({
tagName: 'li',
address: null,
deleteAddress: function(address) {
var controller = this.get('controller'),
currentAddresses = controller.get('temporaryUser.addresses');
if(currentAddresses.length > 1) {
currentAddresses.removeObject(address);
}
else {
//Throw error saying user must have at least 1 address
}
}
});