Creating a simple message dialog module with ampersand.js

After my blogpost from last week, I wanted to share a simple tutorial with you. Today, I will be going into some more practical ampersand.js code, and show you how you can use ampersand.js to the maximum to build your own component for dialog messages in a webapp. This component should be instantiated only once, and can be filled in with all the information you want to pass on to the user from anywhere within the code.

The result will look a little bit like the dialogs we’re using for neoScores, where the button is part of a custom view you can easily pass on to the dialog yourself:

Let’s start off with creating a model, to keep state for message text, status and - optionally - extra content.

var Model = require('ampersand-state');

module.exports = Model.extend({
    props: {
        text: ['string', true, ''],
        status: ['string', true, 'success'],
        // Content should be of type Object, 
        // since we'll be dumping an ampersand-view into it.
        content: ['object', false, null]
    }
});

Next, we’re going to set up a simple view with a basic jade template for a message.

var AmpersandView = require('ampersand-view'),
    Message = require('../domain/Message');

module.exports = AmpersandView.extend({

    template: require('../templates/message.jade'),

    // We're binding text and status to the template.
    bindings: {
        'model.text': {
            type: 'innerHTML',
            selector: '.text'
        },
        'model.status': {
            type: 'class',
            selector: '.bar'
        }
    },

    // We're adding in an event for closing the message dialog.
    // PROTIP: Using both touchstart and mousedown guarantees a more
    // native tapping behaviour on mobile devices.
    events: {
        'touchstart .crossmark': 'hide',
        'mousedown .crossmark': 'hide',
        'click a': 'preventDefault'
    },

    initialize: function() {
        // We're letting the view instantiate its own model when it is initialized.
        this.model = new Message();
    },

    render: function() {
        this.renderWithTemplate(this);

        // The optional extra content is rendered as a subview.
        if (this.model.content)
            this.renderSubview(this.model.content, document.querySelector('.content'));

        return this;
    },

    hide: function(e) {
        this.preventDefault(e);
    },

    preventDefault: function(e) {
        e.preventDefault();
    }
}

Ofcourse you need to add in the jade template too.

.message
    a.btn-hide Hide
    p.text
    .content

It’s time to return to our view, and add in some more code. First of all, we want to instantiate the only once, when booting up our app. This means that our view should not only instantiate its own model, but also create its own root element, if it does not yet exist.

In the initialize function of the view, add something like:

// If the document already contains a div with class message,
// use this as the parentEl of the view.
if (document.querySelector('.message'))
    this.el = document.querySelector('.message');
else {
    // If .message does not exist yet, create a new div.message
    // and add it to the DOM.
    var div = document.createElement('div');
    div.classList.add('message');
    document.body.appendChild(div);

    // Then make that element the parentEl of the view.
    this.el = document.querySelector('.message');
}

Now, let’s make sure that the message can get shown or hidden properly. We want to be able to show the message from wherever in the code, by just calling something like message.show() with some parameters.
Let’s add a show method to the view:

show: function(text, status, content) {
    // Only message text is obligatory,
    // we should build in some fallbacks if the content
    // or status are not provided.
    if (content) this.model.content = content;
    else this.model.unset('content');

    if (status && typeof status !== 'object') this.model.status = status;
    else if (status) this.model.content = status;

    this.model.text = text;

    this.el.classList.add('show');

    // Only render when all props of the model are set.
    this.render();
}

And let’s simply remove the .show class when to hide the message. The hide method should look something like this:

hide: function(e) {
    this.preventDefault(e);
    this.parentEl.classList.remove('show');
}

The MessageView looks ok, now let’s add in some CSS to show/hide the message and to display the status. You can play around with the close button too. At neoScores we use our own SVG icon font for icon buttons.

.message {
  max-height: 0;
  overflow: hidden;
  z-index: 1
  width: 100%;
  padding: 1.5em;
  color: $white;
  transition: max-height 0.5s;
  &.show {
    max-height: 100%;
  }
  &.error {
    background: $red;
    box-shadow: inset 0 -1px 0 1px lighten($red, 2%);
  }
  &.warning {
    background: $yellow;
    box-shadow: inset 0 -1px 0 1px darken($yellow, 2%);
  }
  &.success {
    background: $green;
    box-shadow: inset 0 -1px 0 1px darken($green, 2%);
  }
}

Now there’s only one more thing left to do: initialize the MessageView bootstrapping the app.
This is done in client/app.js in a simple Ampersand setup. Add the MessageView to the app singleton:

// Extends our main app singleton
app.extend({
    me: new Me(),
    people: new People(),
    router: new Router(),
    message: new MessageView(),

PROTIP: with window.app = app; the app singleton is attached to the window scope, which comes in very handy during local development. NEVER bind app to the window scope in production code! If you do, tech-savvy users can get to the complete app object through the console.

You can easily show a welcome message with an input field by calling:

app.message.show('Hello there user, how are you doing?', 'success', new InputView());

Then, just create a new view called InputView where you handle the user input.

Final thoughts

Now you should have a very small component that you can include in whatever Ampersand project you like.
Please shoutout if and when it comes in handy, or if you’ve got some good ideas for ameliorating or extending this component.

For my next post, I’m planning on going a little bit deeper into unit testing Ampersand code with jasmine.