Observe properties on nested object - ember.js

Example fiddle: http://emberjs.jsbin.com/aviyUnA/9/edit?html,js,output
This is my model:
{
name: {
title: 'Mr',
first: 'Potato',
last: 'Head'
},
age: 79
}
How do I create a computed property that observes all the keys inside the name object without listing them manually?
fullName: function() {
var name = this.get('name');
return [name.title, name.first, name.last].join(' ');
}.property('name.??')
Thanks for any help!

You can customize the set call of your model: Check if the value being set involves a property of name and if it does, call notifyPropertyChange on name:
App.MyModel = Ember.Object.extend({
set: function(keyName, value) {
this._super(keyName, value);
if (keyName.indexOf('name.') > -1) {
// a property of `name` has changed => notify observers of `name`
this.notifyPropertyChange('name');
}
}
});
Demo: http://emberjs.jsbin.com/akEjubU/1/edit

Related

ember data array UI not updating on pushObject

I have a list of product-tag that I fetch for my model.
Route:
model: function() {
return {
product_tags: this.store.find('product-tag', {merchant: merchId})
}
}
I have a component that adds tags to the model, however when after I create the record and push it into the model (as suggested on other posts) my UI still isn't updating.
addTag: function(name) {
tag = this.store.createRecord('product-tag', {
name: name
});
this.model.product_tags.toArray().addObject(tag);
tag.save();
}
//model merchant.js
export default DS.Model.extend({
user_id: DS.attr('number'),
product_tags: DS.hasMany('product-tag', {async: true})
});
//model product-tag.js
export default DS.Model.extend({
merchant: DS.belongsTo('merchant'),
name: DS.attr('string'),
});
What am I doing wrong? Thanks in advance.
You should make it array in the route, so u can use it always afterwards like u want. Your calling toArray() which makes a new Array instance, then your model is not hooked to the array u just made.
model: function() {
return {
product_tags: this.store.query('product-tag', {merchant: merchId}).then(function(pt) {
return pt.toArray();
});
}
}
var x = this.get('model.product_tags') === model's p_t // true
var y = this.get('model.product_tags').toArray() === model's p_t // false
Later on just do
addTag: function(name) {
this.get('store').createRecord('product-tag', {
name: name
}).save().then(function(saved){ 
this.get('model.product_tags').pushObject(saved);
}.bind(this);
}

How to bind a model property (from controller) without it firing validation right away?

I have a simple model backed controller with a simple validation on fullname. I added the validation to the models error computed property as shown below. It' works great except that this computed property is "fired" right away showing the "please enter a username" error right when the form is rendered.
Question is -how should I so this to get the nice computed property/2 way data bound property and error message but ... something that won't fire right away (instead waiting for the user to type something first).
var UserController = Ember.Controller.extend({
actions: {
submit: function() {
//verify model is legit ... transitionToRoute if so
}
}
});
var User = Ember.Object.extend({
enteredUsername: "",
username: function() {
var enteredUsername = this.get("enteredUsername");
return enteredUsername.trim();
}.property("enteredUsername"),
usernameError: function() {
var username = this.get("username");
if (!username) {
return "please enter a username";
}
}.property("username")
});
{{input type="text" value=model.enteredUsername}}
<span class="input-error">{{model.usernameError}}</span>
Like #Sisir said, you may need to have some sort of variable to check if the model value is dirty. Here is a way to get that implemented. Basically a model property will comprise of a value and isDirty property. So enteredUsername will be
enteredUsername: {
value: '',
isDirty: false
}
Your modified code will look like
var User = Ember.Object.extend({
enteredUsername: {
value: '',
isDirty: false
},
username: function() {
var value = this.get("enteredUsername.value").trim();
//Once dirty is set, then dont reset.
if(!this.get('enteredUsername.isDirty')) {
this.set('enteredUsername.isDirty', value.length > 0);
}
return value;
}.property('enteredUsername.value'),
usernameError: function() {
var username = this.get("username");
var isDirty = this.get('enteredUsername.isDirty');
if (!username && isDirty) {
return "please enter a username";
}
}.property('username', 'enteredUsername.isDirty')
});
Here is a working demo.

Emberjs promiseArray inside route doesn't return properly

I have a controller for showing item.
Users can put the item in their wish list.
(Item has many users, User has many Items.)
So, when user enter the webpage, I want to show a AddToList or RemoveFromList button to the user based on isAddedToList property.
Below is the code.
User Model:
var User = DS.Model.extend({
username: DS.attr('string'),
email: DS.attr('string'),
avatar: DS.attr('string'),
items: DS.hasMany("item", { async: true })
});
export default User;
ItemModel:
var Item = DS.Model.extend({
name: DS.attr("string"),
priceInCent: DS.attr("number"),
salePriceInCent: DS.attr("number"),
brand: DS.belongsTo("brand"),
itemImages: DS.hasMany("itemImage", { async: true }),
users: DS.hasMany("user", { async: true }),
});
export default Item;
ItemRoute:
var ItemRoute = Ember.Route.extend({
model: function(params) {
var userId = this.get("session").get("userId");
return Ember.RSVP.hash({
item: this.store.find('item', params.item_id),
user: this.store.find('user', userId),
});
},
setupController: function(controller, model) {
controller.set('item', model.item);
controller.set('user', model.user);
}
});
export default ItemRoute;
ItemController:
var ItemController = Ember.Controller.extend({
needs: ["current-user", "application"],
currentUser: Ember.computed.alias("controllers.current-user"),
isAddedToList: function() {
var promiseUsers = this.get("item.users"), user = this.get("user");
return promiseUsers.contains(user);
}.property("item"),
actions: {
addToList: function() {
var item = this.get("item"), user = this.get("user");
item.get("users").pushObject(user);
item.set("addedUserIds", [user.get("id")]);
item.save();
},
removeFromList: function() {
var item = this.get("item"), user = this.get("user");
item.get("users").removeObject(user);
item.set("removedUserIds", [user.get("id")]);
item.save();
}
}
});
export default ItemController;
The problem is when I check the length of promiseUsers with
promiseUsers.get("length")
it always returns 0.
but when I try the same with Chrome console, it returns the length properly.
Do I miss something in the route? How to fix the problem?
The problem is you're using your code synchronously, despite it being an asynchronous property.
The first time you attempt to use an async relationship it will begin resolving the relationship, making a callback to the server is necessary. In your case you try to use the users right away, but they are going to be empty the first time, so you're contains will return false. Since you aren't watching the users' collection it will then update, but the computed property won't update since the computed property was just watching item. This is why when you try it from the console it works, because by the time you attempt to use it in the console it's finished resolving the async collection of users.
isAddedToList: function() {
var promiseUsers = this.get("item.users"), user = this.get("user");
return promiseUsers.contains(user);
}.property("user", 'item.users.[]')

ember.js sending actions to controllers from views

I've created a typeahead view and i'm trying to send an action to the current controller to set a property. Here is my typeahead view
App.Typeahead = Ember.TextField.extend({
dataset_name: undefined, //The string used to identify the dataset. Used by typeahead.js to cache intelligently.
dataset_limit: 5, //The max number of suggestions from the dataset to display for a given query. Defaults to 5.
dataset_template: undefined, //The template used to render suggestions. Can be a string or a precompiled template. If not provided, suggestions will render as their value contained in a <p> element (i.e. <p>value</p>).
dataset_engine: undefined, //The template engine used to compile/render template if it is a string. Any engine can use used as long as it adheres to the expected API. Required if template is a string.
dataset_local: undefined, //An array of datums.
dataset_prefetch: undefined, //Can be a URL to a JSON file containing an array of datums or, if more configurability is needed, a prefetch options object.
dataset_remote: undefined, //Can be a URL to fetch suggestions from when the data provided by local and prefetch is insufficient or, if more configurability is needed, a remote options object.
ctrl_action: undefined,
didInsertElement: function () {
this._super();
var self = this;
Ember.run.schedule('actions', this, function () {
self.$().typeahead({
name: self.get('dataset_name'),
limit: self.get('dataset_limit'),
template: self.get('dataset_template'),
engine: self.get('dataset_engine'),
local: self.get('dataset_local'),
prefetch: self.get('dataset_prefetch'),
remote: self.get('dataset_remote')
}).on('typeahead:selected', function (ev, datum) {
self.selected(datum);
});
});
},
willDestroyElement: function () {
this._super();
this.$().typeahead('destroy');
},
selected: function(datum) {
this.get('controller').send(this.get('ctrl_action'), datum);
}
});
Here's an implementation
App.CompanyTA = App.Typeahead.extend({
dataset_limit: 10,
dataset_engine: Hogan,
dataset_template: '<p><strong>{{value}}</strong> - {{year}}</p>',
dataset_prefetch: '../js/stubs/post_1960.json',
ctrl_action: 'setCompanyDatum',
selected: function (datum) {
this._super(datum);
this.set('value', datum.value);
}
});
and here's my controller
App.PeopleNewController = Ember.ObjectController.extend({
//content: Ember.Object.create(),
firstName: '',
lastName: '',
city: '',
state: '',
ta_datum: undefined,
actions: {
doneEditing: function () {
var firstName = this.get('firstName');
if (!firstName.trim()) { return; }
var lastName = this.get('lastName');
if (!lastName.trim()) { return; }
var city = this.get('city');
if (!city.trim()) { return; }
var state = this.get('state');
if (!state.trim()) { return; }
var test = this.get('ta_datum');
// Create the new person model
var person = this.store.createRecord('person', {
firstName: firstName,
lastName: lastName,
city: city,
state: state
});
// Clear the fields
this.set('firstName', '');
this.set('lastName', '');
this.set('city', '');
this.set('state', '');
// Save the new model
person.save();
},
setCompanyDatum: function(datum) {
this.set('ta_datum', datum);
}
}
});
I'm expecting the setCompanyDatum controller action to be called, but it's not. Everything else is working as expected. The App.Typeahead.selected method is being called with the right action name, but it doesn't actually call the action method.
the controller inside your App.Typeahead points to the instance of the App.Typeahead, not the controller from the route where you are creating the view.
You should just be using sendAction
http://emberjs.jsbin.com/EduDitE/2/edit
{{view App.Typeahead}}
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
},
actions:{
externalAction:function(item){
console.log('helllllo' + item);
}
}
});
App.Typeahead = Ember.TextField.extend({
internalAction: 'externalAction',
didInsertElement: function () {
this.sendAction('internalAction', " I'm a sent action");
this._super();
}
});

