Last active
August 27, 2017 18:04
-
-
Save bigopon/9ffa73a639702a547df81d9df33bdd66 to your computer and use it in GitHub Desktop.
Typed @bindable, @observable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
console.clear(); | |
export function configure(aurelia) { | |
aurelia.use | |
.standardConfiguration() | |
aurelia.start().then(() => aurelia.setRoot()); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template bindable='value'> | |
<label>This is my input | |
<input value.bind='value'/> | |
</label> | |
</template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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