Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to communicate between controllers in Ember.js

Tags:

ember.js

I'd like to create a page where on the left I have fixed view (some filters) which are applied to results on the right.

For example on the left are filters to filter movies by genre, title, year of creation.... On the right are different charts and tables, which update based on the filters selected.

So I was thinking of having a fixed view on the left, then on the right an outlet which would change based on the route.

Is this the correct way? If it is, I can't figure out how to communicate filter changes to controller on the right.

This JSFiddle shows a similar setup : http://jsfiddle.net/DEHcK/2/

In the ContentRoute -> setupController I get an instance of NavigationController, but then I can't figure out how to get changes to someValue.

In the example above, how can ContentController get the changes to someValue in NavigationController?

Is this the correct way to implement the app I described in ember?

JavaScript:


    window.App = Ember.Application.create();

    App.Router.map(function () {
        this.route('content');
    });

    App.ContentRoute = Ember.Route.extend({
        setupController: function (controller) {
            controller.set('navigationController', this.controllerFor('navigation'));
        }
    });

    App.ContentController = Ember.ObjectController.extend({
        navigationController: null,
        observerOfSomeValue: function () {
            this.set('observedValue', this.get('navigationController.someValue'));
        }.observes('navigationController', 'navigationController.someValue'),
        observedValue: null
    });

    App.IndexRoute = Ember.Route.extend({
        redirect: function () {
            this.transitionTo('content');
        }
    });

    App.NavigationView = Ember.View.extend({
        init: function () {
            this._super();
            this.set('controller', this.get('parentView.controller').controllerFor('Navigation'));
        },
        templateName: "navigation"
    });

    App.NavigationController = Ember.ObjectController.extend({
        someValue: 'xx',
        observedValue: null,
        observerOfSomeValue: function () {
            this.set('observedValue', this.someValue);
        }.observes('someValue')
    });

HTML:

<script type="text/x-handlebars" data-template-name="application" >
    <div>
        {{view App.NavigationView}}
    </div>
    <div>
        {{ outlet }}
    </div>
</script>

<script type="text/x-handlebars" data-template-name="navigation" >
    Change {{view Ember.TextField valueBinding="controller.someValue"}}
    <div>observed value in same view: {{controller.observedValue}}</div>
</script>

<script type="text/x-handlebars" data-template-name="content" >
    <div style="margin-top: 2em">
        observed value in another view: {{observedValue}}
    </div>
</script>
like image 745
mr_eko Avatar asked Feb 13 '13 20:02

mr_eko


1 Answers

I've gone ahead and created you a JSFiddle with a basic implementation of what you're after. I think it's worth running through it all though so that you can get a grip of Ember.

Router

We're just configuring our IndexRoute at this point where we store all of our songs for the {{outlet}}.

App.IndexRoute = Ember.Route.extend({
    setupController: function(controller) {
        controller.set('content', [
            Ember.Object.create({ title: 'Stairway to Heaven', genre: 'Metal' }),
            Ember.Object.create({ title: 'Ratts of the Capital', genre: 'Post Rock' }),
            Ember.Object.create({ title: 'Wonderwall', genre: 'Brit Pop' }),
            Ember.Object.create({ title: 'Last Flowers', genre: 'Indie Rock' })
        ]);
    }
});

There's a good chance this code will be replaced with an AJAX call of some sort to your back-end Ruby/PHP. For the time being though, we'll give the IndexRoute the responsibility of setting up the controller (hence setupController). This responsibility could just as well lie on the controller itself, and is probably a good idea to abstract out the AJAX call since you'll have many similar AJAX calls.

You may also decide to use Ember's DataStore, which will change the implementation of the controller again.

Index Controller

Next we're going to setup our IndexController (which is our SongsController really), we want this controller to have two responsibilities:

  1. To store the songs (and perhaps to fetch the songs as well from the back-end);
  2. To filter songs based on filters that are passed into it.

For this we create a computed property to filter out the content, since we don't want to actually manipulate the special content array directly.

App.IndexController = Ember.ArrayController.extend({
    content: [],
    excludeGenres: [],

    filteredContent: function() {
        var excludeTheseGenres = this.get('excludeGenres').mapProperty('genre');
        return this.get('content').filter(function(model) {
            return Boolean(jQuery.inArray(model.get('genre'), excludeTheseGenres) === -1);
        });
    }.property('excludeGenres.length', 'content.length')
});

The excludeGenres will take an array of genre objects. For example, if "Post Rock" is contained within excludeGenres, then we won't show any "Post Rock" related songs, but if it isn't present, then we we'll show them! The IndexController doesn't have the responsibility of maintaining this array, but it does have the responsibility of filtering its content for when this array is updated.

Genres Controller

Perhaps the most confusing controller in our little application, because it doesn't actually have any content of its own, but rather depends on the IndexController's content.

App.GenresController = Ember.ObjectController.extend({
    needs: ['index'],

    genres: function() {
        return this.get('controllers.index').mapProperty('genre').uniq();        
    }.property('controllers.index.length'),

    toggle: function(genreName) {
        var indexController     = this.get('controllers.index'),
            genres              = indexController.get('excludeGenres'),
            genre               = indexController.findProperty('genre', genreName);

        if (genres.findProperty('genre', genreName)) {
            genres.removeObject(genre);
            return;
        }

        genres.pushObject(genre);
    }
});

Genre controller's responsibilities can be defined as so:

  1. To monitor the content array of the IndexController and fetch the unique genre names when its length changes;
  2. To populate the excludeGenres array when a user clicks on a genre in the list to include/exclude it.

To have the toggle method called, we need to specify an action in our genre view to invoke it when clicked: <a {{action "toggle" genre}}>{{genre}}</a>. Now whenever a user clicks on a genre, the toggle method will be invoked, and the genre name passed as the first argument.

Once we're in the toggle method, it will determine whether the genre is already being excluded. If it is being excluded, then it will be removed, and vice-versa. Once we've added/removed the genre, the filteredContent computed property will be fired again, and the index view will be updated seamlessly.

The reason the GenresController doesn't actually have its own content is that it would seem silly to manage two content arrays when the two have a relationship. Since genres can be determined from the songs present in the application, the controller can gather the information from that list, and just pull out the information it requires -- in our case, genres.

This way, if a song is added/removed, then the genre list can be kept in sync.

Conclusion

I think the answer to your original question is, however, that in order for controllers to be communicating with one another, you need to be specifying what it needs (with needs).

like image 58
Wildhoney Avatar answered Nov 06 '22 23:11

Wildhoney