Skip to content

Instantly share code, notes, and snippets.

@bigopon
Last active August 21, 2017 01:27
Show Gist options
  • Save bigopon/ad523ef690e40e9c5cde123cd0c532fa to your computer and use it in GitHub Desktop.
Save bigopon/ad523ef690e40e9c5cde123cd0c532fa to your computer and use it in GitHub Desktop.
<template>
<require from='./my-input.html'></require>
<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>App num: ${num}, type: ${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 (Cursor reset)</b>
<div m-t-5>Coercing Type: number</div>
<my-input m-t-5 type='text' value.two-way='num' ></my-input>
<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()
}
}
/**
* Code for BehaviorPropertyObserver to accomodate the new @bindable decorator
*
* Changes made:
*
* * Inside `constructor`, check for coerce instruction
* * Inside setValue, check for coerce instruction before set
*
* * At the end of the file, there is a modification to `callSubscribers` to add `beforeCoercedValue` in the fn call,
* in order to pass it around to prevent update DOM
*
*/
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,
newValue,
beforeCoercedValue
} = this;
this.notqueued = true;
if (newValue === oldValue) {
return;
}
if (this.selfSubscriber) {
this.selfSubscriber(newValue, oldValue, beforeCoercedValue);
}
this.callSubscribers(newValue, oldValue, beforeCoercedValue);
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);
}
}
let arrayPool1 = [];
let arrayPool2 = [];
let poolUtilization = [];
BehaviorPropertyObserver.prototype.callSubscribers = function callSubscribers(newValue, oldValue, beforeCoercedValue) {
let context0 = this._context0;
let callable0 = this._callable0;
let context1 = this._context1;
let callable1 = this._callable1;
let context2 = this._context2;
let callable2 = this._callable2;
let length = this._contextsRest ? this._contextsRest.length : 0;
let contextsRest;
let callablesRest;
let poolIndex;
let i;
if (length) {
// grab temp arrays from the pool.
poolIndex = poolUtilization.length;
while (poolIndex-- && poolUtilization[poolIndex]) {
// Do nothing
}
if (poolIndex < 0) {
poolIndex = poolUtilization.length;
contextsRest = [];
callablesRest = [];
poolUtilization.push(true);
arrayPool1.push(contextsRest);
arrayPool2.push(callablesRest);
} else {
poolUtilization[poolIndex] = true;
contextsRest = arrayPool1[poolIndex];
callablesRest = arrayPool2[poolIndex];
}
// copy the contents of the "rest" arrays.
i = length;
while (i--) {
contextsRest[i] = this._contextsRest[i];
callablesRest[i] = this._callablesRest[i];
}
}
if (context0) {
if (callable0) {
callable0.call(context0, newValue, oldValue, beforeCoercedValue);
} else {
context0(newValue, oldValue, beforeCoercedValue);
}
}
if (context1) {
if (callable1) {
callable1.call(context1, newValue, oldValue, beforeCoercedValue);
} else {
context1(newValue, oldValue);
}
}
if (context2) {
if (callable2) {
callable2.call(context2, newValue, oldValue, beforeCoercedValue);
} else {
context2(newValue, oldValue, beforeCoercedValue);
}
}
if (length) {
for (i = 0; i < length; i++) {
let callable = callablesRest[i];
let context = contextsRest[i];
if (callable) {
callable.call(context, newValue, oldValue, beforeCoercedValue);
} else {
context(newValue, oldValue, beforeCoercedValue);
}
contextsRest[i] = null;
callablesRest[i] = null;
}
poolUtilization[poolIndex] = false;
}
}
/**
* Code for BindableProperty to accomodate the new @bindable decorator
*
* Changes made: Inside `createObserver` to pass coerce instruction to `BehaviorPropertyObserver`
*/
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;
}
}
/**
* Code for the new @bindable decorator
*/
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);
}
/**
* Code for HtmlBehaviorResource to accomodate the new @bindable decorator
*
* Changes made: Inside `create`, check for bindings with twoWay binding mode to alter the `call` function
*/
import {
HtmlBehaviorResource,
Controller,
bindingMode,
DOM,
sourceContext,
ValueAttributeObserver,
} from 'aurelia-framework'
const targetContext = 'Binding:target';
HtmlBehaviorResource.prototype.create = function(container: Container, instruction?: BehaviorInstruction, element?: Element, bindings?: Binding[]): Controller {
let viewHost;
let au = null;
instruction = instruction || BehaviorInstruction.normal;
element = element || null;
bindings = bindings || null;
if (bindings) {
for (let i = 0, ii = bindings.length; ii > i; ++i) {
let binding = bindings[i];
if (binding.mode !== bindingMode.twoWay) continue;
if (binding.target.nodeType !== 1) continue;
console.log({ binding })
binding.call = alteredBindingCall;
}
}
if (this.elementName !== null && element) {
if (this.usesShadowDOM) {
viewHost = element.attachShadow(this.shadowDOMOptions);
container.registerInstance(DOM.boundary, viewHost);
} else {
viewHost = element;
if (this.targetShadowDOM) {
container.registerInstance(DOM.boundary, viewHost);
}
}
}
if (element !== null) {
element.au = au = element.au || {};
}
let viewModel = instruction.viewModel || container.get(this.target);
let controller = new Controller(this, instruction, viewModel, container);
let childBindings = this.childBindings;
let viewFactory;
if (this.liftsContent) {
//template controller
au.controller = controller;
} else if (this.elementName !== null) {
//custom element
viewFactory = instruction.viewFactory || this.viewFactory;
container.viewModel = viewModel;
if (viewFactory) {
controller.view = viewFactory.create(container, instruction, element);
}
if (element !== null) {
au.controller = controller;
if (controller.view) {
if (!this.usesShadowDOM && (element.childNodes.length === 1 || element.contentElement)) { //containerless passes content view special contentElement property
let contentElement = element.childNodes[0] || element.contentElement;
controller.view.contentView = { fragment: contentElement }; //store the content before appending the view
contentElement.parentNode && DOM.removeNode(contentElement); //containerless content element has no parent
}
if (instruction.anchorIsContainer) {
if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
controller.view.addBinding(childBindings[i].create(element, viewModel, controller));
}
}
controller.view.appendNodesTo(viewHost);
} else {
controller.view.insertNodesBefore(viewHost);
}
} else if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(element, viewModel, controller));
}
}
} else if (controller.view) {
//dynamic element with view
controller.view.controller = controller;
if (childBindings !== null) {
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
controller.view.addBinding(childBindings[i].create(instruction.host, viewModel, controller));
}
}
} else if (childBindings !== null) {
//dynamic element without view
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(instruction.host, viewModel, controller));
}
}
} else if (childBindings !== null) {
//custom attribute
for (let i = 0, ii = childBindings.length; i < ii; ++i) {
bindings.push(childBindings[i].create(element, viewModel, controller));
}
}
if (au !== null) {
au[this.htmlName] = controller;
}
if (instruction.initiatedByBehavior && viewFactory) {
controller.view.created();
}
return controller;
}
function alteredBindingCall(context, newValue, oldValue, beforeCoercedValue) {
if (!this.isBound) {
return;
}
if (context === sourceContext) {
oldValue = this.targetObserver.getValue(this.target, this.targetProperty);
newValue = newValue = arguments.length === 4 ? beforeCoercedValue : this.sourceExpression.evaluate(this.source, this.lookupFunctions);
if (newValue !== oldValue) {
this.updateTarget(newValue);
}
if (this.mode !== bindingMode.oneTime) {
this._version++;
this.sourceExpression.connect(this, this.source);
this.unobserve(false);
}
return;
}
if (context === targetContext) {
newValue = arguments.length === 4 ? beforeCoercedValue : newValue;
if (newValue !== this.sourceExpression.evaluate(this.source, this.lookupFunctions)) {
this.updateSource(newValue);
}
return;
}
throw new Error(`Unexpected call context ${context}`);
}
// ValueAttributeObserver.prototype.setValue = function(newValue, surpressNotify) {
// newValue = newValue === undefined || newValue === null ? '' : newValue;
// if (this.element[this.propertyName] !== newValue) {
// this.element[this.propertyName] = newValue;
// surpressNotify ? null : this.notify();
// }
// }
<!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>
import './html-behavior'
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
*/
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