Question

I am working on a habit tracking app. It is written in Ember and backed by CouchDB.

On the main screen I want to show a list of habits and the last time an event was recorded for each. Here are what my models look like:

App.Habit = DS.Model.extend( {
  type: DS.attr('string', { defaultValue: 'habit' } ),
  name: DS.attr( 'string' ),
  color: DS.attr( 'string' ),
  events: DS.hasMany( 'event', { async: true, defaultValue: [] } ),
  style: function() {
    return 'background-color: %@'.fmt( this.get( 'color' ) )
  }.property( 'color' ),
  lastTime: function() {
    return this
      .get( 'events' )
      .then( function( events ) {
        var times = events.map( function( e ) {
          return e.get( 'time' )
        } )
        return times.length == 0 ? undefined : times.sort()[0]
      } )
  }.property( 'events' ),
} )

App.Event = DS.Model.extend( {
  type: DS.attr('string', { defaultValue: 'event' } ),
  time: DS.attr( 'date' ),
  habit: DS.belongsTo( 'habit' )
} )

Note that the lastTime property of Habit, returns a promise. When I did it without the promise the array was length 0. My handlebars template looks like:

<script type="text/x-handlebars" id="habits">
  <div id="habits">                                                                                                                                                                                        
    {{#each}}                                                                                                                                                                                              
      <div class="row" {{action 'createEvent' id}}>
        <div class="col-xs-1 color" {{bind-attr style=style}}></div>
        <div class="col-xs-8">{{name}}</div>
        <div class="col-xs-3" data-role="timer">{{format-time-numeric lastTime}}</div>
      </div>                                                                                                                                                                                               
    {{/each}}                                                                                                                                                                                              
  </div>
</script>

The format-time-numeric helper is just:

Ember.Handlebars.registerBoundHelper( 'format-time-numeric', function( time ) {
  return moment( time ).format( 'YYYY/M/D @ H:mm' )
} )

The time getting passed to moment is a promise and is rendering as a bunch of NaNs. If I pass a promise back from format-time, it renders as [object].

Was it helpful?

Solution

Your could use reduceComputed property for this case:

  maxTime: Ember.reduceComputed('events.@each.time', {
    initialValue: -Infinity,
    addedItem: function(accValue, event) { 
      return Math.max(accValue, event.get('time'));
    },
    removedItem: function(accValue, event) { 
      if (event.get('time') < accValue ) {
        return accValue;
      }
    }
  })

OTHER TIPS

Don't return a promise from the

lastTime

Instead, write it as:

lastTime: function() {
    var times =  this.get('events').map(function(event) {
                      return event.get('time');
                  });      
    return times.length == 0 ? undefined : times.sort()[0]
  } )
}.observes('events').property(),

Your model's logic is flawed. It is observing changes in 'events' but then also getting 'events' via a Promise. If 'events' has already been resolved then you do not require the Promise. If 'events' has not then you cannot observe it.

Models are generally not the place for Promises - your view does not understand how to display a Promise. Promises are generally resolved in Routes and the results stored in Models. I would rearchitect to move it to the Route or if this is not possible have some action or observer set the property in the model.

Option 1: If events are already in memory:

See Hrishi answer

Option 2: If events are not in memory and a Promise is required:

Something like this may work:

lastTime: null,

And then in your controller have an action similar to this (or use some other way of trigger similar code):

lastTimeAction: function() {
   var self = this;
   this.get( 'events' ).then( function( events ) {
      var times = events.map( function( e ) {
         return e.get( 'time' );
      });
      self.set('lastTime', times.length == 0 ? undefined : times.sort()[0]);
  });
}

Quoting from your comment:

I thought Ember was supposed to wait for the promises to complete and send the results to the template.

That is... a tricky question. To put it simply, no, it won't wait. The only time that I can think of that id does wait is in the model hooks. If you return a promise from beforeModel, model, or afterModel, the router will pause before continuing. However, that doesn't apply to controllers or templates, and doesn't apply to your situation when you're getting the async relationship of a model.

So, one potential solution for you is to pause the router until the events relationship is loaded. Something like this in your router:

model: function(params) {
    return this.store.find('habit', params.habit_id).then(function(habit) {
        // Return another promise, to continue pausing the router
        return habit.get('lastTime');
    });
}

That will pause the router (and not render your template) until your lastTime property fully computes.

Another option (which I recommend) is to just have the lastTime property inserted into the template asynchronously. If you change the way the property is computed, you can have the template update when the events are fetched:

lastTime: function() {
    var events = this.get('events');

    if (events.length <= 0) {
        return 0; // Or some other default
    }

    return events.mapBy('time').sort()[0];
}.property('events.@each.time')

So your template will initially show 0, but then update with the real time when events is ready.

The third option, as Michael Benin mentioned, is to set the events relationship on your model to be a synchronous one. You could compute your property immediately, but you'd have to include all of the events models in your JSON whenever you fetched a habit model. (Not recommended. Embrace the asynchronous.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top