Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I share data between Ember components?

My Ember app has a route that contains 2 different components and one controller with an index.hbs template.

Here's what it looks like:

enter image description here

1) A user can select multiple filters from the dropdowns of the Filter Component

2) The DataGrid is a separate component from the filter

3) A user can select multiple rows from the DataGrid by checking boxes

4) Create Custom Report button fires "sendAction" to the route's controller

This data is not model-specific... it's just temporary data that is required before I can make a custom report.

Ember best practices are "Data Down / Actions Up", and from what I read, you shouldn't be trying to access a component from a controller.

The problem, though, is that the createCustomReport method in the controller needs to have access to all of the filters that were selected in the filter-component along with all of the rows that were checked in the grid-component.

My first instinct is to set the properties on the component itself - have it maintain its own state - then get a reference to the component from the controller in order to get its state before passing it off to the report function.

But apparently that is a no-no.


Here's my Current Solution:

Each time I select a filter, there is a sendAction that bubbles up to the controller from the component and sets a custom property on the controller.

Also, each time I select a checkbox from the grid, another sendAction goes to the component, then bubbles up to the controller and sets a custom property on the controller for selected grid rows.

Then, when I click "createCustomReport" the method that fires in the controller has access to the properties that I set earlier - because they are all on the controller now.

So it looks something like this:

import Ember from 'ember';

export default Ember.Controller.extend({

    myFirstFilter: undefined,
    mySecondFilter: undefined,

    actions: {
        createCustomReport() {
            // do something with all those component properties you've been setting
        },

        // These are triggered by the sendAction on the respective component
        firstFilterMethod(myProperty1) {                
            this.set('myFirstFilter', myProperty1.name);
        },

        secondFilterMethod(myProperty2) {               
            this.set('mySecondFilter', myProperty2.name);
        },

        ... etc...


    }
});

Here's My Problem With This

I'm not directly accessing the components from the controller, but by using the "Actions Up" principle, I'm setting properties on the controller that are view specific.

Coming from a Sencha ExtJS background where controllers have references to their views, I find this very weird.

By not getting references to components, I'm supposed to be decoupling my controller from its views... but since all the properties I'm setting would normally be on the view, the controller ends up being even more coupled to the view than it would be if I were to just get a reference to the component.

Is this considered "best practice" in Ember or is there a better way for me to get the data of all these separate components in order to fire off the createCustomReport method?

like image 826
PhillipKregg Avatar asked Jul 06 '16 21:07

PhillipKregg


People also ask

How do you pass a model to a component in Ember?

To set the component up to receive parameters this way, you need to set the positionalParams attribute in your component class. import Component from '@ember/component'; export default Component. extend({}).

Is Ember a library or framework?

Ember. js is a productive, battle-tested JavaScript framework for building modern web applications. It includes everything you need to build rich UIs that work on any device.

Is Ember component based?

Ember knows which subclass powers a component based on its filename. For example, if you have a component called blog-post , you would create a file at app/components/blog-post. js . If your component was called audio-player-controls , the file name would be at app/components/audio-player-controls.

What is controller in Ember?

What is a Controller? A Controller is routable object which receives a single property from the Route – model – which is the return value of the Route's model() method. The model is passed from the Route to the Controller by default using the setupController() function.


1 Answers

Alright, I think I've managed to solve my own problem and come around to the Ember way of doing things.

I found 2 different solutions, each of which have their merits. Plus I've created 2 small Ember Twiddle mini tutorials on how to solve state propagation and sharing component data.

Both solutions are fully compliant with the Ember 2.6 way of doing things: No Controllers Needed.

The first one is using an Ember Service.


I built a simple movie list that can be viewed here: https://ember-twiddle.com/c91e98cd255a556311417ac603ab0315

By following the comments inside the files and looking over the Ember Twiddle above, all your questions should be answered on how to implement this.

Since a Service is a singleton, I can inject it into my components and into my route and its sole purpose will be to maintain the data of its associated component.

Here is what the component looks like:

import Ember from 'ember';

export default Ember.Component.extend({
  movieService: Ember.inject.service('movie-displayer-service'),
  currentSelectedMovie: '',

  didInsertElement: function() {
    // When the component is inserted into the DOM tree, use the model to set
    // the 'currentSelectedMovie' property.
    this.set('currentSelectedMovie', this.get('model').currentSelectedMovie);   
  },

  actions: {
    selectMovie: function(movie) {
      // Instead of saving state in the component itself, let's
      // save it in a service that can be consumed anywhere
      // in the application.

     this.get('movieService').setupCurrentSelectedMovie(movie);

     // When the movie changes, we can override the 'currentSelectedMovie' property
     // that is being populated with the 
     this.set('currentSelectedMovie', movie);   

    }
  }
});

Here is what the Service looks like:

import Ember from 'ember';

export default Ember.Service.extend({
  currentSelectedMovie: undefined,

  setupCurrentSelectedMovie: function(movie) {
   this.set('currentSelectedMovie', movie); 
  },

  showSelectedMovie: function() {
    if (this.get('currentSelectedMovie')) {
        alert("The current selected movie of the movie-displayer component is:  \n" + this.get('currentSelectedMovie'));
    } else {
        alert('Please Select a Movie First');
    }
  }
});

Here is the Component's handlebars file:

<div class="movie-list-container">
    <h4 class="movie-list-title">Movie List</h4>

  <ul>
    {{#each model.movies as |movie|}}

        {{!--   'eq' is a helper function that I made
                    to compare two values.  You can check it out in
              the 'helpers' folder.
      --}}
        <li class="{{if (eq movie currentSelectedMovie) "selected" "not-selected"}}" {{action 'selectMovie' movie}}>{{movie}}</li>
    {{/each}}
  </ul>

</div>

And here is what the route looks like:

import Ember from 'ember';

export default Ember.Route.extend({
  movieService: Ember.inject.service('movie-displayer-service'),

  model: function() {
    return {
        currentSelectedMovie: this.get('movieService').currentSelectedMovie,

      movies: ['Captain America: Civil War', 'Guardians of the Galaxy', 'Ant Man']
    }
  },

  actions: {
    showServiceState: function() {
        this.get('movieService').showSelectedMovie();
    }
  }
});

Pros of Service solution:

Being a singleton, I can access the data of this component anywhere in the application.

Cons of Service solution:

I have to inject this into every file that I want to use it in - thereby creating dependencies as I go. The other solution is to use an Ember Initializer class that will automatically inject it into Routes, Controllers, or Components upon app startup. Of course, this means that it would go into every single instance of what it is injected into which could be overkill.


The second solution sends state from the component to the router without a service


The second Ember Twiddle is a simple restaurant list that shows how to propagate state without the need of a service:

https://ember-twiddle.com/dffc679fb96434ba6698161ba7617d15

Here is the component's handlebars file:

<div class="restaurant-list-container">
  <ul>
    {{#each model as |restaurant|}}
      <li class="{{if (eq currentlySelectedRestaurant restaurant ) 'selected' 'not-selected' }}" {{action 'selectRestaurant' restaurant}}>{{restaurant}}</li>
    {{/each}}
  </ul>

</div>

Here is the Route file:

import Ember from 'ember';

export default Ember.Route.extend({  
  // Properties Here
    currentlySelectedRestaurant: 'Please Select a Restaurant',

  model: function() {
    return ['Taco Bell', 'McDonalds', 'Dennys']
  },

  actions: {
    setupRestaurantState : function(restaurant) {
        this.set('currentlySelectedRestaurant', restaurant);
    },

    getComponentState: function() {
     alert(this.get('currentlySelectedRestaurant'));
    }
  }
});

And here is the Component file:

import Ember from 'ember';

export default Ember.Component.extend({

  currentlySelectedRestaurant: undefined,

  actions: {
    selectRestaurant: function(restaurant) {

      // The 'sendAction' method is where the magic happens.
      // A method called 'stateSetter' references a function
      // that lives either on the controller or the route.
      // This was setup when the component was instantiated in the
      // fancy-restaurants.hbs file.
      this.sendAction('stateSetter', restaurant);
      this.set('currentlySelectedRestaurant', restaurant);
    }
  }
});

Notice that the Route contains an undefined state property: 'currentlySelectedRestaurant'.

This could easily be an object with multiple properties or an array.

You could also have a generic name like "componentState" and store whatever you choose to send up from any component: options checked on a filtered list or selected items from a grid for instance.

Pros of not using a Service:

It's easier to do. Just use sendAction() in your component to bubble up to the router. And there are no extra files created or any dependencies.

Cons of not using a Service

Since the model data flows down from the route level, you won't be able to access the state if you change routes.


Each solution is viable so I will leave it up to you to find what works best.

Also, I'm not marking this as the answer just yet because someone else may have a better solution and it would be nice to get some feedback on this.

like image 167
PhillipKregg Avatar answered Jan 01 '23 23:01

PhillipKregg