How to pass a #tracked object from an Ember route model hook - ember.js

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).

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 update nested state in Ember Octane

So my situation is as follows:
I got a component with a couple of input fields that represent a contact and are filled with data from a service:
#service('contact-data') contact;
Each field stands for one property that is accessed via
{{contact.properties.foo}}
I have the properties saved as a JS object to easily filter out empty fields when using them and I tracked it with #tracked like so:
export default class MyService extends Service {
#tracked properties = {id: 0, foo: "", bar : "abc", };
#action updateProperty(name, value) {
this.properties[name] = value;
}
}
However, the properties do not re-render properly in the component and the textfields do not get updated.
I'd appreciate any help with this! Thanks!
Any time you have a bunch of nested state like that which needs to be tracked, just tracking the top-level object won't cause updates to the internals of that object to propagate out. You need to track the internal properties, or you need to reset the whole object you're tracking.
You have basically two rough options for dealing with updates to those internal properties:
If the object has a well-known shape, extract it into a utility class which uses #tracked on the fields, and instantiate the utility class when you create the service. Then updates to those fields will update.
If the object is really being used like a hash map, then you have two variant options:
Use https://github.com/pzuraq/tracked-built-ins, if you don't need IE11 support
Do a "pure functional update", where you do something like this.properties = { ...this.properties, foo: newValue };
Of these, (1) is pretty much always going to be the cheapest and have the best performance. Doing (2.1) will be a little more expensive, because it requires the use of a Proxy, but not enough that you would normally notice. Doing (2.2) will end up triggering a re-render for every property in the properties used anywhere in the app, even if it didn't change.
In the case you've described, it appears the fields are well known, which means you should reach for that class. The solution might look something like this:
import Service from '#ember/service';
import { action } from '#ember/object';
import { tracked } from '#glimmer/tracking';
class TheProperties {
#tracked id;
#tracked foo;
#tracked bar;
}
export default class MyService extends Service {
properties = new TheProperties();
#action updateProperty(name, value) {
this.properties[name] = value;
}
}
Note that #tracked installs getters and setters in place of plain class properties, so if you need to use this for a JSON payload somewhere or similar, you'll also want to implement toJSON on the utility class:
class TheProperties {
#tracked id;
#tracked foo;
#tracked bar;
toJSON() {
let { id, foo, bar } = this;
return { id, foo, bar };
}
}
There's another add-on that does basically the same thing for Array and Objects as tracked-built-ins.
It's a proxy that basically notifies the root that an update has occurred somewhere. The advantage against tracked-built-ins is that the nesting depth is not limited as it's common for JSON to have deep nesting.
The drawbacks are similar to tracked-built-ins in terms of performance. Use it sparingly and try not to use it in tables with hundreds/thousands of rows as re-rendering is going to be not performant.

record not added to model when i call save() on a new record in Ember-Data

