Skip to content

Instantly share code, notes, and snippets.

@bigopon
Last active August 3, 2017 22:47
Show Gist options
  • Save bigopon/62373c3d13c1a790c2f2ef6a8dc8ca4b to your computer and use it in GitHub Desktop.
Save bigopon/62373c3d13c1a790c2f2ef6a8dc8ca4b to your computer and use it in GitHub Desktop.
<template>
<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 (Cursor reset)</b>
<div m-t-5>Coercing Type: number</div>
<input m-t-5 type='text' value.bind='num' />
<div m-t-5>Value type in viewModel: ${numType}</div>
</div>
<div class='input-wrap'>
<b>bindable (No cursor reset)</b>
<div m-t-5>Coercing Type: number</div>
<input m-t-5 type='text' value.bind='num | toString' />
<div m-t-5>Value type in viewModel: ${numType}</div>
</div>
<div class='input-wrap'>
<b>bindable</b>
<div>Coercing Type: boolean</div>
<input m-t-5 type='text' value.bind='bool' />
<div m-t-5>Value type in viewModel: ${boolType}</div>
</div>
</template>
import {observable} from './observable'
import {bindable} from './bindable'
import {
HtmlBehaviorResource
} from 'aurelia-framework'
/**
* 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
@observable number2
numChanged() {
this.numType = typeof this.num;
}
boolChanged() {
this.boolType = typeof this.bool;
}
randChanged() {
this.randType = typeof this.rand;
}
attached() {
console.log('App attached', {app: this});
}
}
export class ToStringValueConverter {
toView(val) {
return val === null || val === void 0 ? '' : val.toString()
}
}
import * as LogManager from 'aurelia-logging';
import {
subscriberCollection,
ValueAttributeObserver
} from 'aurelia-binding';
import {TaskQueue} from 'aurelia-task-queue';
import {coerces} from './coerce';
type CoerceInstruction = string | { (value: any): any }
/**
* An implementation of Aurelia's Observer interface that is used to back bindable properties defined on a behavior.
*/
@subscriberCollection()
export class BehaviorPropertyObserver {
/**
* Creates an instance of BehaviorPropertyObserver.
* @param taskQueue The task queue used to schedule change notifications.
* @param obj The object that the property is defined on.
* @param propertyName The name of the property.
* @param selfSubscriber The callback function that notifies the object which defines the properties, if present.
* @param initialValue The initial value of the property.
* @param coerce Instruction on how to convert value in setter
*/
constructor(taskQueue: TaskQueue, obj: Object, propertyName: string, selfSubscriber: Function, initialValue: any, coerce?: CoerceInstruction) {
this.taskQueue = taskQueue;
this.obj = obj;
this.propertyName = propertyName;
this.notqueued = true;
this.publishing = false;
this.selfSubscriber = selfSubscriber;
this.currentValue = this.oldValue = initialValue;
if (typeof coerce !== 'undefined') {
this.setCoerce(coerce);
}
}
setCoerce(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;
}
this.coerce = c;
}
/**
* Gets the property's value.
*/
getValue(): any {
return this.currentValue;
}
/**
* Sets the property's value.
* @param newValue The new value to set.
*/
setValue(newValue: any): void {
let oldValue = this.currentValue;
let realNewValue = this.coerce ? this.coerce(newValue) : newValue;
if (oldValue !== realNewValue) {
this.oldValue = oldValue;
this.beforeCoercedValue = newValue;
this.currentValue = realNewValue;
if (this.publishing && this.notqueued) {
if (this.taskQueue.flushing) {
this.call();
} else {
this.notqueued = false;
this.taskQueue.queueMicroTask(this);
}
}
}
}
/**
* Invoked by the TaskQueue to publish changes to subscribers.
*/
call(): void {
let oldValue = this.oldValue;
let newValue = this.currentValue;
this.notqueued = true;
if (newValue === oldValue) {
return;
}
if (this.selfSubscriber) {
this.selfSubscriber(newValue, oldValue);
}
this.callSubscribers(newValue, oldValue);
this.oldValue = newValue;
}
/**
* Subscribes to the observerable.
* @param context A context object to pass along to the subscriber when it's called.
* @param callable A function or object with a "call" method to be invoked for delivery of changes.
*/
subscribe(context: any, callable: Function): void {
this.addSubscriber(context, callable);
}
/**
* Unsubscribes from the observerable.
* @param context The context object originally subscribed with.
* @param callable The callable that was originally subscribed with.
*/
unsubscribe(context: any, callable: Function): void {
this.removeSubscriber(context, callable);
}
}
import {_hyphenate} from './util';
import {bindingMode} from 'aurelia-framework';
import {Container} from 'aurelia-framework';
import {metadata} from 'aurelia-framework';
// Modified for testing
import {
BehaviorPropertyObserver
} from './behavior-property-observer';
function getObserver(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];
}
/**
* Represents a bindable property on a behavior.
*/
export class BindableProperty {
/**
* Creates an instance of BindableProperty.
* @param nameOrConfig The name of the property or a cofiguration object.
*/
constructor(nameOrConfig: string | Object) {
if (typeof nameOrConfig === 'string') {
this.name = nameOrConfig;
} else {
Object.assign(this, nameOrConfig);
}
this.attribute = this.attribute || _hyphenate(this.name);
if (this.defaultBindingMode === null || this.defaultBindingMode === undefined) {
this.defaultBindingMode = bindingMode.oneWay;
}
this.changeHandler = this.changeHandler || null;
this.owner = null;
this.descriptor = null;
}
/**
* Registers this bindable property with particular Class and Behavior instance.
* @param target The class to register this behavior with.
* @param behavior The behavior instance to register this property with.
* @param descriptor The property descriptor for this property.
*/
registerWith(target, behavior, descriptor) {
behavior.properties.push(this);
behavior.attributes[this.attribute] = this;
this.owner = behavior;
if (descriptor) {
this.descriptor = descriptor;
return this._configureDescriptor(descriptor);
}
return undefined;
}
_configureDescriptor(descriptor) {
const 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(this, name).getValue();
};
descriptor.set = function(value) {
getObserver(this, name).setValue(value);
};
descriptor.get.getObserver = function(obj) {
return getObserver(obj, name);
};
return descriptor;
}
/**
* Defines this property on the specified class and behavior.
* @param target The class to define the property on.
* @param behavior The behavior to define the property on.
*/
defineOn(target, behavior) {
let name = this.name;
let handlerName;
if (this.changeHandler === null) {
handlerName = name + 'Changed';
if (handlerName in target.prototype) {
this.changeHandler = handlerName;
}
}
if (this.descriptor === null) {
Object.defineProperty(target.prototype, name, this._configureDescriptor(behavior, {}));
}
}
/**
* Creates an observer for this property.
* @param viewModel The view model instance on which to create the observer.
* @return The property observer.
*/
createObserver(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 !== undefined) {
initialValue = typeof defaultValue === 'function' ? defaultValue.call(viewModel) : defaultValue;
}
return new BehaviorPropertyObserver(this.owner.taskQueue, viewModel, this.name, selfSubscriber, initialValue, this.coerce);
}
_initialize(viewModel, observerLookup, attributes, behaviorHandlesBind, boundProperties) {
let selfSubscriber;
let observer;
let attribute;
let defaultValue = this.defaultValue;
if (this.isDynamic) {
for (let key in attributes) {
this._createDynamicProperty(viewModel, observerLookup, behaviorHandlesBind, key, attributes[key], boundProperties);
}
} else if (!this.hasOptions) {
observer = observerLookup[this.name];
if (attributes !== null) {
selfSubscriber = observer.selfSubscriber;
attribute = attributes[this.attribute];
if (behaviorHandlesBind) {
observer.selfSubscriber = null;
}
if (typeof attribute === 'string') {
viewModel[this.name] = attribute;
observer.call();
} else if (attribute) {
boundProperties.push({
observer: observer,
binding: attribute.createBinding(viewModel)
});
} else if (defaultValue !== undefined) {
observer.call();
}
observer.selfSubscriber = selfSubscriber;
}
observer.publishing = true;
}
}
_createDynamicProperty(viewModel, observerLookup, behaviorHandlesBind, name, attribute, boundProperties) {
let changeHandlerName = name + 'Changed';
let selfSubscriber = null;
let observer;
let info;
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);
}
observer = observerLookup[name] = new BehaviorPropertyObserver(
this.owner.taskQueue,
viewModel,
name,
selfSubscriber
);
Object.defineProperty(viewModel, name, {
configurable: true,
enumerable: true,
get: observer.getValue.bind(observer),
set: observer.setValue.bind(observer)
});
if (behaviorHandlesBind) {
observer.selfSubscriber = null;
}
if (typeof attribute === 'string') {
viewModel[name] = attribute;
observer.call();
} else if (attribute) {
info = {
observer: observer,
binding: attribute.createBinding(viewModel)
};
boundProperties.push(info);
}
observer.publishing = true;
observer.selfSubscriber = selfSubscriber;
}
}
import {
HtmlBehaviorResource,
metadata
} from 'aurelia-framework'
import {BindableProperty} from './bindable-property'
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) {
console.log(a, !!a)
return !!a;
},
date(a) {
return new dateCons(a);
}
};
/**@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>
<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>
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
*/
const capitalMatcher = /([A-Z])/g;
function addHyphenAndLower(char) {
return '-' + char.toLowerCase();
}
export function _hyphenate(name) {
return (name.charAt(0).toLowerCase() + name.slice(1)).replace(capitalMatcher, addHyphenAndLower);
}
//https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM
//We need to ignore whitespace so we don't mess up fallback rendering
//However, we cannot ignore empty text nodes that container interpolations.
export function _isAllWhitespace(node) {
// Use ECMA-262 Edition 3 String and RegExp features
return !(node.auInterpolationTarget || (/[^\t\n\r ]/.test(node.textContent)));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment