Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Bestra/8befa3117217a4f93fb3 to your computer and use it in GitHub Desktop.
Save Bestra/8befa3117217a4f93fb3 to your computer and use it in GitHub Desktop.
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['flash-message'],
classNameBindings: ['type'],
type: function() {
return 'flash-message--' + this.get('message.type');
}.property('message.type'),
fadeIn: function() {
this.$().hide().fadeIn(250);
}.on('didInsertElement'),
actions: {
remove: function() {
var self = this;
this.$().fadeOut(function() {
self.sendAction('remove', this.get('message'));
});
}
}
});
<div class="flash-message-content">
{{unbound message.text}}
<div class="flash-message-remove" {{action "remove"}}>&times;</div>
</div>
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['flash-messages'],
layoutName: 'flash-messages',
actions: {
removeMessage: function(message) {
this.get('flash.messages').removeObject(message);
}
}
});
{{#each message in flash.messages}}
{{flash-message remove="removeMessage"
message=message}}
{{/each}}
import Ember from 'ember';
export default Ember.Object.extend({
messages: [],
displayMessage: function(type, message) {
this.get('messages').pushObject({
text: message,
type: type
});
},
displayErrorMessagesFromResponse: function(response) {
for (var key in response.errors) {
if(!response.errors.hasOwnProperty(key)) { continue; }
this.displayMessage('error', this.formatKey(key) + ' ' + response.errors[key].join(', '));
}
},
formatKey: function(key) {
return key.underscore().replace('_', ' ').capitalize();
},
clearMessages: function() {
this.set('messages', []);
}
});
import Flash from 'app/services/flash';
import Ember from 'ember';
export default {
name: 'flashMessages',
initialize: function(container, application) {
container.register('flashMessages:main', Flash);
application.inject('route', 'flash', 'flashMessages:main');
application.inject('controller', 'flash', 'flashMessages:main');
application.inject('component:flashMessages', 'flash', 'flashMessages:main');
Ember.Route.reopen({
enter: function() {
this._super.apply(this, arguments);
var routeName = this.get('routeName');
var target = this.get('router.router.activeTransition.targetName');
if (routeName !== 'loading' && routeName === target) {
this.flash.clearMessages();
}
}
});
}
};

Showing Flash-style Messages in Ember

Flash messages are ubiquitous on the web. Update your credit card info on any given site and you'll see the friendly "Your information has been updated" message when the page reloads. Having a consitent notification area on the page that users can glean status and errors from can also be very convenient from a development standpoint, for better or worse. The flash in a Rails application acts like a global hash that is cleared out after each request. It's a pattern that's not too difficult to mimic in Ember.

We'll need a few pieces in our app in order to display the flash.

  • A place to store the flash
  • A way to clear the flash after transitions
  • A place to display the flash message(s)

Storing application state

Let me come right out and say it. You should have a really good reason to stash any sort of state on window these days, even more so if you're using ES6 modules. It's easy, it's tempting, it'll make future-you want to make a time machine just to go back and stop past-you from doing it. From what I've describe of our flash so far, we'll need access to it from the routes and probably from our templates (which means we'll need it in the controllers).

One common idiom for storing application state is to use a dedicated controller for it. By default Ember's controllers are singletons, and once instantiated they stick around for the lifetime of the application. If we made a FlashController, we could easily get to it from the routes using this.controllerFor('flash'). In our controllers we could use needs to make the FlashController available anywhere we needed to. There are a few tradeoffs for going with the the controller-as-a-service approach. On the plus side, it very visible to other developers. Seeing this.get('controllers.flash.message') in a controller leaves no doubt as to where that message came from. On the downside, if any part of your application code would need access to the flash (maybe a low-level View), the amount of glue code to pass the flash down through your templates can get frustrating. Also, needs: will be deprecated with (along with controllers in general) with the coming of routable components. Already Ember 1.10 has a new service injection api that you can use to add dependencies to arbitrary objects, but for the purposes of this article I'm going to stick with the API as of 1.9.

Another way to manage the state of the flash is through the application's container. I'm not going to talk in detail about the container here. There are two great presentations by Matthew Beale that will give you a good background. Here we're going to register a flash message service into our application container, then we'll inject it where we need it.

A service to hold messages

The flash message service is just a plan old extension of Ember.Object We wanted to be able to display more than one message at a time, hence the array rather than a hash.

// app/services/flash.js

import Ember from 'ember';

export default Ember.Object.extend({
  messages: [],

  displayMessage: function(type, message) {
    this.get('messages').pushObject({
      text: message,
      type: type
    });
  },

  clearMessages: function() {
    this.set('messages', []);
  }
});

We do the registration and injection in an initializer. Notice we didn't define the flash service in the initializer itself. By keeping the service separate from the injection we can easily test it.

// app/initializers/flash.js

import Flash from 'app/services/flash';
export default {
  name: 'flashMessages',
  initialize: function(container, application) {
    container.register('flashMessages:main', Flash);
    application.inject('route', 'flash', 'flashMessages:main');
    application.inject('controller', 'flash', 'flashMessages:main');
    application.inject('component:flashMessages', 'flash', 'flashMessages:main');
  }
};

Calling container.register will by default make the registered class a singleton. Next we inject that singleton into all of our controllers, routes, and into the FlashMessagesComponent (which we'll define shortly). Calling this.flash from any of those instances will get us the flash service.

Resetting flash messages after transitions

Unfortunately the current route api doesn't give us events that we can hook into for the transitions. The didTranstion action could be a little tricky to hook into for every route. For now we'll reopen Ember.Route's enter method, call super, then clear the flash messages when the transition has reached its target route. This part could probably be improved

// app/initializers/flash.js

// add this code to the flash initializer.  remember to import Ember from 'ember'
Ember.Route.reopen({
      enter: function() {
        this._super.apply(this, arguments);

        var routeName = this.get('routeName');
        var target    = this.get('router.router.activeTransition.targetName');

        if (routeName !== 'loading' && routeName === target) {
          this.flash.clearMessages();
        }
      }
    });

Displaying the messages

For showing the flash itself, a simple component in the application template will suffice. You can find the code in this gist Notice that we don't pass the flash into the component since we injected it directly in the initializer.

Services > Globals

Leveraging Ember's container allows you to separate business concepts and behavior from the lifecycle of your application. Often a service can just be a plain old Ember class that's easy to reason about and test on its own. Injecting dependencies into your app can also provide some ergonomic benefits in development.

@lordhumunguz
Copy link

good stuff! I think it's ready.

It's basically the pattern I use as well for flash messages, super useful. Have you tested use cases where the flash is set in one route and you actually want it to show up in a different route? For example, the error occurs on a multi-page form submit, I set the message on the final validation step and push the user back to the beginning of the form.

Also I had a gotcha with service object the other day where the object was re-instantiated with every route transition. This may be fixed with container.register('flashMessages:main', Flash); but I haven't tested it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment