Skip to content

Instantly share code, notes, and snippets.

@bigopon
Last active August 27, 2017 18:04
Show Gist options
  • Save bigopon/9ffa73a639702a547df81d9df33bdd66 to your computer and use it in GitHub Desktop.
Save bigopon/9ffa73a639702a547df81d9df33bdd66 to your computer and use it in GitHub Desktop.
<template>
<require from='./my-input.html'></require>
<style>
b { font-weight: bold; }
input { display: block; width: 100%; }
</style>
<style>
.input-wrap { display: block; padding: 5px; background: #dedede; border: 1px solid #eee; }
.input-wrap + .input-wrap { margin-top: 10px; }
b { font-weight: bold; }
[m-t-5] { margin-top: 5px; }
</style>
<div class='input-wrap'>
<b>bindable</b>
<div m-t-5>Coercing Type: number</div>
<input m-t-5 type='text' value.bind='num' />
<div m-t-5>App num: ${num}, type: ${numType}</div>
</div>
<div class='input-wrap'>
<b>bindable</b>
<div>Coercing Type: boolean</div>
<div>This is quite a special case,
do we want to update input as 'true', 'false', or keep its value, whatever it is?
</div>
<input m-t-5 type='text' value.bind='bool' />
<div m-t-5>Value inViewModel: ${bool}. Type: ${boolType}</div>
</div>
<div class='input-wrap'>
<b>bindable</b>
<div m-t-5>Coercing Type: <b>date</b></div>
<input m-t-5 type='text' value.bind='date' />
<div m-t-5>App num: ${date}, type: ${dateType}</div>
</div>
</template>
import {observable} from './observable'
import {bindable} from './bindable'
/**
* Coerce to number
*/
@observable.number('rand')
export class App {
randType = 'undefined'
/**
* Coerce to number
*/
@bindable.number num;
numType = 'undefined'
/**
* Coerce to boolean
*/
@bindable.boolean bool;
boolType = 'undefined'
@bindable.number number1
@bindable.date date
@observable number2
numChanged() {
this.numType = typeof this.num;
}
boolChanged() {
this.boolType = typeof this.bool;
}
randChanged() {
this.randType = typeof this.rand;
}
dateChanged() {
this.dateType = typeof this.date;
}
attached() {
window.app = this;
console.log('App attached', {app: this});
}
}
export class ToStringValueConverter {
toView(val) {
return val === null || val === void 0 ? '' : val.toString()
}
}
import './bindable-property';
import * as LogManager from 'aurelia-logging';
import {Container} from 'aurelia-framework';
import {metadata} from 'aurelia-framework';
import {
BindableProperty,
BehaviorPropertyObserver
} from 'aurelia-framework';
import { coerces } from './coerce';
function getObserver(behavior, instance, name) {
let lookup = instance.__observers__;
if (lookup === undefined) {
// We need to lookup the actual behavior for this instance,
// as it might be a derived class (and behavior) rather than
// the class (and behavior) that declared the property calling getObserver().
// This means we can't capture the behavior in property get/set/getObserver and pass it here.
// Note that it's probably for the best, as passing the behavior is an overhead
// that is only useful in the very first call of the first property of the instance.
let ctor = Object.getPrototypeOf(instance).constructor; // Playing safe here, user could have written to instance.constructor.
let behavior = metadata.get(metadata.resource, ctor);
if (!behavior.isInitialized) {
behavior.initialize(Container.instance || new Container(), instance.constructor);
}
lookup = behavior.observerLocator.getOrCreateObserversLookup(instance);
behavior._ensurePropertiesDefined(instance, lookup);
}
return lookup[name];
}
BindableProperty.prototype._configureDescriptor = function(behavior, descriptor) {
let coerce = this.coerce;
let name = this.name;
descriptor.configurable = true;
descriptor.enumerable = true;
if ('initializer' in descriptor) {
this.defaultValue = descriptor.initializer;
delete descriptor.initializer;
delete descriptor.writable;
}
if ('value' in descriptor) {
this.defaultValue = descriptor.value;
delete descriptor.value;
delete descriptor.writable;
}
descriptor.get = function() {
return getObserver(behavior, this, name).getValue();
};
if (coerce) {
let coerceFn = this._resolveCoerce(coerce);
if (coerceFn.validate) {
descriptor.set = function(value) {
let coercedValue = coerceFn(value);
let oldValue = this[name];
if (!coerceFn.validate(value, coercedValue, oldValue)) {
return;
}
if (coercedValue === oldValue) {
return;
}
getObserver(behavior, this, name).setValue(coercedValue);
}
} else {
descriptor.set = function(value) {
let coercedValue = coerceFn(value);
let oldValue = this[name];
if (coercedValue === oldValue) {
return;
}
getObserver(behavior, this, name).setValue(coercedValue);
}
}
} else {
descriptor.set = function(value) {
getObserver(behavior, this, name).setValue(value);
};
}
descriptor.get.getObserver = function(obj) {
return getObserver(behavior, obj, name);
};
return descriptor;
}
BindableProperty.prototype._resolveCoerce = function(coerce) {
let c;
switch (typeof coerce) {
case 'function':
c = coerce; break;
case 'string':
c = coerces[coerce]; break;
default: break;
}
if (!c) {
LogManager
.getLogger('behavior-property-observer')
.warn(`Invalid coerce instruction. Should be either one of ${Object.keys(coerces)} or a function.`);
c = coerces.none;
}
return c;
}
BindableProperty.prototype.createObserver = function(viewModel) {
let selfSubscriber = null;
let defaultValue = this.defaultValue;
let changeHandlerName = this.changeHandler;
let name = this.name;
let initialValue;
if (this.hasOptions) {
return undefined;
}
if (changeHandlerName in viewModel) {
if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue, oldValue) => {
viewModel[changeHandlerName](newValue, oldValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
} else {
selfSubscriber = (newValue, oldValue) => viewModel[changeHandlerName](newValue, oldValue);
}
} else if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue, oldValue) => viewModel.propertyChanged(name, newValue, oldValue);
} else if (changeHandlerName !== null) {
throw new Error(`Change handler ${changeHandlerName} was specified but not declared on the class.`);
}
if (defaultValue !== void 0) {
initialValue = typeof defaultValue === 'function' ? defaultValue.call(viewModel) : defaultValue;
if (this.coerce) {
let coerceFn = this._resolveCoerce(this.coerce);
initialValue = coerceFn(initialValue);
}
}
return new BehaviorPropertyObserver(this.owner.taskQueue, viewModel, this.name, selfSubscriber, initialValue);
}
/**
* Code for the new @bindable decorator
*/
import './bindable-property';
import {
HtmlBehaviorResource,
metadata
} from 'aurelia-framework'
import {BindableProperty} from 'aurelia-framework'
export function bindable(nameOrConfigOrTarget, key, descriptor) {
let config; // tobe inited to object later and used inside deco()
let deco = function(target, key2, descriptor2) {
/**
* key2 = truthy => decorated on a class instance
* key2 = falsy => decorated on a class
*/
let isClassDecorator = !key2;
let actualTarget = key2 ? target.constructor : target;
let r = metadata.getOrCreateOwn(metadata.resource, HtmlBehaviorResource, actualTarget);
let prop;
let propType;
let userDidDefineCoerce = config.coerce !== void 0;
config.name = config.name || key2;
/**
* Support for Typescript decorator, with metadata on property type.
* Will check for typing only when user didn't explicitly set coerce
*/
if (!userDidDefineCoerce) {
propType = metadata.getOwn(metadata.propertyType, target, key2);
if (propType) {
config.coerce = classCoerceMap.get(propType) || 'none';
}
}
prop = new BindableProperty(config);
// config = null; // Does IE still have this memory leak ?
return prop.registerWith(actualTarget, r, descriptor2);
};
if (!nameOrConfigOrTarget) {
/**
* placed on property initializer with parens, without any params
* @example:
* class ViewModel {
* @bindable() property
* }
* @bindable() class ViewModel {}
*/
config = {};
return deco;
}
if (!key) {
/**
* placed on a class
* @example
* @bindable('name') class MyViewModel {}
* @bindable({ ... }) class MyViewModel {}
*/
config = typeof nameOrConfigOrTarget === 'string' ? { name: nameOrConfigOrTarget } : nameOrConfigOrTarget;
return deco;
}
/**
* placed on a property initializer without parens
* @example
* class ViewModel {
* @bindable property
* }
*
*/
// let target = nameOrConfigOrTarget;
// nameOrConfigOrTarget = null;
config = { name: key };
return deco(nameOrConfigOrTarget, key, descriptor);
}
export function registerTypeBindable(type) {
return bindable[type] = function(targetOrConfig, key, descriptor) {
if (!targetOrConfig) {
/**
* MyClass {
* @observable.number() num
* }
*/
return bindable({ coerce: type });
}
if (!key) {
/**
* @observable.number('num')
* class MyClass {}
*
* @observable.number({...})
* class MyClass
*
* class MyClass {
* @observable.number({...})
* num
* }
*/
targetOrConfig = typeof targetOrConfig === 'string' ? { name: targetOrConfig } : targetOrConfig;
targetOrConfig.coerce = type;
return bindable(targetOrConfig);
}
/**
* class MyClass {
* @observable.number num
* }
*/
return bindable({ coerce: type })(targetOrConfig, key, descriptor);
}
}
['string', 'number', 'boolean', 'date'].forEach(registerTypeBindable);
import * as LogManager from 'aurelia-framework';
const numCons = Number;
const dateCons = Date;
const _isFinite = isFinite;
const _isNaN = isNaN;
export const coerces = {
none(a) {
return a;
},
number(a) {
var val = numCons(a);
return !_isNaN(val) && _isFinite(val) ? val : 0;
},
string(a) {
return '' + a;
},
boolean(a) {
return !!a;
},
date(a) {
return new dateCons(a);
}
};
/**
* These validate functions act as a guard to prevent circle update
*/
coerces.boolean.validate = function(val, coercedValue, oldValue) {
// if incoming value is not a string, should always coerce so whatever coerced result is, it's valid
if (typeof val !== 'string') return true;
if (val === 'true' && oldValue) return false;
if (val === 'false' && !oldValue) return false;
return true;
};
coerces.date.validate = function(val, coercedValue, oldValue) {
return (typeof val === 'string' && !/^\d+$/.test(val))
&& coercedValue instanceof Date
&& _isFinite(coercedValue)
&& coercedValue.getTime() !== oldValue.getTime();
};
/**@type {Map<Function, string>} */
export const classCoerceMap = new Map([
[Number, 'number'],
[String, 'string'],
[Boolean, 'boolean'],
[Date, 'date']
]);
/**
* Map a class to a string for typescript property coerce
* @param Class {Function} the property class to register
* @param strType {string} the string that represents class in the lookup
* @param converter {function(val)} coerce function tobe registered with @param strType
*/
export function mapCoerceForClass(Class, strType, coerce) {
coerce = coerce || Class.coerce;
if (typeof strType !== 'string' || typeof coerce !== 'function') {
LogManager
.getLogger('behavior-property-observer')
.warn(`Bad attempt at mapping coerce for class: ${Class.name} to type: ${strType}`);
return;
}
coerces[strType] = coerce;
coerceClassMap.set(Class, strType);
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
<style>
body {
padding: 20px;
}
.form-component {
display: block;
margin-bottom: 20px;
}
</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>
console.clear();
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
aurelia.start().then(() => aurelia.setRoot());
}
<template bindable='value'>
<label>This is my input
<input value.bind='value'/>
</label>
</template>
import * as LogManager from 'aurelia-framework';
import {
coerces,
classCoerceMap,
mapCoerceForClass
} from './coerce';
import { metadata } from 'aurelia-framework';
type ObservableConfig = { name: string, changeHandler(curr: any, prev: any): any, coerce?: Function | string }
type TargetOrConfig = Function | ObservableConfig
export function observable(targetOrConfig: TargetOrConfig, key: string, descriptor?: PropertyDescriptor) {
/**
*
* @param {Function | {}} target
* @param {string} key
* @param {PropertyDescriptor} descriptor
* @param {ObservableConfig} config
*/
function deco(target, key, descriptor, config) { // eslint-disable-line no-shadow
let userDidDefineCoerce;
let propType;
let coerce;
// Setting up coerce
userDidDefineCoerce = config && typeof config.coerce !== 'undefined';
if (userDidDefineCoerce) {
switch (typeof config.coerce) {
case 'string':
coerce = coerces[config.coerce]; break;
case 'function':
coerce = config.coerce; break;
}
if (!coerce) {
LogManager
.getLogger('aurelia-observable-decortaor')
.warn(`Invalid coerce instruction. Should be either one of ${Object.keys(coerces)} or a function.`)
}
coerce = coerce || coerces.none;
} else {
propType = metadata.getOwn(metadata.propertyType, target, key);
if (propType) {
coerce = coerces[classCoerceMap.get(propType)] || coerces.none;
}
}
/**
* When called with parens, config will be undefined
* @example
* @observable() class MyVM {}
*
* class MyVM {
* @observable() prop
* }
*/
/**
* When called without parens on a class, key will be undefined
* @example
* @observable class MyVM {}
*/
const isClassDecorator = key === undefined;
if (isClassDecorator) {
target = target.prototype;
key = typeof config === 'string' ? config : config.name;
}
// use a convention to compute the inner property name
let innerPropertyName = `_${key}`;
const innerPropertyDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
writable: true
};
// determine callback name based on config or convention.
const callbackName = (config && config.changeHandler) || `${key}Changed`;
if (descriptor) {
// babel passes in the property descriptor with a method to get the initial value.
// set the initial value of the property if it is defined.
if (typeof descriptor.initializer === 'function') {
let temp = descriptor.initializer();
innerPropertyDescriptor.value = coerce ? coerce(temp) : temp;
}
} else {
// there is no descriptor if the target was a field in TS (although Babel provides one),
// or if the decorator was applied to a class.
descriptor = {};
}
// make the accessor enumerable by default, as fields are enumerable
if (!('enumerable' in descriptor)) {
descriptor.enumerable = true;
}
// we're adding a getter and setter which means the property descriptor
// cannot have a "value" or "writable" attribute
delete descriptor.value;
delete descriptor.writable;
delete descriptor.initializer;
// Add the inner property on the prototype.
Reflect.defineProperty(target, innerPropertyName, innerPropertyDescriptor);
// add the getter and setter to the property descriptor.
descriptor.get = function() { return this[innerPropertyName]; };
descriptor.set = function(newValue) {
let oldValue = this[innerPropertyName];
let realNewValue = coerce ? coerce(newValue) : newValue;
if (realNewValue === oldValue) {
return;
}
// Add the inner property on the instance and make it nonenumerable.
this[innerPropertyName] = realNewValue;
Reflect.defineProperty(this, innerPropertyName, { enumerable: false });
if (this[callbackName]) {
this[callbackName](realNewValue, oldValue, key);
}
};
// make sure Aurelia doesn't use dirty-checking by declaring the property's
// dependencies. This is the equivalent of "@computedFrom(...)".
descriptor.get.dependencies = [innerPropertyName];
if (isClassDecorator) {
/**
* No need return as runtime code will look like this
*
* observable(class Vm {})
*/
Reflect.defineProperty(target, key, descriptor);
} else {
/**
* Runtime code will look like this:
*
* class Vm {
* constructor() {
* observable(this, 'prop', descriptor); // the descriptor that is return from following line
* }
* }
*/
return descriptor;
}
}
if (key === undefined) {
// parens...
return (t, k, d) => deco(t, k, d, targetOrConfig);
}
return deco(targetOrConfig, key, descriptor);
}
/**
* @param {string} type
*/
export function registerTypeObservable(type) {
/**
* There no attempts to protect user from mis-using the decorators.
* ex. @observable({}, accidentParam) class SomeClass {}
* If we have some flag to use in if block, which can be remove at build time, it would be great.
*/
return observable[type] = function(targetOrConfig, key, descriptor) {
if (targetOrConfig === void 0) {
/**
* MyClass {
* @observable.number() num
* }
*
* @observable.number()
* class MyClass {}
*/
return observable({ coerce: type });
}
if (key === void 0) {
/**
* @observable.number('num')
* class MyClass {}
*
* @observable.number({...})
* class MyClass
*
* class MyClass {
* @observable.number({...})
* num
* }
*/
targetOrConfig = typeof targetOrConfig === 'string' ? { name: targetOrConfig } : targetOrConfig;
targetOrConfig.coerce = type;
return observable(targetOrConfig);
}
/**
* class MyClass {
* @observable.number num
* }
*/
return observable({ coerce: type })(targetOrConfig, key, descriptor);
}
}
['string', 'number', 'boolean', 'date'].forEach(registerTypeObservable);
/*
| typescript | babel
----------|------------------|-------------------------
property | config | config
w/parens | target, key | target, key, descriptor
----------|------------------|-------------------------
property | target, key | target, key, descriptor
no parens | n/a | n/a
----------|------------------|-------------------------
class | config | config
| target | target
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment