Bind Ember.Map to a Ember.TextField value - ember.js

How can I bind an Ember Map value to a TextField value. Let's say I have this configuration:
App.AppsController = Em.Controller.extend({
selections: null
});
App.AppsRoute = Ember.Route.extend({
setupControllers: function(controller, model) {
controller.set('selections', Ember.Map.create());
}
});
And in my template:
{{view Ember.TextField valueBinding="bindingToMap"}}
I have tried to valueBinding="controller.selections.somekey" where somekey is a key from my map. However, the value is never bound. Note that initially the map is empty. Can this be the root of the problem?
EDIT:
I have also tried to use the binding with an integer value in the controller and it works. So the problem comes when I bind a more complex data structure such as a Map. I couldn't find anything in the docs explaining how to bind a map.

Caveat: I'm pretty new to Ember myself.
Looking at the implementation of Ember.Map, I don't think you can currently (in 1.0.0-pre2) do this. Ember.Map implements create, get, and set, but is not a normal Ember object. So among other things, "properties" in a map aren't really properties, and there's no observables support. The handlebars implementation relies heavily on observable support, so I think what you're doing won't work.
Someone correct me if I'm wrong, but this is what I'm looking at in the 1.0.0-pre2 code:
var Map = Ember.Map = function() {
this.keys = Ember.OrderedSet.create();
this.values = {};
};
/**
#method create
#static
*/
Map.create = function() {
return new Map();
};
Map.prototype = {
/**
Retrieve the value associated with a given key.
#method get
#param {anything} key
#return {anything} the value associated with the key, or undefined
*/
get: function(key) {
var values = this.values,
guid = guidFor(key);
return values[guid];
},
...
Point being, it implements its own get (and create) rather than extending Ember.Object...so no observable support. Though I might have missed it if it reopen's later or something.
EDIT:
Also, not really what you asked, but if you're building an interface that relies on the existence of some key, you should really probably be defining your own model class that has these keys as properties. You can still set them to null if they're "not set". If you also need the ability to set arbitrary keys, make one of your properties an Ember.Map and call it otherProperties or something, and put them in there. But if your view depends on a known key, it should be a defined property.

Related

Emberjs: Use args as tracked property

I am doing a store query in the controller and then passing the result down to a child component.
// Controller
#tracked comment;
#action
async fetchComment() {
const comment = await this.store.query('note', {
filter: {
listing: this.listing.id,
},
});
if (comment && comment.length > 0) {
this.comment = comment.firstObject;
}
}
// Template
<MyComponent #comment={{this.comment}} />
I want to use the arg to populate a tracked property in the child component.
// Component
#tracked comment = this.args.comment;
I found this does not work but works well as a getter. However with a getter I am unable to set this to null when deleting that record.
I have also tried to set the tracked property in the Constructor but that didnt work either.
I suspect this has something to do with passing in a promise or store object in the args because this works fine with static data.
why your code does not work
this code can not work:
#tracked comment = this.args.comment;
this is because comment on the controller is initially undefined but will later bet set to comment.firstObject when the network request is done and the await in your fetchComment function returns.
Generally everythings on args basically always behaves like its #tracked (while more accurate you would describe it as getters). So this usually will just update fine. But the assignment #tracked comment = this.args.comment; only happens once when you create the component, so you no longer depend on updates on args.
why you can not set this.args.comment to null
If you use a getter or directly always use this.args.comment you can not change this reference. This is because this.args is always readonly. you can change objects on this.args.something, but you never can change the reference or primitive value on this.args.
Sidenote: this is only true if the component was called with <AngleBracket /> syntax. For the older {{curly-component}} syntax this is not true. So this does not depend on the component itself but how the component gets called.
you could notify the controller to remove the reference
one good thing to do is to pass down a deleteComment action to the component that basically does something like this.comment = null on the controller. then you use this.args.comment directly or by a getter and you can call this.args.deleteComment() to set comment on the controller to null, which will update anything that uses this.args.comment or a getter that returns this.args.comment.
this is essentially because in your architecture the controller owns the data (because it loads it). so the controller is also responsible to delete it.
if you use ember-data you can check isDeleted
if its a ember-data model (which it probably is if you call this.store) then it has a isDeleted property. you can use this to check if the record is deleted, since ember-data records dont disappear if they get deleted. which is exactly because of problems like this.
how you use another property to shadow a argument
you could do something like this to shadow an argument in your component:
#tracked commentIsDeleted = false;
get comment() {
return this.commentIsDeleted
? null
: this.args.comment;
}
this way at first this.comment will work like a normal getter, but you can shadow delete it by setting this.commentIsDeleted = true;. From that on this.comment will be null.

How to pass a #tracked object from an Ember route model hook

My question is two-fold:
Where is the best place to put some kind of polling logic - in the route file right?
How do I pass this constantly updating value from the Route to some child component? Labeling some variable as "#tracked" and then passing the tracked variable via the model hook?
Let's say I have something like this:
routes/index.js
export default class IndexRoute extends Route {
#tracked
recent: {
A: 0,
...
},
constructor() {
super(...arguments);
this.getRecent();
}
getRecent() {
// poll data / fetch latest
const {A, ...partialObject} = this.recent;
this.recent = { ...partialObject, A: <some new value fetched>};;
later(this, this.getRecent, 2000);
}
model() {
return this.recent;
}
}
application.hbs
<p>constantly updating "this.recent": {{ this.model.A }} </p>
I thought if I use the model hook like this, it would be tracked and therefore auto-update but that was not the case. I have this sample Ember Twiddle that emulates what I'm trying to do. I tried to force a re-compute by reassigning the entire variable but it didn't work.
This question is a deeper dive from my initial question here.
You are returning a reference to object stored in this.recent in your model hook. But the getRecent method does not change that object but overrides this.recent. After the first execution of getRecent method the model of the route and this.recent aren't the same object anymore. The model of the route, which you can access through this.modelFor(this.routeName) is the initial value and this.recent is the new value.
You want to mutate the object returned from model hook instead.
The object given in your example has a fixed schema. This allows you to mark the property A as tracked:
recent: {
#tracked A: 0,
...
}
As currently you return the value of this.recent in your model hook. But instead of overwriting it in getRecent method, you are only changing the value of it's property A:
getRecent() {
this.recent.A = <some new value fetched>;
later(this, this.getRecent, 2000);
}
If you don't know the schema of the object returned in model hook or if you are dealing with an array, it's a little bit more complicated. You wouldn't have a property to decorate with #tracked. I would recommend to use the tracked-built-ins package in that case.
For arrays you can also fallback to legacy MutableArray from #ember/array/mutable package. But you must make sure in that case that you use it's custom methods to manipulate the array (e.g. pushObject instead of push).

Ember.js: How do I determine if a property has really changed in an Observer?

Let's say I have Articles which are backed by Sources. Each article has one source.
Sources have associated HTML which will be rendered on screen.
I want this HTML to be rendered only if the source changed.
App.ArticleView = Ember.View.extend({
didInsertElement: function() {
this.addObserver('controller.source.id', function() {
console.log(arguments);
renderHTML();
});
});
});
This behaves exactly as stated in the addObserver documentation, "Note that the observers are triggered any time the value is set, regardless of whether it has actually changed. Your observer should be prepared to handle that."
If setting a controller.model of Article A with source 1 is followed by setting a controller.model of Article B with source 1, the observer will call the method but I want to prevent renderHTML() from happening.
The documentation mentions "Observer Methods" which I'm not sure how to put to use in this case. Its signature (function(sender, key, value, rev) { };) looks exactly like what I need, but in my tests the arguments to the observer method are always 0: (current view), 1: "controller.source.id".
How can I get the previous value of controller.source.id, so as to determine whether to renderHTML() or not?
Ember.set won't set the value if it's the same as the current value, so your observer won't fire unless the value changes.
Here's an example:
http://emberjs.jsbin.com/jiyesuzi/2/edit
And here's the code in Ember.set that does it (https://github.com/emberjs/ember.js/blob/master/packages_es6/ember-metal/lib/property_set.js#L73)
// only trigger a change if the value has changed
if (value !== currentValue) {
Ember.propertyWillChange(obj, keyName);
if (MANDATORY_SETTER) {
if ((currentValue === undefined && !(keyName in obj)) || !obj.propertyIsEnumerable(keyName)) {
Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter
} else {
meta.values[keyName] = value;
}
} else {
obj[keyName] = value;
}
Ember.propertyDidChange(obj, keyName);
}
Unfortunately for you're case, Ember considers changing a portion of the chain as changing the item, aka if controller.source changes, then the observer will fire. You'll need to track your id differently to avoid you're observer from firing. You can create a different observer that always sets the current id on a local property, and then it won't fire the update when the chain is broken.
Inside the controller
currentArticleId: null,
watchArticle: function(){
this.set('currentArticleId', this.get('article.id'));
}.observes('article.id')
And then you would watch controller.currentArticleId

Ember.computed not functioning as expected when defined within a method

I am attempting to use Ember.computed to setup a computed property from within one of my view's methods. I tried to use the syntax shown in this fiddle, but as you can see, it does not seem to actually do what I was hoping. Any pointers in the right direction would be much appreciated.
http://jsfiddle.net/skane/H5ma5/1/
this.set('myComputed', Ember.computed(function() {return "funky"}).property());
Steve
This won't work this way, since Ember has to do perform some of its magic. I had a look at the source of Ember and found this:
// define a computed property
Ember.defineProperty(contact, 'fullName', Ember.computed(function() {
return this.firstName+' '+this.lastName;
}).property('firstName', 'lastName'));
#method defineProperty
#for Ember
#param {Object} obj the object to define this property on. This may be a prototype.
#param {String} keyName the name of the property
#param {Ember.Descriptor} [desc] an instance of `Ember.Descriptor` (typically a
computed property) or an ES5 descriptor.
You must provide this or `data` but not both.
#param {anything} [data] something other than a descriptor, that will
become the explicit value of this property.
So the following should work in your case:
Ember.defineProperty(this, 'myComputed', Ember.computed(function() {
return "funky";
}).property());

Force a controller to always act as a proxy to a model in Ember

I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.