Skip to content

Instantly share code, notes, and snippets.

@vermilion
Forked from jdanyow/app.html
Last active April 5, 2018 07:30
Show Gist options
  • Save vermilion/6e8a1ef7825abbfe1ec93242d1adcbbc to your computer and use it in GitHub Desktop.
Save vermilion/6e8a1ef7825abbfe1ec93242d1adcbbc to your computer and use it in GitHub Desktop.
<template>
<require from="./dynamic-expression-binding-behavior"></require>
<h4>Validation works when binding to model.firstName directly</h4>
<div class="form-group">
<input type="text" class="form-control" value.bind="model.firstName & validate">
</div>
<h4>No validation events when rebound prop used :(</h4>
<label repeat.for="prop of properties">
${prop.label}
<input type="text" class="form-control" value.bind="model | get:model:prop.expression | localDebug & validate">
</label>
</template>
import {inject} from 'aurelia-dependency-injection';
import {Parser} from 'aurelia-binding';
import {
ValidationControllerFactory,
ValidationController,
ValidationRules,
validateTrigger
} from 'aurelia-validation';
import {BootstrapFormRenderer} from './bootstrap-form-renderer';
@inject(ValidationControllerFactory)
export class App {
model = {
firstName: 'John',
lastName: 'Doe',
address: {
address1: '1 Main Street',
address2: '',
city: 'Burlington',
state: 'VT',
zip: '05401'
}
};
properties = [
{ label: 'First Name', expression: 'firstName' },
{ label: 'Last Name', expression: 'lastName' },
{ label: 'Address 1', expression: 'address.address1' },
{ label: 'Address 2', expression: 'address.address2' },
{ label: 'City', expression: 'address.city' },
{ label: 'State', expression: 'address.state' },
{ label: 'Zip', expression: 'address.zip' }
];
controller = null;
constructor(controllerFactory) {
this.controller = controllerFactory.createForCurrentScope();
this.controller.addRenderer(new BootstrapFormRenderer());
this.controller.validateTrigger = validateTrigger.change;
ValidationRules
.ensure(a => a.firstName).satisfies(value => {
console.log('Validating value "' + value + '"');
return true;
})
.on(this.model);
}
}
@inject(Parser)
export class GetValueConverter {
constructor(parser) {
this.parser = parser;
}
toView(_: any, model: any, propertyExpression: string): any {
let getExp = this.parser.parse(propertyExpression);
return getExp.evaluate({ bindingContext: model });
}
fromView(value: any, model: any, propertyExpression: string): any {
let setExp = this.parser.parse(propertyExpression + ' = $value');
setExp.evaluate({
bindingContext: model,
overrideContext: { $value: value }
});
return model;
}
}
export class LocalDebugValueConverter {
toView(value: any): any {
console.info(`[DEBUG-toView]`, JSON.stringify(value));
return value;
}
fromView(value: any): any {
console.info(`[DEBUG-fromView]`, JSON.stringify(value));
return value;
}
}
import {
ValidationRenderer,
RenderInstruction,
ValidateResult
} from 'aurelia-validation';
export class BootstrapFormRenderer {
render(instruction: RenderInstruction) {
for (let { result, elements } of instruction.unrender) {
for (let element of elements) {
this.remove(element, result);
}
}
for (let { result, elements } of instruction.render) {
for (let element of elements) {
this.add(element, result);
}
}
}
add(element: Element, result: ValidateResult) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// add the has-error class to the enclosing form-group div
formGroup.classList.add('has-error');
// add help-block
const message = document.createElement('span');
message.className = 'help-block validation-message';
message.textContent = result.message;
message.id = `validation-message-${result.id}`;
formGroup.appendChild(message);
}
remove(element: Element, result: ValidateResult) {
if (result.valid) {
return;
}
const formGroup = element.closest('.form-group');
if (!formGroup) {
return;
}
// remove help-block
const message = formGroup.querySelector(`#validation-message-${result.id}`);
if (message) {
formGroup.removeChild(message);
// remove the has-error class from the enclosing form-group div
if (formGroup.querySelectorAll('.help-block.validation-message').length === 0) {
formGroup.classList.remove('has-error');
}
}
}
}
import {inject} from 'aurelia-dependency-injection';
import {Parser} from 'aurelia-binding';
import {rebaseExpression} from './expression-rebaser';
@inject(Parser)
export class DynamicExpressionBindingBehavior {
constructor(parser) {
this.parser = parser;
}
bind(binding, source, rawExpression) {
// Parse the expression that was passed as a string argument to
// the binding behavior.
let expression = this.parser.parse(rawExpression);
// Rebase the expression
expression = rebaseExpression(expression, binding.sourceExpression);
// Squirrel away the binding's original expression so we can restore
// the binding to it's initial state later.
binding.originalSourceExpression = binding.sourceExpression;
// Replace the binding's expression.
binding.sourceExpression = expression;
}
unbind(binding, source) {
// Restore the binding to it's initial state.
binding.sourceExpression = binding.originalSourceExpression;
binding.originalSourceExpression = null;
}
}
import {ExpressionCloner, AccessMember, CallMember} from 'aurelia-binding';
export class ExpressionRebaser extends ExpressionCloner {
constructor(base) {
super();
this.base = base;
}
visitAccessThis(access) {
if (access.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return this.base;
}
visitAccessScope(access) {
if (access.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return new AccessMember(this.base, access.name);
}
visitCallScope(call) {
if (call.ancestor !== 0) {
throw new Error('$parent expressions cannot be rebased.');
}
return new CallMember(this.base, call.name, this.cloneExpressionArray(call.args));
}
}
export function rebaseExpression(expression, baseExpression) {
let visitor = new ExpressionRebaser(baseExpression);
return expression.accept(visitor);
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<style>
label, input {
display: block;
margin-bottom: 10px;
}
</style>
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-validation');
aurelia.start().then(() => aurelia.setRoot());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment