Whilst working on a cool little Pokemon focused Aurelia app using the Aurelia CLI I encountered a scenario I didn’t immediately know the answer too. I wanted an object which represents my form elements and their values are bound to this object.
I then had a submit button which I wanted to only enable when all fields on the object had been filled out. At first I tried an @observable
and couldn’t get it to work, then I had an epiphany: what about a computed getter?
The only caveat with this approach is you need to know your object properties up front if you want to avoid dirty-checking. I knew what each input would be bound too, so this wasn’t going to be an issue.
Below is literally the code copied and pasted from my app, this is what I am using:
import {computedFrom} from 'aurelia-framework';
export class MyViewModel {
currentSighting = {
name: null,
cp: null,
latitude: null,
longitude: null
};
@computedFrom('currentSighting.name', 'currentSighting.cp', 'currentSighting.latitude', 'currentSighting.longitude')
get sightingIsSubmittable() {
let currentSighting = this.currentSighting;
return (currentSighting.name && currentSighting.cp && currentSighting.latitude && currentSighting.longitude);
}
}
<template>
<form submit.delegate="handleSightingSubmit($event)">
<input type="text" value.bind="currentSighting.name" placeholder="Please select from the list">
<br>
<input type="text" value.bind="currentSighting.cp">
<br>
<input type="text" value.bind="currentSighting.latitude" readonly>
<br>
<input type="text" value.bind="currentSighting.longitude" readonly>
<input type="submit" disabled.bind="!sightingIsSubmittable">
</form>
</template>
One thing you’ll notice is all of my input elements are bound to properties on the currentSighting
object. This keeps everything nice and neat, it also allows me to post if off to the server in a JSON request.
Look at the submit button at the bottom of the form, notice the line disabled.bind="!sightingIsSubmittable"
we are binding to our getter and because we are using computeds in the form of computedFrom
we eliminate any dirty checking so it’s only updated when something changes on our object.
Lastly we also inverse the boolean value provided from the better as we want false to enable the button and true to disable it. That is really all there is to it. Now you can make things in your app work off of multiple values and their specific values.
Just try to not abuse computeds. In this instance it makes sense to use a getter and computeds, but in some cases, there might be a better way.
If you really want to avoid dirty-checking, then it’s totally possible to solve this without “knowing” all the properties on the model object.
The MultiObserver from this post: http://eisenbergeffect.bluespire.com/aurelias-adaptive-binding/ , can be used to observe a bunch of named properties on an object (which is good if you want to observe a select few properties, and/or the object is not a pure Model, like a ViewModel). Though this could be fairly easily modified to take a model only (or have an “overload”):
import {inject, BindingEngine} from ‘aurelia-framework’;
@inject(BindingEngine)
export class MultiObserver {
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
}
observe(object, callback) {
let properties = Object.getOwnPropertyNames(object);
let subscriptions = [], i = properties.length, propertyName;
while(i–) {
propertyName = properties[i];
subscriptions.push(this.bindingEngine.propertyObserver(object, propertyName).subscribe(callback));
}
return {
dispose: () => {
while(subscriptions.length) {
subscriptions.pop().dispose();
}
}
}
}
}
In your getter, why do you create a local variable:
let currentSighting = this.currentSighting;
instead of using the global (this.) version in the if?
Very helpful! Wondering if you could include a mention (or example!) of how you’d handle the situation where some of your form fields are arrays? I guess @computedFrom wouldn’t help much there?