Matt McCormick
Matt McCormick

Reputation: 13200

How to set Flash Messages in Aurelia?

I'm trying to figure out how to make flash messages in Aurelia. I've created a custom element flash-message and required it in app.html but the message doesn't update. It does display correctly if I set it to a default value.

app.html

<template>
  <require from="./resources/elements/flash-message"></require>
  <flash-message></flash-message>
</template>

flash-message.html

<template>
  <div class="alert alert-success">${message}</div>
</template>

flash-message.js

import {bindable, bindingMode} from 'aurelia-framework';

export class FlashMessage {
  @bindable({defaultBindingMode: bindingMode.twoWay}) message = 'Default';

  setMessage(newValue) {
    this.message = newValue;
  }
}

object-detail.js

import {FlashMessage} from './resources/elements/flash-message';

export class ObjectDetail {
  static inject() {return [FlashMessage]};

  constructor(flash) {
    this.flash = flash;
  }

  activate(params, routeConfig) {
    this.flash.setMessage('Activate');
  }
}

The activate() code is being called as well as the setMessage() method but the displayed messaged doesn't updated. What am I missing?

Upvotes: 1

Views: 950

Answers (3)

Matt McCormick
Matt McCormick

Reputation: 13200

From LStarky's message, I discovered toastr which I wasn't aware of before so I ended up just using that library instead.

npm install toastr --save

aurelia.json (Under bundle -> dependencies)

{
  "name": "toastr",
  "path": "../node_modules/toastr",
  "main": "toastr",
  "resources": [
    "build/toastr.min.css"
  ]
}

app.html

<require from="toastr/build/toastr.min.css"></require>

view-model.js

import toastr from 'toastr';

action() {
  toastr.success('Toastr visible!');
}

Upvotes: 4

LStarky
LStarky

Reputation: 2777

Since you initially only require the template in app.html without instantiating the class in app.js, Aurelia treats it as a custom element, which means it has its own instance (not a singleton). You are basically working with two different instances of FlashMessage, and therefore the properties of one is not reflected in the other.

If you want it to be instantiated as a singleton class, you'll also need to import the component in app.js and inject it in the constructor so that it is treated like a component rather than a custom element.

app.js

import {FlashMessage} from './resources/elements/flash-message';

@inject(FlashMessage)
export class App {
  constructor(flashMessage) {
    this.flashMessage = flashMessage;
  }
  // ...
}

Confusion between Custom Element and Class/ViewModel

Since all class properties are considered public, you don't even need the setMessage(newValue) method. You can update the message property from object-detail.js like this:

this.flash.message = 'Activate';

Also, the @bindable line is intended to be used so that you can instantiate it with the variable value in the HTML code, like this:

<flash-message message="Show this message"></flash-message>

If you don't plan to use it like this, I would skip the whole @bindable line. Your flash-message.js could be simplified to just this:

export class FlashMessage {
  constructor() {
    this.message = 'Default';
  }
}

Use of Event Aggregator for Flash Messages

I implemented a Flash Message class with similar goals, using the Toastr 3rd party library (just because I liked the UI). But it's not hard to set it up any way you want. The best approach, I believe, to allowing any part of your app to set a flash message is to use Aurelia's Event Aggregator. The following code snippet might help you get it set up.

flash-message.js

import { inject } from 'aurelia-framework'
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator)
export class FlashMessage {

  constructor(eventAggregator) {
    this.eventAggregator = eventAggregator;
    this.eventAggregator.subscribe('ShowFlashMessage', this.showMessage);
  }

  showMessage(message) {
    this.message = message;
    // hide after 10 seconds
    window.setTimeout(hideMessage, 10000);
  }

  hideMessage() {
    this.message = "";
  }
}

This is obviously simplified and doesn't handle multiple messages, or renewing the timer when a second message is posted, but it should be enough to get you started.

To set a message from another part of your app, you can simply first inject the eventAggregator and save in your constructor, and then publish a message like this:

this.eventAggregator.publish('ShowFlashMessage', "Record saved");

My Toastr Implementation in Aurelia:

Similar to what you did, I created a common class called FlashMessage in a subfolder called common in my src folder.

//src/common/flash-message.js
import * as toastr from 'toastr';
import { inject } from 'aurelia-framework'
import { EventAggregator } from 'aurelia-event-aggregator';

@inject(EventAggregator)
export class FlashMessage {

    constructor(eventAggregator) {
        this.eventAggregator = eventAggregator;
        this.eventAggregator.subscribe('ewFlashSuccess', this.showSuccess);
        this.eventAggregator.subscribe('ewFlashInfo', this.showInfo);
        this.eventAggregator.subscribe('ewFlashWarning', this.showWarning);
        this.eventAggregator.subscribe('ewFlashError', this.showError);

        // Not sure why this is not working... if you figure it out, let me know.
        toastr.options = {
            positionClass: "toast-top-left",
            showEasing: "swing",
            hideEasing: "linear",
            showMethod: "fadeIn",
            hideMethod: "fadeOut",
            preventDuplicates: true,
            closeButton: true
        }
    }

    showSuccess(message) {
        toastr.success(message, null, {preventDuplicates: true, closeButton: true});
    }

    showInfo(message) {
        toastr.info(message, null, {preventDuplicates: true, closeButton: true});
    }

    showWarning(message) {
        toastr.warning(message, null, {preventDuplicates: true, closeButton: true});
    }

    showError(message) {
        toastr.error(message, null, {preventDuplicates: true, closeButton: true});
    }

}

Then, I injected and instantiated it in app.js like this:

import { inject } from 'aurelia-framework';
import { FlashMessage } from './common/flash-message';
@inject(Core, FlashMessage)
export class App {
  constructor(core, flashMessage) {
    this.flashMessage = flashMessage;
  }
  // ...
}

I also had to require the CSS in app.html like this:

<require from="toastr/build/toastr.min.css"></require>

All of this depends on having Toastr properly installed (I installed it with npm install toastr --save) and correctly required as a dependency in aurelia.json (I'm using the CLI).

                {
                    "name": "toastr",
                    "path": "../node_modules/toastr",
                    "main": "toastr",
                    "resources": [
                        "build/toastr.min.css"
                    ]
                },

Final thoughts

Please also see Ashley Grant's response for a better explanation of getting a handle on your ViewModel as well as a working GistRun to fix your immediate issues. Ashley is much more experienced than I am with Aurelia so if parts of my solution don't work, his most likely will! :-)

Upvotes: 5

Ashley Grant
Ashley Grant

Reputation: 10887

I would recommend getting a reference to the ViewModel of the custom element using view-model.ref="flash". Note that you won't be able to use this from the activate callback though as no binding will have occurred at that point in the page lifecycle. I'm using the attached callback in my example below.

Here's an example: https://gist.run?id=76ef47a5327a34560737f4ade1038305

app.html

<template>
  <require from="./flash-message"></require>
  <flash-message view-model.ref="flash"></flash-message>
</template>

app.js

export class App {
  attached(params, routeConfig) {
    this.flash.setMessage('Activate');
  }
}

flash-message.html

<template>
  <div class="alert alert-success">${message}</div>
</template>

flash-message.js

import {bindable, bindingMode} from 'aurelia-framework';

export class FlashMessage {
  @bindable({defaultBindingMode: bindingMode.twoWay}) message = 'Default';

  setMessage(newValue) {
    this.message = newValue;
  }
}

Upvotes: 4

Related Questions