Using Ember-Data 0.13-59 & Ember 1.0.0 RC 6 (from starter kit)
Problem: upon save() to a new record made from App.Userstat.createRecord({ ... }) the server gets the POST and successfully returns an id but the new record is not available in the Userstat model.
To better understand example: this is a quiz app(for multiple choice questions). Each question has several choices and when a user selects a choice, their choice to the corresponding question is stored in a Model, App.Userstat.
At each question, the app needs to know whether the user has already answered this question or if it's new.
I use a computed property as a setter and getter. The setter is called when a user selects a choice (the choice's value is passed to computed property). First it checks if a record exists for the user's current question. If it doesn't exist it will create a new record. If it does exist, it should only issue a PUT request with updated content.
Code Updated(July 8, 11AM)
App.UserstatsController = Ember.ArrayController.extend();
App.QuestionController = Ember.ObjectController.extend({
needs: "userstats",
chosen = function(key, value) {
// getter
if(value === undefined) {
// something goes here
// setter
} else {
// the question.id is used to compare for an existing record in Userstat mdoel
var questionId = this.get('id');
var questionModel = this.get('model');
// does any Userstat record belong to the current question??
var stats = this.get('controllers.Userstats');
var stat = stats.get('model').findProperty('question.id', questionId);
// if no record exists for stat, means user has not answered this question yet...
if(!stat) {
newStat = App.Userstat.createRecord({
"question" : questionModel,
"choice" : value // value passed to the computed property
)}
newStat.save(); // I've tried this
// newStat.get('store').commit(); // and this
return value;
// if there is a record(stat) then we will just update the user's choice
} else {
stat.set('choice', value);
stat.get('store').commit();
return value;
}
}.property('controllers.Userstats')
No matter how many times I set chosen it always sends a POST (as opposed to an update only sending a PUT request) because it never adds the record to the model the first time.
To demonstrate further, in the setter part of the computed property, when I put this code:
var stats = this.get('controllers.Userstats')
console.log stats
the Userstats controller shows all previously existing records, but not newly submitted records!
How come the new record isn't available after I save() or commit() it???
Thanks :)
EDIT
maybe it has something to do with me adding a record to the singular model App.Userstat and then when I look for it, I'm searching using the UserstatsController which is an Array controller???
I don't know if it's a typo, but the computed property is defined the wrong way and should be like this:
App.QuestionController = Ember.ObjectController.extend({
needs: 'userstats',
choice: 'controllers.userstats.choice',
chosen: function(key, value) {
...
}.property('choice')
...
});
Inside the property() you should also define properties that trigger the computed property if they change. This way if choice changes the chosen cp will be triggered.
Please let me know if it helps.

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.

Adding item to filtered result from ember-data

I have a DS.Store which uses the DS.RESTAdapter and a ChatMessage object defined as such:
App.ChatMessage = DS.Model.extend({
contents: DS.attr('string'),
roomId: DS.attr('string')
});
Note that a chat message exists in a room (not shown for simplicity), so in my chat messages controller (which extends Ember.ArrayController) I only want to load messages for the room the user is currently in:
loadMessages: function(){
var room_id = App.getPath("current_room.id");
this.set("content", App.store.find(App.ChatMessage, {room_id: room_id});
}
This sets the content to a DS.AdapterPopulatedModelArray and my view happily displays all the returned chat messages in an {{#each}} block.
Now it comes to adding a new message, I have the following in the same controller:
postMessage: function(contents) {
var room_id = App.getPath("current_room.id");
App.store.createRecord(App.ChatMessage, {
contents: contents,
room_id: room_id
});
App.store.commit();
}
This initiates an ajax request to save the message on the server, all good so far, but it doesn't update the view. This pretty much makes sense as it's a filtered result and if I remove the room_id filter on App.store.find then it updates as expected.
Trying this.pushObject(message) with the message record returned from App.store.createRecord raises an error.
How do I manually add the item to the results? There doesn't seem to be a way as far as I can tell as both DS.AdapterPopulatedModelArray and DS.FilteredModelArray are immutable.
so couple of thoughts:
(reference: https://github.com/emberjs/data/issues/190)
how to listen for new records in the datastore
a normal Model.find()/findQuery() will return you an AdapterPopulatedModelArray, but that array will stand on its own... it wont know that anything new has been loaded into the database
a Model.find() with no params (or store.findAll()) will return you ALL records a FilteredModelArray, and ember-data will "register" it into a list, and any new records loaded into the database will be added to this array.
calling Model.filter(func) will give you back a FilteredModelArray, which is also registered with the store... and any new records in the store will cause ember-data to "updateModelArrays", meaning it will call your filter function with the new record, and if you return true, then it will stick it into your existing array.
SO WHAT I ENDED UP DOING: was immediately after creating the store, I call store.findAll(), which gives me back an array of all models for a type... and I attach that to the store... then anywhere else in the code, I can addArrayObservers to those lists.. something like:
App.MyModel = DS.Model.extend()
App.store = DS.Store.create()
App.store.allMyModels = App.store.findAll(App.MyModel)
//some other place in the app... a list controller perhaps
App.store.allMyModels.addArrayObserver({
arrayWillChange: function(arr, start, removeCount, addCount) {}
arrayDidChange: function(arr, start, removeCount, addCount) {}
})
how to push a model into one of those "immutable" arrays:
First to note: all Ember-Data Model instances (records) have a clientId property... which is a unique integer that identifies the model in the datastore cache whether or not it has a real server-id yet (example: right after doing a Model.createRecord).
so the AdapterPopulatedModelArray itself has a "content" property... which is an array of these clientId's... and when you iterate over the AdapterPopulatedModelArray, the iterator loops over these clientId's and hands you back the full model instances (records) that map to each clientId.
SO WHAT I HAVE DONE
(this doesn't mean it's "right"!) is to watch those findAll arrays, and push new clientId's into the content property of the AdapterPopulatedModelArray... SOMETHING LIKE:
arrayDidChange:function(arr, start, removeCount, addCount){
if (addCount == 0) {return;} //only care about adds right now... not removes...
arr.slice(start, start+addCount).forEach(function(item) {
//push clientId of this item into AdapterPopulatedModelArray content list
self.getPath('list.content').pushObject(item.get('clientId'));
});
}
what I can say is: "its working for me" :) will it break on the next ember-data update? totally possible
For those still struggling with this, you can get yourself a dynamic DS.FilteredArray instead of a static DS.AdapterPopulatedRecordArray by using the store.filter method. It takes 3 parameters: type, query and finally a filter callback.
loadMessages: function() {
var self = this,
room_id = App.getPath('current_room.id');
this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
})
// set content only after promise has resolved
.then(function (messages) {
self.set('content', messages);
});
}
You could also do this in the model hook without the extra clutter, because the model hook will accept a promise directly:
model: function() {
var self = this,
room_id = App.getPath("current_room.id");
return this.store.filter(App.ChatMessage, {room_id: room_id}, function (msg) {
return msg.get('roomId') === room_id;
});
}
My reading of the source (DS.Store.find) shows that what you'd actually be receiving in this instance is an AdapterPopulatedModelArray. A FilteredModelArray would auto-update as you create records. There are passing tests for this behaviour.
As of ember.data 1.13 store.filter was marked for removal, see the following ember blog post.
The feature was made available as a mixin. The GitHub page contains the following note
We recommend that you refactor away from using this addon. Below is a short guide for the three filter use scenarios and how to best refactor each.
Why? Simply put, it's far more performant (and not a memory leak) for you to manage filtering yourself via a specialized computed property tailored specifically for your needs