How can I rerender when object (a dictionary) changes? - ember.js

I have the following .hbs
<button {{action "addToObject"}}>
Add to Object
</button>
<br />
{{#each this.sortedDictionary as |item|}}
{{item.name}}<br />
{{/each}}
and the corresponding .js code for it:
import Component from '#ember/component';
import EmberObject, {set, computed, observer} from '#ember/object';
export default Component.extend(
{
myDictionary: null,
counter: 0,
init()
{
this._super( ...arguments );
console.log( "test-comp init" );
this.myDictionary = {};
set( this.myDictionary, "hello", "42" );
},
sortedDictionary: computed( 'myDictionary', function()
{
let sortedDictionary = [];
if ( this.myDictionary )
{
const keys = Object.keys( this.myDictionary );
keys.sort();
console.log( "test-comp keys", keys );
sortedDictionary = keys.reduce( ( sorted, itemName ) =>
{
const value =
{
name: itemName,
...this.myDictionary[ itemName ]
}
sorted.push( value );
return sorted;
}, []);
}
console.log( "test-comp sortedDictionary", sortedDictionary );
return sortedDictionary;
}),
actions:
{
addToObject()
{
set( this.myDictionary, `hello${this.counter}`, "42" );
this.counter++;
console.log( "test-comp addToObject", this.myDictionary );
}
}
});
In my mind, when I press the Add to Object button, my .hbs should rerender. However, it does not.
I am assuming this is because sortedDictionary is not recomputed and cannot detect that myDictionary has changed.
What is the correct solution to this problem?

In your ember computed function, only dependency is myDictionary. So computed will fire only theres a reference change to myDictionary object. You can tweak this by calling dictionary event change or adding another dependency which changes when firing the action (in your example, counter). If you are using counter as a dependency for the computed property, you have to use set function to trigger dependency change.
In addition to that, hbs doesn't need to have this to get myDictionary since it uses related component as its context

Related

Emberjs Getting array property returns undefined

I want to define a reactive array property in a component so that every time the array is updated, it auto-updates necessary HTML content.
I thought this was going to be straightforward, but it didn't work as I expected. Every time I retrieve the property via this.get('array'), it returns undefined.
// components/test-component.js
export default Ember.Component.extend({
_array: [],
array: Ember.computed('_array',{
get(key) { return this.get('_array'); },
set(key,value) { this.set('_array', value); }
}),
isEmpty: Ember.computed('array', function() {
// Here, this.get('array') returns undefined. Why?
return this.get('array').length;
}),
actions: {
addNew() {
this.get('array').push(Date.now());
}
},
init() {
this._super(...arguments);
this.set('array', [1,2,3]);
},
});
I also noticed that in the init method, if I retrieve the array property right after setting it, it also returns undefined. Why is this happening?
Here is a twiddle. It is supposed to iterate the array, and show a list of all items, but it is currently crashing because it returns undefined.
The problem you are currently seeing is that you need to add a return in the set method. Also you should use the Ember.computed('array.[]') syntax where you listen for changes to the array itself.
But you would be better off using an Ember array so that you don't need a second array:
import Ember from 'ember';
export default Ember.Component.extend({
array: undefined,
init() {
this._super(...arguments);
this.set('array', Ember.A());
},
actions: {
addNew() {
this.get('array').addObject(Date.now());
}
}
});
and
<ul>
{{#each array as |item|}}
<li>{{item}}</li>
{{else}}
<li>No items</li>
{{/each}}
</ul>
<button onclick={{action 'addNew'}}>Add New</button>

How to get `check-box` value from template to route?

In my Ember page, I don't have the controller, but I have the route and hbs files. When a user clicks on the checkbox, how can I retrieve the value (true/false) in my route action method?
Here is my template :
<div class="agreementContent">
{{rdc-checkbox label="*I agree to the <a href='#' (action 'goToTerms')> Terms & Conditions </a>" action="acceptTerms" checked=terms}}
</div>
My route file:
import Ember from 'ember';
export default Ember.Route.extend({
model(params){
console.log('got the model', params );
return this.store.peekRecord('card-list', params.id );
},
actions:{
goToTerms(){
console.log("confirm clicked");
},
acceptTerms( event ){
const value = this.get('terms');
console.log("accept Terms clicked", value );//undefined
}
}
});
UPDATE
Tried like :
actions:{
goToTerms(){
console.log("confirm clicked");
},
acceptTerms( event ){
console.log("accept Terms clicked", this.modelFor(this.routeName) );// gives the model value as null
}
}

Dynamically render components in Ember for ember-i18n

First of all, ember's component helper doesn't help in this case. That would only help if I knew how many components I needed to render, and in what order.
I need to be able to render components based on a string like:
{{user}} has made a bid of {{bid}}, where:
{{user}} and {{bid}} will be replaced with components.
the given string is unknown, with an unknown number of dynamic sections representing components (The components would be passed in with the given string).
If these dynamic sections were helpers, this would be easy - but helpers just don't cut it for certain items in my game.
Ideally I could do something like this:
{{translated-content
content='{{user}} has made a bid of {{bid}}'
user=(component 'user-ui')
bid=(component 'bid-ui') }}
Is this possible with ember?
With some help, I've come up with the following component which works with ember-i18n, and ember 1.11 or later.
It could likely be optimised further, but it works nice and fast the way it is.
Create a new component
ember g component t-t
template.hbs
{{#each parts as |part|}}
{{#if part.isComponent}}
{{component part.content}}
{{else}}
{{part.content}}
{{/if}}
{{/each}}
component.js
import Ember from 'ember';
const { $ } = Ember;
export default Ember.Component.extend({
tagName: 'span',
updateComponents: Ember.on('didReceiveAttrs',function(opts){
let newAttrs = opts.newAttrs;
let components = {};
$.each(newAttrs,(key,val)=>{
if( key !== 't' && typeof val === 'object' ){
let keys = Object.keys(val);
if(keys.length && keys[0].indexOf('COMPONENT_')>=0){
components[key] = val;
}
}
});
this.set('_components',components);
}),
parts: Ember.computed('_components','t','i18n.locale',function(){
let attrs = [];
let components = this.get('_components');
let componentKeys = Object.keys(components);
$.each(this.attrs,(key,val)=>{
if( key !== 't'){
if( componentKeys.indexOf(key)<0 ){
attrs[key] = val;
} else {
attrs[key] = `{{${key}}}`;
}
}
});
let content = this.get('i18n').t(this.get('t'),attrs).toString();
content = content.replace(/\{\{(\w+?)\}\}/g,(fullMatch)=>{
return `{{split}}${fullMatch}{{split}}`;
});
let parts = content.split('{{split}}');
parts.forEach((val,i)=>{
let isComponent;
let key = val.replace(/\{\{(\w+?)\}\}/g,(fullMatch,key)=>{
isComponent = true;
return key;
});
if(isComponent && components[key]){
parts[i] = {
isComponent: true,
content: components[key]
};
} else {
parts[i] = {
content: Ember.String.htmlSafe(val)
};
}
});
return parts;
}),
}).reopenClass({
positionalParams: ['t']
});
Usage
{{t-t
'your-ember-i18n-path'
key1='Normal Content (example)'
key2=(component 'your-component') }}

Emberjs - modifying array

I have some issue with Ember that I don't really understand. I have an array like this :
var device.images = [
0: {
url: "url",
key: "key"
},
1: "url2",
2: "url3",
3: "url4"
];'
And in my controller, inside a map I try to modify every item from the array doing this :
device.images[idx] = image;
image is a new object that look like the first item of my array. But I always get the assertion failed :
Assertion Failed: You must use Ember.set() to access this property (of [object Object]...
How can I fix that ? because that should normally works no ?
[edit] Here is the complete code :
loadGallery: function( device ) {
var that = this;
var images = device.images.map( function( item, idx ) {
return new Ember.RSVP.Promise(function( resolve ) {
if ( item.url ) {
resolve( item );
}
else {
resolve( that.imageStore.readImage( that, item ) );
}
})
.then( function( image ) {
device.images.set( idx, image );
that.set( 'selectedItem.images', device.images );
return image;
});
});
return Ember.RSVP.Promise.all( images );
},
[Edit: first attempt was wrong]
It seems you should be able to use item[index] = value construct. Example usage:
App.IndexRoute = Ember.Route.extend({
resolveImage: function (that, item) {
return {
url: item,
key: "resolved"
};
},
model: function() {
var device = {
images: [
{url: "a", key: "existing"},
"b",
"c"
]
};
var that = this;
var images = device.images.map(function(item, idx) {
return new Ember.RSVP.Promise(function(resolve) {
if (item.url) {
resolve(item);
}
else {
resolve(that.resolveImage(that, item));
}
})
.then(function(image) {
device.images[idx] = image;
return image;
});
});
return Ember.RSVP.hash({
model: Ember.RSVP.Promise.all(images),
device: device
});
},
setupController: function (c, m) {
c.set("model", m.model);
c.set("device", m.device);
}
});
<script type="text/x-handlebars" data-template-name="index">
<h2>From model</h2>
<ul>
{{#each item in model}}
<li>{{item.url}} - {{item.key}}</li>
{{/each}}
</ul>
<h2>From device</h2>
<ul>
{{#each item in device.images}}
<li>{{item.url}} - {{item.key}}</li>
{{/each}}
</ul>
</script>
JSBin here.
Barring some bug elsewhere in your code, the only remaining solution is that you are using Ember.Data and images isn't really an array but a custom class that merely implements one or several of Ember's array/enumerable mixins. [...] construct is a syntactic sugar that is hardcoded into javascript and not available to library authors to override.
I can't help you there much, as I don't use Ember.Data. You can try using explicit array mutation methods from Ember.MutableArray interface. Eg:
device.images.replace(idx, 1, [image]);
Alternatively, use object.set(key, value) construct (my first suggestion), with caveat that key value must be a string. Eg:
device.images.set(String(idx), image);
If neither of these work, you can just try to build up a new array instead of replacing members of the existing one.

How to override a property so it returns a different value if the property is empty?

I am probably going to use an ArrayController + itemController setup to solve this, but maybe this is better off inside the model layer.
I want to override the property of an object to return another value if the property is empty. I think this i best described in code (jsfiddle: http://jsfiddle.net/ahx_/Tqw4C/2/).
App = Ember.Application.create()
App.Teacher = Ember.Object.extend()
App.Pupil = Ember.Object.extend({
// TODO Add a property ('answer') that returns teacher.answer unless this.answer is defined
// Pseudo-code:
// answer: function(key, value) {
// if(Ember.isEmpty(this.answer)) {
// return this.get('teacher.answer')
// } else {
// return this.answer
// }
// }.property('answer')
})
App.IndexController = Ember.Controller.extend({
init: function() {
this._super()
teacher = App.Teacher.create({answer: 'correct'})
this.set('pupil1', App.Pupil.create({ teacher: teacher, answer: 'incorrect' }))
this.set('pupil2', App.Pupil.create({ teacher: teacher }))
}
})
You need to add another property as .property() cannot refer to itself.
Object:
App.Pupil = Ember.Object.extend({
answerToShow: function(){
return this.get('answer') ? this.get('answer') : this.get('teacher.answer')
}.property('answer')
})
Template:
<script type="text/x-handlebars" data-template-name="index">
Pupil1 ('incorrect'): {{pupil1.answerToShow}}
<br>
Pupil2 ('correct'): {{pupil2.answerToShow}}
</script>
Demo: http://jsfiddle.net/Tqw4C/5/