Computed Property not firing

I have a computed property cookieToggle that I'm using in a LoginController. The basic idea is that it would observe the username and rememberMe fields and set or clear the username cookie as appropriate. Unfortunately when I update either of the dependant fields it never calls the cookieToggle function (as observed by the lack of the console message that every call should produce). My main question is: why not? My secondary question is: is this a reasonable use of Ember's computed property?
App.LoginController = Ember.ObjectController.extend({
CLIENT_ID: 1,
username: null,
password: null,
rememberMe: false,
cookieToggle: function() {
var rememberMe = this.get('rememberMe');
var username = this.get('username');
console.log("cookie toggle");
if (rememberMe) {
$.cookie('auth_username', username);
} else {
$.removeCookie('auth_username');
}
return rememberMe;
}.property('rememberMe','username'),
init: function() {
this._super();
if ($.cookie('auth_username')) {
this.set('username', $.cookie('auth_username'));
this.set('rememberMe', true);
}
},
loginUser: function() {
var router = this.get('target');
var data = this.getProperties('username', 'password', 'rememberMe');
var user = this.get('model');
$.post('/api/oauth/user_credentials', { username: data.username, password: data.password, client_id: this.get('CLIENT_ID') }, function(results) {
// App.AuthManager.authenticate(results.api_key.access_token, results.api_key.user_id);
console.log(results);
$.cookie('auth_user', results.user.id);
router.transitionTo('users/login');
});
}
});
A computed property is not the right decision in this case. You want to use an Observer. You even use this verb yourself, right? :-) Just change your declaration from property to observes :
cookieToggle: function() {
var rememberMe = this.get('rememberMe');
var username = this.get('username');
console.log("cookie toggle");
if (rememberMe) {
$.cookie('auth_username', username);
} else {
$.removeCookie('auth_username');
}
}.observes('rememberMe','username') // will fire every time when one of those properties change