Hi,
Ember is awesome, thanks for all of your hard work on this! :-)
I'm an Ember newbie, so forgive me if I've missed something obvious (I've spent time Googling this issue and still can't find a solution) but it seems to me that Ember computed properties aren't working as documented/intended on array properties like length.
I'm trying to build my own queue:
// app/custom-objects/processing-queue-item.js
import Ember from 'ember';
export default Ember.Object.extend({
payload: null,
extraContext: null,
processingState: 'pending', // pending, succeeded, failed
processingOutcome: null, // null for pending, result for succeeded, error for failed
toString() {
return `{ProcessingQueueItem: processingState=${this.get('processingState')}, processingOutcome=${this.get('processingOutcome')}, extraContext=${this.get('extraContext')}, payload=${this.get('payload')}}`;
}
});
// app/custom-objects/processing-queue.js
import Ember from 'ember';
import ProcessingQueueItem from './processing-queue-item';
export default Ember.Object.extend(Ember.Enumerable, {
queueName: null,
init() {
this.set('items', []);
this.get('items');
this.get('items.length');
this.get('length'); // Force observation
},
/*
* Public API
*/
enqueue(payload, extraContext = null) {
let itemToEnqueue = ProcessingQueueItem.create({ payload: payload, extraContext: extraContext });
this.get('items').pushObject(itemToEnqueue);
this.scheduleProcessing();
return itemToEnqueue;
},
toString() {
return `{ProcessingQueue: queueName=${this.get('queueName')}, length=${this.get('length')}}`;
},
/*
* Internal API
*/
scheduleProcessing() {
Ember.run(() => {
this.maybeProcessAnItem();
});
},
maybeProcessAnItem() {
console.log(`maybe process an item ${this}`);
},
/*
* Ember.Enumerable mixin
*/
length: Ember.computed('items.length', function() {
return this.get('items.length');
}),
nextObject(index, previousObject, context) {
return this.get('items').nextObject(index, previousObject, context);
}
});
This class is incomplete, but I want to start displaying queue contents in a template to help with debugging but I can't get that to work. Here are my controller and template:
// app/controllers/dashboard.js
import Ember from 'ember';
import ProcessingQueue from '../custom-objects/processing-queue';
export default Ember.Controller.extend({
init() {
this._super(...arguments);
this.set('processingQueue', ProcessingQueue.create({ queueName: 'DashboardQueue' }));
this.get('processingQueue');
this.get('processingQueue.length');
this.get('queueLength');
},
queueLength: Ember.computed('processingQueue.length', function() {
return this.get('processingQueue.length');
}),
});
// app/templates/dashboard.hbs
<h1>Dashboard</h1>
<h2>Queue Length: '{{queueLength}}'</h2>
{{#each processingQueue as |queueItem|}}
<p>{{queueItem.payload}}</p>
{{/each}}
{{outlet}}
The problem is, in the <h2>Queue Length: '{{queueLength}}'</h2>, the queue length is always undefined until I add items to the queue. But this is not true, the queue has an empty array and a length of 0. Using $E from the dashboard controller from EmberInspector I can see that $E.get('processingQueue.length') and $E.get('queueLength') are both undefined.
What's strange is that as soon as I add items to the queue, the queue length becomes defined, 1, 2, 3, ... and keeps up and syncs the template as I add queue items. So the first $E.get('processingQueue').enqueue('foo') automagically updates the template to show a queue length of '0', then '1' and so on.
Why is it undefined though before I've enqueued any items? I tried adding gets all over the place according to Unconsumed Computed Properties Do No Trigger Observers but that doesn't seem to help.
Any ideas? It's entirely possible that I misunderstand something about computed properties here, but I don't understand what and why ... I've tried volatile(), [], @each and all that and I can't get that to make a difference either. Something is not right ...
Any help would be hugely appreciated and I'd be willing to add to the Wiki, write a blog post and maybe release my queue as open source as a thank you. :-)
Thanks! And thanks again for making Ember so awesome!
but it seems to me that Ember computed properties aren't working as documented/intended on array properties like length.
They should. Something seems strange.
Do you mind reproducing this in https://ember-twiddle.com that way it is easy for us to see whats-up, and debug the issue.
Thanks for responding so quickly. Happy to - setting up the twiddle right now!
OK, I shared the twiddle.
You can see that when the queue starts off empty, the length computed property is undefined. However as I add items, the length property goes to 1, 2, 3 as expected. But when I clear the queue, the length goes back to 0.
I have tried all sorts of tricks like: put an initial item in the queue then remove it within init, try to get the computed property on the run loop, etc., and nothing seems to work. The length always starts out as undefined.
Also, any idea why the {{#each processingQueue ... doesn't work? ProcessingQueue is an Ember.enumerable so I would expect this to just work right?
Thanks again! If there's anything else I can do to help, please let me know and I'd be happy to do it. :-)
@joelpresence {{#each processingQueue.items ...}} for the iteration, and {{processingQueue.items.length}} for the counting will be just fine.
The problem is kinda strange, but the work-around is easy. So its not the end of the world, although improving this is something we should do. Because the result is extremely WAT.
toStringI'm going to explain whats going on here (so i don't forget what I discovered, and can explore fixin):
processing-queue.js init invokes this.set('items', [])processing-queue.js toString has been overridden to be: toString() {
return `{ProcessingQueue: queueName=${this.get('queueName')}, length=${this.get('length')}}`;
}
this.set the following development assertion in ember toStrings's thistoString is invoked, which reads length which then reads the property we are in the process of set'ing items.items.length right during when items (or more accurately just right before) is set, the value of items is undefined which ends up being the cached value.init() the object is not considered to be fully initialized so we short circuit and don't track/broadcast changes, which results in the cache not being evicted.This is really silly, but I'll have to think about what the right solution is (I can think of several).
some other notes as I read:
run you have in the schedule is not required: https://guides.emberjs.com/v2.9.0/applications/run-loop/ can share more on when/and when not to use itinit you often forget this._super(...arguments) calls, without that the object wont initialize properly, and once we move to ES6 classes you will get a syntax errora smaller reproduction: https://ember-twiddle.com/e37fb6e7d7f5da27622e44d1a319e7bb?fileTreeShown=false&openFiles=controllers.application.js%2C
I'll likely write a test tomorrow
A less convoluted example that demonstrates the same issue is:
Ember.Object.extend({
init() {
this._super(...arguments);
this.get('length'); // forces length to cache
this.set('items', []); // changes the data backing length, but since we are during initialization `length` is not informed of the change and thus is cache is never purged.
},
length: Ember.computed.readOnly('items.length')
})
Although broadcasting changes during init should most likely continue to not happen, I do believe this.set('length', []) it may be reasonable to explore the local cache of this.length to be evicted.
Or maybe the fix should be making sure, we don't unexpectedly invoke user code (That may have side-affects) via a development assertion.
Hi @stefanpenner thanks so much for your detailed replies!
When you say:
in your init you often forget this._super(...arguments) calls, without that the object wont initialize properly, and once we move to ES6 classes you will get a syntax error
What do you mean? I thought that according to Initializing Instances that this._super(...args); was only necessary if I am extending a framework class like Component. I am invoking the super init in my application controller, but not in my ProcessingQueue since the latter is not a framework class in my mind. Or do I need to invoke this._super(...args); in init even for a simple subclass of Ember.Object? Please confirm. I'm happy to do so if need be. :-)
You're right, removing the custom toString() fixes it, thank you so very much! :-) But is there a way to make a custom toString that shows the length that would not cause this problem? The reason I added the custom toString is that for some of my other custom objects that extend Ember.Object I was having issues logging them to the console - they would print out something about unknown mixin and Ember.inspect would also print them as unknown mixin. So I had to roll my own toString.
Either way, I'll remove the toString for now and keep on coding!
Thanks again for all of your careful advice and review of this issue. I am very grateful for your help and for all that you do for Ember! :-)
Best wishes.
OK, here's what I mean happens when I remove the toString on ProcessingQueue:
// when maybeProcessAnItem runs:
maybeProcessAnItem() {
// TODO: actually grab queue items and do some meaningful processing
console.log(`maybe process an item ${this}`);
},
// it logs the following to the console:
processing-queue.js:44 maybe process an item <(unknown mixin):ember461>
So without a custom toString, my ProcessingQueue now logs as <(unknown mixin):ember461>. That's no good for me ... any ideas why this is happening and how I can fix it (without a custom toString)? Even Ember.inspect($E.processingQueue) gives the sam unknown mixin log to the console ... Something is wrong with Ember.inspect maybe?
@stefanpenner, I did run into (another?) issue regarding computed properties and arrays. Using the @each key works fine if properties of the array elements are changed. But adding new elements do not trigger re-calculation of the computed property. According to the current docs for 2.10 this should be the case (?)
@peStoney I believe it should (and is tested) could you provide a reproduction.
@stefanpenner I created this small example:
export default Ember.Controller.extend({
values: null,
init() {
this.set("values", [
Ember.Object.create({ count: 1 }),
Ember.Object.create({ count: 0 }),
Ember.Object.create({ count: 3 })
]);
console.log("Array Count: " + this.get('arrayLength'));
Ember.run.later(() => {
this.get('values').push(Ember.Object.create({ count: 2 }))
console.log("Array Count: " + this.get("arrayLength"));
Ember.run.later(() => {
this.get('values')[0].set('count', 5);
console.log('Array Count: ' + this.get('arrayLength'));
}, 1000);
}, 1000);
},
arrayLength: Ember.computed('[email protected]', function() {
return this.get('values.length');
}),
});
The console output looks like this:
Array Count: 3
Array Count: 3
Array Count: 4
If I did understand the docs right the second line should already output 4 instead of 3 (?)
@peStoney [].push is not observed but [].pushObject is. So for array mutations to be observed you need to use that. For a list of methods that are observed, I believe you can look at: http://emberjs.com/api/classes/Ember.MutableArray.html#method_pushObject
@stefanpenner Awesome! That did the job - I should have digged more into the docs... sorry.
See https://github.com/emberjs/ember.js/issues/14724 - I need to create another object with a custom toString because Ember.inspect just gives the unhelpful 'unknown mixin' ... Any ideas?
Thanks!
@stefanpenner You are the man! Thanks.
@stefanpenner <3 ty
Closing as no longer relevant!
Most helpful comment
@peStoney
[].pushis not observed but[].pushObjectis. So for array mutations to be observed you need to use that. For a list of methods that are observed, I believe you can look at: http://emberjs.com/api/classes/Ember.MutableArray.html#method_pushObject