Skip to content

Instantly share code, notes, and snippets.

@niieani
Last active November 22, 2017 19:43
Show Gist options
  • Save niieani/c0434498592445f6b7e46d40f9331259 to your computer and use it in GitHub Desktop.
Save niieani/c0434498592445f6b7e46d40f9331259 to your computer and use it in GitHub Desktop.
Aurelia Observer Test + evaluate + connectable-binding change + fix
<template>
<require from="./example"></require>
<binding-intercept-example if.bind="show"></binding-intercept-example>
</template>
export class App {
show = true;
// constructor() {
// setInterval(() => this.show = !this.show, 2000);
// }
}
export function bindingIntercept(interceptor) {
return function interception(definition, propertyName, descriptor) {
let viewModelValue = descriptor.initializer && descriptor.initializer();
let observers = new Map();
let observerId = 0;
const observableGetter = function() { return viewModelValue; }
observableGetter.getObserver = function(targetClass) {
const thisObserverId = observerId++;
return {
// doNotCache: true,
expressionHook: function(expression, binding) {
if (!expression.__intercepted__) {
expression.assign = (scope, value) => {
// assignement coming from View
if (this.interceptorInstance.next)
this.interceptorInstance.next(value);
}
expression.evaluate = (scope, lookupFunctions) => {
return this.lastValue;
}
expression.__intercepted__ = true;
}
// workaround:
// updates value the first time after the expression is hooked
// -- unfortunately the target is first evaluated and then connected
// this is called during connect and therefore the initial value sneaks in.
// binding.updateTarget(binding.sourceExpression.evaluate(binding.source, binding.lookupFunctions));
},
bindings: new Set(),
subscribe: function(context, binding) {
if (this.bindings.size === 0) {
this.lastValue = undefined;
this.interceptorInstance = getInterceptor(interceptor, viewModelValue, this);
}
this.bindings.add(binding);
observers.set(thisObserverId, this);
},
unsubscribe: function(context, binding) {
this.bindings.delete(binding);
if (this.bindings.size === 0) {
observers.delete(thisObserverId);
if (this.interceptorInstance.dispose) {
this.interceptorInstance.dispose();
this.interceptorInstance = undefined;
}
}
}
}
}
delete descriptor.writable;
delete descriptor.initializer;
Object.defineProperty(descriptor, 'set', {
value: function (newViewModelValue) {
// assignement coming from ViewModel:
viewModelValue = newViewModelValue;
updateObservers(interceptor, viewModelValue, observers);
}
});
Object.defineProperty(descriptor, 'get', {
value: observableGetter
});
}
}
function getBindingValueSetter(observer) {
return function (value) {
if (observer.lastValue !== value) {
observer.lastValue = value;
if (observer.interceptorInstance) {
for (let binding of observer.bindings) {
binding.call('Binding:source');
}
}
}
}
}
function updateObservers(interceptor, viewModelValue, observers) {
for (let observer of observers.values()) {
if (observer.interceptorInstance.dispose) {
observer.interceptorInstance.dispose();
}
observer.interceptorInstance = getInterceptor(interceptor, viewModelValue, observer);
for (let binding of observer.bindings) {
binding.call('Binding:source');
}
}
}
function getInterceptor(interceptor, viewModelValue, observer) {
return interceptor(viewModelValue, getBindingValueSetter(observer)) || {};
}
import * as aureliaBinding from 'aurelia-binding';
import * as aureliaTemplatingBinding from 'aurelia-templating-binding';
export const sourceContext = 'Binding:source';
const slotNames = [];
const versionSlotNames = [];
for (let i = 0; i < 100; i++) {
slotNames.push(`_observer${i}`);
versionSlotNames.push(`_observerVersion${i}`);
}
function addObserver(observer, expression) {
// debugger;
// find the observer.
let observerSlots = this._observerSlots === undefined ? 0 : this._observerSlots;
let i = observerSlots;
while (i-- && this[slotNames[i]] !== observer) {
// Do nothing
}
// if we are not already observing, put the observer in an open slot and subscribe.
if (i === -1) {
i = 0;
while (this[slotNames[i]]) {
i++;
}
this[slotNames[i]] = observer;
observer.subscribe(sourceContext, this);
// increment the slot count.
if (i === observerSlots) {
this._observerSlots = i + 1;
}
}
if (observer && observer.expressionHook) {
observer.expressionHook(expression, this);
}
// set the "version" when the observer was used.
if (this._version === undefined) {
this._version = 0;
}
this[versionSlotNames[i]] = this._version;
}
function observeProperty(obj, propertyName, expression) {
let observer = this.observerLocator.getObserver(obj, propertyName);
addObserver.call(this, observer, expression);
}
aureliaBinding.AccessScope.prototype.connect = function connect(binding, scope) {
let context = aureliaBinding.getContextFor(this.name, scope, this.ancestor);
// monkey patching observeProperty
binding.observeProperty = observeProperty;
binding.observeProperty(context, this.name, this);
}
aureliaBinding.AccessMember.prototype.connect = function connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (obj) {
// monkey patching observeProperty
binding.observeProperty = observeProperty;
binding.observeProperty(obj, this.name, this);
}
}
aureliaBinding.AccessMember.prototype.connect = function connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (obj) {
// monkey patching observeProperty
binding.observeProperty = observeProperty;
binding.observeProperty(obj, this.name, this);
}
}
aureliaBinding.AccessKeyed.prototype.connect = function connect(binding, scope) {
this.object.connect(binding, scope);
let obj = this.object.evaluate(scope);
if (obj instanceof Object) {
this.key.connect(binding, scope);
let key = this.key.evaluate(scope);
// observe the property represented by the key as long as it's not an array
// being accessed by an integer key which would require dirty-checking.
if (key !== null && key !== undefined
&& !(Array.isArray(obj) && typeof(key) === 'number')) {
// monkey patching observeProperty
binding.observeProperty = observeProperty;
binding.observeProperty(obj, key, this);
}
}
}
/*
aureliaTemplatingBinding.ChildInterpolationBinding.prototype.call = function call() {
if (!this.isBound) {
return;
}
if (this.mode !== aureliaBinding.bindingMode.oneTime) {
this._version++;
this.sourceExpression.connect(this, this.source);
if (value instanceof Array) {
this.observeArray(value);
}
this.unobserve(false);
}
var value = this.sourceExpression.evaluate(this.source, this.lookupFunctions);
this.updateTarget(value);
};
aureliaTemplatingBinding.ChildInterpolationBinding.prototype.bind = function bind(source) {
if (this.isBound) {
if (this.source === source) {
return;
}
this.unbind();
}
this.isBound = true;
this.source = source;
var sourceExpression = this.sourceExpression;
if (sourceExpression.bind) {
sourceExpression.bind(this, source, this.lookupFunctions);
}
if (this.mode === aureliaBinding.bindingMode.oneWay) {
(0, aureliaBinding.enqueueBindingConnect)(this);
}
var value = sourceExpression.evaluate(source, this.lookupFunctions);
this.updateTarget(value);
};
*/
<template>
<p>Promise Binding: ${somePromise + ' post-concat' & intercept: func}</p>
<!--<p>Promise Binding: ${somePromise ? somePromise : 'loading... (TENARY)'}</p>-->
<!--<p>Promise Binding: ${somePromise + ' post-concat'}</p>-->
<!--<p>Promise Binding: ${'pre-concat ' + somePromise + ' post-concat'}</p>-->
<!--<p>Promise Binding: ${somePromise || 'loading (OR ||)...'}</p>-->
<!--<button click.delegate="changePromise()">New Promise</button>-->
<!--<br/><br/>-->
<!--<p>Obj Promise Binding: ${someObjPromise.value || 'loading...'}</p>-->
<!--<br/><br/>-->
<!--<p>Timer Binding: ${someTimer}</p>-->
<!--<br/><br/>-->
<!--Two Way Binding (see console):<br/>-->
<!--<input value.bind="firstName">-->
<!--<button click.delegate="changeFirstName()">Change Name to Bazyli</button>-->
</template>
import {bindingIntercept} from './binding-intercept';
export class BindingInterceptExample {
@bindingIntercept(promiseBinding)
somePromise = new Promise(resolve => setTimeout(() => resolve('Hello!'), 2000));
changePromise() {
this.somePromise = new Promise(resolve => setTimeout(() => resolve('Awesome!'), 2000));
}
@bindingIntercept(promiseBinding)
someObjPromise = new Promise(resolve => setTimeout(() => resolve({value: 'accessed promise.value!'}), 2000));
@bindingIntercept(timer) someTimer;
@bindingIntercept(twoWayIntercept) firstName = 'Rob';
changeFirstName() {
this.firstName = 'Bazyli';
}
func() {
console.log('intercepted', arguments, this);
}
}
function promiseBinding(promise, bindingValueSetter) {
bindingValueSetter(undefined); // initial value;
promise.then(value => bindingValueSetter(value));
}
function timer(any, bindingValueSetter) {
let i = 0;
let timer = setInterval(() => bindingValueSetter(i++), 1000);
return {
dispose: () => clearInterval(timer)
}
}
function twoWayIntercept(value, bindingValueSetter) {
console.log('ViewModel changed value to:', value);
bindingValueSetter(value); // initial value;
return {
next: (value) => {
console.log('View changed value to:', value)
}
}
}
const interceptMethods = ['updateTarget', 'updateSource', 'callSource'];
export class InterceptBindingBehavior {
bind(binding, scope, interceptor) {
let i = interceptMethods.length;
while (i--) {
let method = interceptMethods[i];
if (!binding[method]) {
continue;
}
binding[`intercepted-${method}`] = binding[method];
let update = binding[method].bind(binding);
binding[method] = interceptor.bind(binding, method, update);
}
}
unbind(binding, scope) {
let i = interceptMethods.length;
while (i--) {
let method = interceptMethods[i];
if (!binding[method]) {
continue;
}
binding[method] = binding[`intercepted-${method}`];
binding[`intercepted-${method}`] = null;
}
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</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(['./connectable-binding', 'aurelia-bootstrapper']);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment