Last active
August 3, 2017 22:47
-
-
Save bigopon/62373c3d13c1a790c2f2ef6a8dc8ca4b 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> | |
<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> |
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' | |
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() | |
} | |
} |
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-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); | |
} | |
} |
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 {_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; | |
} | |
} |
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 { | |
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); |
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) { | |
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); | |
} |
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> | |
<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
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 | |
*/ |
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
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