Nir Cohen
Nir Cohen

Reputation: 75

EmberJS + EmberData: how to listen to model load from server

Environment:

Ember.VERSION = 1.7.1

DS.VERSION = 1.0.0-beta.14.1

Problem: I'm trying to run some code when the view is finally rendered on initial application load, the problem is the model is not yet loaded because the client fetches it from the server.

so the flow (I guess) that happens is something like this:

  1. Ember renders the view, requests the model
  2. Model returns a promise but view is rendered anyhow
  3. After AJAX is finished model is updated and view is re rendered

I want to run some code (bootstrap init of tooltips) after step 3 is completed, but I dont really sure how to.

just to point out, after the model is fetched from server when I leave and return to the same route everything is works fine (because the model returns from the store and not from server)

what I tried so far:

How can catch event after template is rendered at EmberJS?

App.ApartmentsView = Ember.View.extend({
   didInsertElement: function () {
       $('[data-toggle="tooltip"]').tooltip();
   }
});

this didnt work, because at that point the model length is 0

listening to model change:

App.ApartmentsRoute = Ember.Route.extend({
   model: function () {
       return this.store.filter('apartment').then(function (model) {
           console.log(model);
           return model;
       });
   }
});

Again, at this point the model.content.length = 0 I guess at this point the store just creates the model and only at a later point updates it.

I've also tried listening to the arrayContentDidChange property in the controller, but at this point the model is not rendered yet, I can use a timeout but I think its a bad solution.

UPDATE1: here is the template:

<script type="text/x-handlebars" data-template-name="apartments" id="apartments">
.
.
{{#each }}
   <tr>
      <td>{{rating}}</td>
      <td>{{price}}</td>
      <td>{{street}} {{building}} {{city}}</td>
      <td class="text-right hidden-print">
      <button type="button" class="btn btn-default" aria-label="Left Align" data-toggle="tooltip" data-placement="top" title="Navigate" {{ action 'navigate' }}>
         <span class="glyphicon glyphicon-transfer" aria-hidden="true"></span>
      </button>
      <button type="button" class="btn btn-default" aria-label="Left Align" data-toggle="tooltip" data-placement="top" title="Center Apartment" {{ action 'centerApartment' }}>
         <span class="glyphicon glyphicon-screenshot" aria-hidden="true"></span>
      </button>
      <button type="button" class="btn btn-default" aria-label="Left Align" data-toggle="tooltip" data-placement="top" title="NOT WORKING" {{ action 'setTime' }}>
         <span class="glyphicon glyphicon-time" aria-hidden="true"></span>
      </button>
      <button type="button" class="btn btn-default" aria-label="Left Align" data-toggle="tooltip" data-placement="top" title="NOT WORKING" {{ action 'share' }}>
         <span class="glyphicon glyphicon-share-alt" aria-hidden="true"></span>
      </button>
      {{#link-to 'apartment' this classNames="btn btn-default"}}<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>{{/link-to}}
      <div class="btn btn-default" data-toggle="tooltip" data-placement="top" title="Delete Apartment" {{action 'deleteApartment' }}>X</div>
      </td>
    </tr>
{{/each}}
.
.

</script>

Any suggestions?

Upvotes: 1

Views: 445

Answers (3)

code-jaff
code-jaff

Reputation: 9330

As @torazaburo already mentioned, the issue was in the filter function. Simply

model : function(){
  return this.store.filter('apartment', {}, function(item){
    return true;
  });
}

would work for you. Obviously, this is analogous to this.store.find('apartment')

Actually this is a simple hack (including empty object as query) to get data from server before filter.

But, to make sure that tooltips get initialized on newly added elements, much better approach is mentioned by @torazaburo

DEMO

Upvotes: 0

user663031
user663031

Reputation:

Your description of the lifecycle is incorrect. It should be:

  1. Ember enters the route.

  2. Route's model hook is invoked.

  3. The returned value from the model hook is resolved.

  4. Proceed with the transition, including rendering the template and calling its didInsertElement hook.

The problem is that your model hook is returning a promise from the filter, but at that point, as you've found, there are no records in the store yet, and thus nothing to filter. What I am missing from your question is when and how you do expect the apartments to be loaded from the server. filter will not do that for you. For our purposes, let's assume you have a routine, returning a promise, called getApartments, that ensures that the necessary models are loaded from the server. Perhaps it is something as simple as

getApartments: function() {
    return this.store.find('apartments');
}

Then replace your model hook with something like this:

model: function () {
  return this.getApartments() .
    then(function(model) {
      return this.store.filter('apartment', filterfunc);
    })
  ;
}

Now Ember will wait until the promise from getApartments is resolved, then use the filtered result as its model, and then render, so the element you want should be available for manipulation in your didInsertElement hook.

However, this will still fail when additional apartments are added to the store later, because didInsertElement has already run when the element was added. To solve, this problem, I suggest moving your logic for rendering individual apartments into a component. This is probably a good way to factor your code anyway.

components/show-apartment/template.hbs

<tr>
  <td>{{content.rating}}</td>
  ...
  <button type="button" class="btn btn-default" aria-label="Left Align" data-toggle="tooltip" data-placement="top" title="Navigate" {{ action 'navigate' content}}>
  ...
</tr>

Then, in the component's JS, initialize the tooltip in didInsertElement:

components/show-apartment/component.js

export default Ember.Component.extend({

  didInsertElement: function() {
    $(this).find('[data-toggle="tooltip"]').tooltip();
  }

});

Then in your top-level template, use the new component to display each apartment:

{{#each apartment in model}}
  {{show-apartment content=apartment navigate=navigate}}
{{/each}}

Now, each time a new apartment comes into the store, the filtered model will reflect it, the {{#each}} will generate a new entry, a new component will be created and rendered, and the didInsertElement logic on that component be run to initialize that particular tooltip.

By the way, your model hook is calling store.filter with no filter function. What are you trying to accomplish with that?

Using isLoaded is a hack and a code smell, not necessary in the land of promises.

Upvotes: 2

Sushant
Sushant

Reputation: 1414

If you want something to render only after its model is loaded then I guess you can check for isLoaded key in the template. Something like this

template.hbs

{{#if model.isLoaded}}
     All the HTML stuff that will be shown only if model is loaded.
{{/if}}

Upvotes: 0

Related Questions