Friday, 27 January 2017

Aurelia and select2

Aurelia - a great JavaScript framework for web development - but it's still in it's early stages so sometimes documentation isn't verbose enough and third party libraries haven't been integrated properly

meet

Select2 - The jQuery replacement for select boxes.


If you're integrating Select2 into a JavaScript framework such as Aurelia or Angular, you need to ensure that it can handle a few things:
  1. Changes in your ViewModel need to be reflected in the View
  2. Interactions with the View (by the user) need to update the ViewModel
To see the final version, jump straight to the bottom. Note: I use typescript in these code examples.

Vanilla select

These are both achieved with a normal select box by using the following syntax:

<select name="program" id="program" style="width: 100%;" class="form-control" value.bind="selectedId">
    <option repeat.for="p of programs" model.bind="p.programId">${p.name}</option>
</select>

You can see here that:
  • I have an array of objects in programs, which have properties programId and name.
  • The options (values of the drop-down) are repeated for every program.
  • The value of the selected drop-down is put into selectedId.
  • I've used model.bind because p is an object. If programs was an array of strings I would use value.bind.
A sample view model which sets up two programs be:
export class Test {
 programs = [{programId: 1, name: 'Program 1'}, {programId: 2, name: 'Program 2'}];
 selectedId: number;
}

And now every time you select an item, selectedId will be updated to reflect this change. Changes to selectedId will also cause the view to reflect the new selected value.

Select2

To turn this into a pretty Select2 input, all we need to do is call .select2() on the element, right?
$("#program").select2();
The problem comes when you update the ViewModel's selectedId property. Because Select2 hides the actual select box and replaces it with some nice html to give you that funky look, Select2 is only listening for changes to the select input and not some JavaScript property that it knows nothing about.

The solution is to trigger Select2 into updating it's own view when updates happen to our ViewModel. But How? Combine Aurelia's observer with Select2's event watch. Let's expand our Test class to setup the Select2 element and watch for viewModel changes:
export class Test {
 programs = [{programId: 1, name: 'Program 1'}, {programId: 2, name: 'Program 2'}];
 selectedId: number;
 select;
 
 constructor() {
  this.select = $("#program").select2();
 }
 
 selectedIdChanged() {
  if (this.select)
   this.select.trigger("change");
 }
}
You can see the convention of xxxChanged() to watch a property called xxx.
Are we done now? Not quite - When you change the selected value, you're now clicking on a Select2 element and not the actual select input that Aurelia is bound to, so we also need to update the ViewModel after Select2 notices a change. This ViewModel update would trigger an infinite loop of updates, so we need to wrap it in a test:
this.select.on("change", (event: JQueryEventObject, options: any) => {
     if (event.originalEvent) {
         return;
     }
     if (this.selectedId != (event.target).value) {
         this.selectedId = (event.target).value;
     }
}
It's important to note here that we need to use coercion (!= instead of !==) for this comparison, because select input values are strings, whereas our original object (the program.programId) is a number.

Still not done.

Select2 uses the option's value to determine which object has been chosen by the user, so make sure you include a value.bind as well as a model.bind on the options:
<option model.bind="p.programId" repeat.for="p of programs" value.bind="p.programId">${p.name}</option>

And now you're done! ... or are you?

A Select2 Custom Attribute

But wait, this isn't very reusable. There's a lot of code to put in each class that uses Select2. Let's turn it into a custom attribute with the following features:

  • watch ViewModel changes and update Select2 accordingly
  • watch user changes and update Aurelia's ViewModel accordingly
  • allow modification of Select2 options on each use of the attribute
  • Narrow down our JQuery selector to just the element in question without needing to consider all other element IDs on the page
For this sample, I'm using some Aurelia features that you should be familiar with, such as two-way binding, injection, etc.

select2-attribute.ts:
import {autoinject, bindable, bindingMode, customAttribute} from "aurelia-framework";

@customAttribute("select2")
@autoinject()
export class Select2Attribute {
    @bindable({ defaultBindingMode: bindingMode.twoWay }) value: any;
    @bindable({ defaultBindingMode: bindingMode.oneTime }) allowClear: boolean = false;

    private select: any;

    constructor(private element: Element) { }

    attached() {
        let clear = ((this.allowClear as any) === "true");
        if (this.select) {
            return;
        }
        this.select = $(this.element)
            .select2({ allowClear: clear, placeholder: "Select an option..." });
        this.select.on("change", (event: JQueryEventObject, options: any) => {
            if (event.originalEvent) {
                return;
            }
            if (this.value != (event.target).value) {
                this.value = (event.target).value;
            }
        });
    }

    valueChanged(newVal, oldVal) {
        if (this.select)
            this.select.trigger("change");
    }
    
    detached() {
        if (this.select) {
            this.select.off("change");
            this.select.select2("destroy");
        }
        this.select = null;
    }
}

usage:

<select select2="value.bind: selectedId; allow-clear: false" name="program" id="program" style="width: 100%;" class="form-control" value.bind="selectedId">
    <option repeat.for="p of programs" model.bind="p.programId" value.bind="p.programId">${p.name}</option>
</select>

Notes on usage:

  • the attribute select2="..." is the magic that Aurelia is looking for. Inside this attribute we need to specify value.bind to bind to our selectedId we discussed above. This is because a custom attribute can't see the other value.bind on the select element.
  • allow-clear is just one option that Select2 supports. Add as many as you like!
  • remember to include model.bind and value.bind on your options
Please let me know if this works for you! Many thanks to Dwayne and his blog on Select2 for getting me started.

1 comment:

Gregory Dickson said...

jspm install select2 -o "{ format: 'global' } "

Note, to install select2 use the above command

 
Copyright 2009 Another Blog. Powered by Blogger Blogger Templates create by Deluxe Templates. WP by Masterplan