bittersweetryan
bittersweetryan

Reputation: 3443

Ember.js filtering data from a dynamic route

I'm trying to filter data based on a search string using a dynamic route. When using the transitionToRoute function from a controller the model data from the route is returned to the viewproperly, however when navigating directly to the url or refreshing the page all the the forEach call is not being executed because the length of the data in the model is 0.

I have a feeling this is because the data is being loaded asynchronously, but I'm not sure how to delay the forEach loop and the rendering of the view until the find's promise is resolved and the forEach loop has completed.

Here is the model function of my router:

model : function( params ){
    var lists = App.List.find( ), //gets all the lists
        query = params.query, //query string from url
        re = new RegExp( query, 'i' );

    this.set( 'query', query );

    return lists.forEach( function( list ){
        var cards = list.get( 'cards' ).forEach( function( card ){

            //the view has a class bound to the hide property of each card
            card.set( 'hide',
                ( query.length ) ? !( re.test( card.get( 'description' ) ) ) : false
            );
        } );

        return list;
    });
}

When a user hits the application with a url with a query string of #/search/red I want only the cards that have "red" in them returned.

Upvotes: 4

Views: 2364

Answers (3)

mavilein
mavilein

Reputation: 11668

I just rediscovered this question and this is my try to give an answer. As i already mentioned in the comment, this are my basic ideas:

with Computed Properties:

  • Don't do the filtering in the model hook, just return the lists.
  • Set the query on the controller of your Route (named SearchController?)
  • Do the filtering in the controller as computed property

with Observers: (closer to your original code)

  • Don't do the filtering in the model hook, just return the lists.
  • Set the query on the controller of your Route (named SearchController?)
  • Take the logic to hide cards from your model hook and implement it as an observer on the Controller

The best approach would be to use computed properties, but i was not sure how to go about it (the Ember Team states that computed properties often lead to better code). Therefore here is a rough sketch of the code with the Observer approach. This should work out ok as a start:

Route:

model : function( params ){
    this.set( 'query', params.query );
    return App.List.find(); //gets all the lists
},
setupController : function(controller, model) {
    this._super(controller, model);
    // setupController is a good location to setup your controller
    controller.set("query", this.get("query"));
}

Controller:

App.SearchController = Ember.ArrayController.extend({
    query : '',
    // this observer will fire:
    // 1.: every time a list object is added or removed from the underlying array
    // 2.: the query changes
    modelAndQueryObserver : function(){
            re = new RegExp( this.get("query"), 'i' );
        return this.get("model").forEach( function( list ){
            var cards = list.get( 'cards' ).forEach( function( card ){
            //the view has a class bound to the hide property of each card
            card.set( 'hide',
                    ( query.length ) ? !( re.test( card.get( 'description' ) ) ) : false
                );
            } );

            return list;
        });
    }.observes("model.@each", "query")
});

Upvotes: 7

Jeremy Green
Jeremy Green

Reputation: 8574

You're right about the problem being due to the asynchronous call. When you enter the route directly the object that is returned from your find call is a promise, and not a live list. You need to wait for the promise to resolve before dealing with it.

Something like this should work:

model : function( params ){
    var lists = App.List.find( ), //gets all the lists
        query = params.query, //query string from url
        re = new RegExp( query, 'i' );

    this.set( 'query', query );

    // 'lists' is a promise, once it has resolved we can deal with it
    lists.then(function(realLists){ // realLists is a real collection of models
      realLists.forEach(function(list){
         list.get('cards').forEach( function( card ){
           //the view has a class bound to the hide property of each card
            card.set( 'hide',
                ( query.length ) ? !( re.test( card.get( 'description' ) ) ) : false
            );
         });
      });
    });

    // return the 'lists 'promise immediately
    // maybe before the 'lists.then' function above has run
    // Ember will resolve the promise for you and set up the controller correctly
    return lists;
}

Note that depending on your loading method (side loading vs additional http call) when you call list.get('cards') above, you may actually get a promise instead of a collection of cards. If that's the case you can use the same technique.

cards = list.get('cards');
cards.then(function(realCards){
  realCards.forEach(...);
});

Also worth noting is that in the latest versions of Ember Data, the method for finding has changed. Instead of App.List.find( ) you'd do this.store.find('list'). (The transition guide has more info about the breaking changes. https://github.com/emberjs/data/blob/master/TRANSITION.md)

Upvotes: 0

seenivasan
seenivasan

Reputation: 186

Here is one implementation where model and content properties of a controller is clearly separated at setupController hook of a corresponding route.

I have list of balls with different colors in a separate file.

balls.js

[
    {"id":1,"color":"darkred"},
    {"id":2,"color":"lightred"},
    {"id":3,"color":"darkgreen"},
    {"id":4,"color":"lightgreen"},
    {"id":5,"color":"darkblue"},
    {"id":6,"color":"lightblue"}
]

App.js

App = Ember.Application.create();

App.Router.map(function() {
    this.resource('balls',{path:"/balls/:color"});
});
App.BallsRoute = Ember.Route.extend({
    model: function(params) {
        return params;  
    },

    serialize:function(model){return {color:model.color}},

    setupController:function(cont,model){
        var balls=Em.A();
        if(!App.Balls)
            App.Balls=$.getJSON("/start/js/balls.js");
        App.Balls.then(function(json){
            var re=new RegExp(model.color)
            balls.setObjects(json);
            var filtered =balls.filter(function(o,i){return re.test(o.color);});
            cont.set('content',filtered);
        });
  }
 });
App.ApplicationController=Em.Controller.extend({
    searches:[{color:"red"},{color:"blue"},{color:"green"}]
    });
App.BallsController=Em.ArrayController.extend({

});

HTML

<script type="text/x-handlebars">
    <h2>Welcome to Ember.js</h2>
    <nav>
        {{#each item in searches}}
            {{#link-to "balls" item}} {{item.color}} {{/link-to}}
        {{/each}}
    </nav>
    {{outlet}}
</script>

<script type="text/x-handlebars" data-template-name="balls">
    <ul>
    {{#each controller}}
      <li>{{color}}</li>
    {{/each}}
    </ul>
</script>

I am not using Ember data, as I am not comfortable with it.

Kindly check this Bin

Upvotes: 0

Related Questions