Is it possible to make a route's model a computed / tracked property?

992 views Asked by At

I'm trying to have a route's model hook return some array that is constantly updated via a polling mechanism using Ember later. The route file looks like this:

export default class IndexRoute extends Route {
  recent: [],

  init() {
    ...
    this.getRecent();
  }

  getRecent() {
    // poll data / fetch latest
    this.recent.push(newStuff);
    
    later(this, this.getRecent, 2000);
  }

  model() {
    return this.recent;
  }
}
 

Then in my controller, I wanted to create a @computed / @tracked property based on the route's model:

export default class IndexController extends Controller {
  // @tracked model; // this also didn't work
  @computed('model.@each') // this doesn't work
  get computedModel() {
    console.log('computedModel'); // prints only once, when the model hook is first run
    return this.model;
  }
}

I thought what this SO post suggested would have worked but it didn't :(

I saw this post but this was for Ember 1.13 so not exactly a modern solution.

Similarly this post had outdated content too.

Is what I'm trying to do possible? Alternatively I was thinking of moving the data into the Controller and making a computed property of a Controller variable instead. Taking all suggestions!

1

There are 1 answers

3
mistahenry On BEST ANSWER

The fundamental problem with your current approach is that you are not using Ember Array specific functions. There is often some magic that happens when you create arrays in Ember that automatically creates them as Ember arrays (at least this was the case when using the old style syntax with .create and when prototype extensions were allowed). You can always explicitly create an Ember array with:

import { A } from '@ember/array';

export default class ApplicationRoute extends Route{
  recent = A([]);
}

Anyway, when you just use this.recent.push(newStuff);, this native array prototype is not instrumented in a way that Ember's tracking/observer system can know that the a new value has been added to this.recent and subsequently trigger a rerender. This applies to both tracked properties as well as the traditional observer system pre-Octane.

Instead, when interacting with arrays that are being displayed in templates (ie arrays that need to be observed), you must use special Ember.Array specific functions pushObject like this.recent.pushObject(newStuff).

getRecent() {
  // poll data / fetch latest
  this.recent.pushObject(this.current);
  this.current = this.current + 1;
    
  later(this, this.getRecent, 2000);
}

If you wanted to go full on tracked property style, you can avoid using the Ember array, but you must force a recompute by reassigning the array to the tracked property. Here's an example in a component

import Component from '@glimmer/component';
import { tracked} from '@glimmer/tracking';
import { later } from '@ember/runloop';
export default class extends Component {
  @tracked
  trackedArray = [];

  current = 0;
    constructor(){
    super(...arguments);
    this.doPoll();
  }
    
    doPoll() {
    // essentially pushing but via a reassign with the new element
    // you could also `pushObject` here if that feels better
    this.trackedArray = [...this.trackedArray, this.current];
    this.current = this.current + 1;
    
    later(this, this.doPoll, 2000);
  }
}

Here is a Ember Twiddle that shows both approaches in action.

PS. The model property of a route is only quasi-dynamic. During the transition, the return value of the model hook is automatically assigned to the route's currentModel property. This value is then passed into setupController as the second parameter and automatically assigned to the controller's model property if no setupController is defined or if the setupController invokes super of the base implementation. When modelFor is called for a particular route, the currentModel property is returned and model is not reinvoked. This is all to say that having the poll in the model function itself would not automatically update the controller's model property. Everything works fine in your example since the model reference never changes (you're just mutating the array).