Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 85 additions & 9 deletions src/bindable-property.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import {bindingMode} from 'aurelia-binding';
import {Container} from 'aurelia-dependency-injection';
import {metadata} from 'aurelia-metadata';

const reflectionConfigured = Symbol('reflection');

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
// 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
// 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);
Expand All @@ -27,6 +29,14 @@ function getObserver(instance, name) {
return lookup[name];
}

export type BindablePropertyConfig = {
defaultBindingMode?: bindingMode,
reflectToAttribute?: boolean | {(el: Element, name: string, newVal, oldVal): any},
name?: string,
attribute?: any,
changeHandler?: string
}

/**
* Represents a bindable property on a behavior.
*/
Expand All @@ -35,7 +45,7 @@ export class BindableProperty {
* Creates an instance of BindableProperty.
* @param nameOrConfig The name of the property or a cofiguration object.
*/
constructor(nameOrConfig: string | Object) {
constructor(nameOrConfig: string | BindablePropertyConfig) {
if (typeof nameOrConfig === 'string') {
this.name = nameOrConfig;
} else {
Expand All @@ -58,10 +68,16 @@ export class BindableProperty {
* @param descriptor The property descriptor for this property.
*/
registerWith(target: Function, behavior: HtmlBehaviorResource, descriptor?: Object): void {
let { reflectToAttribute } = this;

behavior.properties.push(this);
behavior.attributes[this.attribute] = this;
this.owner = behavior;

if (reflectToAttribute) {
behavior.hasReflections = true;
}

if (descriptor) {
this.descriptor = descriptor;
return this._configureDescriptor(descriptor);
Expand Down Expand Up @@ -134,27 +150,64 @@ export class BindableProperty {
let defaultValue = this.defaultValue;
let changeHandlerName = this.changeHandler;
let name = this.name;
let reflectToAttribute = this.reflectToAttribute;
let initialValue;
let attrName;
let reflectFunction;

if (this.hasOptions) {
return undefined;
}

if (reflectToAttribute) {
attrName = this.attribute === undefined ? _hyphenate(name) : this.attribute;
reflectFunction = typeof reflectToAttribute === 'function' ? reflectToAttribute : reflectFunctions[reflectToAttribute];
}

if (changeHandlerName in viewModel) {
if ('propertyChanged' in viewModel) {
if (reflectFunction !== undefined) {
selfSubscriber = (newValue, oldValue) => {
callReflection(viewModel, reflectFunction, attrName, newValue);
viewModel[changeHandlerName](newValue, oldValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
} else {
selfSubscriber = (newValue, oldValue) => {
viewModel[changeHandlerName](newValue, oldValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
}
} else {
if (reflectFunction !== undefined) {
selfSubscriber = (newValue, oldValue) => {
callReflection(viewModel, reflectFunction, attrName, newValue);
viewModel[changeHandlerName](newValue, oldValue);
};
} else {
selfSubscriber = (newValue, oldValue) => viewModel[changeHandlerName](newValue, oldValue);
}
}
} else if ('propertyChanged' in viewModel) {
if (reflectFunction !== undefined) {
selfSubscriber = (newValue, oldValue) => {
viewModel[changeHandlerName](newValue, oldValue);
callReflection(viewModel, reflectFunction, attrName, newValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
} else {
selfSubscriber = (newValue, oldValue) => viewModel[changeHandlerName](newValue, oldValue);
selfSubscriber = (newValue, oldValue) => viewModel.propertyChanged(name, 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.`);
}

/**
* When view model doesn't have change handler but this property has reflection
*/
if (selfSubscriber === null && reflectFunction !== undefined) {
selfSubscriber = (newValue, oldValue) => callReflection(viewModel, reflectFunction, attrName, newValue);
}

if (defaultValue !== undefined) {
initialValue = typeof defaultValue === 'function' ? defaultValue.call(viewModel) : defaultValue;
}
Expand Down Expand Up @@ -248,3 +301,26 @@ export class BindableProperty {
observer.selfSubscriber = selfSubscriber;
}
}

/**
* @param viewModel the view model instance
* @param attrName name of attribute will be set on the element
* @param newValue
*/
function callReflection(viewModel: Object, reflectFunction: (element: Element, attrName: string, val) => any, attrName: string, newValue) {
let { __element__ } = viewModel.__observers__;
reflectFunction(__element__, attrName, newValue);
}

const reflectFunctions = {
string(element, attrName, newValue) {
element.setAttribute(attrName, newValue);
},
boolean(element, attrName, newValue) {
if (newValue) {
element.setAttribute(attrName, '');
} else {
element.removeAttribute(attrName);
}
}
};
19 changes: 12 additions & 7 deletions src/html-behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class HtmlBehaviorResource {
this.attributes = {};
this.isInitialized = false;
this.primaryProperty = null;
this.hasReflections = false;
}

/**
Expand Down Expand Up @@ -155,8 +156,8 @@ export class HtmlBehaviorResource {
for (i = 0, ii = properties.length; i < ii; ++i) {
properties[i].defineOn(target, this);
}
// Because how inherited properties would interact with the default 'value' property
// in a custom attribute is not well defined yet, we only inherit properties on
// Because how inherited properties would interact with the default 'value' property
// in a custom attribute is not well defined yet, we only inherit properties on
// custom elements, where it's not a problem.
this._copyInheritedProperties(container, target);
}
Expand Down Expand Up @@ -330,6 +331,10 @@ export class HtmlBehaviorResource {
let childBindings = this.childBindings;
let viewFactory;

if (element !== null && this.hasReflections) {
this.observerLocator.getOrCreateObserversLookup(viewModel).__element__ = element;
}

if (this.liftsContent) {
//template controller
au.controller = controller;
Expand Down Expand Up @@ -424,9 +429,9 @@ export class HtmlBehaviorResource {
}

_copyInheritedProperties(container: Container, target: Function) {
// This methods enables inherited @bindable properties.
// We look for the first base class with metadata, make sure it's initialized
// and copy its properties.
// This methods enables inherited @bindable properties.
// We look for the first base class with metadata, make sure it's initialized
// and copy its properties.
// We don't need to walk further than the first parent with metadata because
// it had also inherited properties during its own initialization.
let behavior, derived = target;
Expand All @@ -441,7 +446,7 @@ export class HtmlBehaviorResource {
break;
}
}
behavior.initialize(container, target);
behavior.initialize(container, target);
for (let i = 0, ii = behavior.properties.length; i < ii; ++i) {
let prop = behavior.properties[i];
// Check that the property metadata was not overriden or re-defined in this class
Expand All @@ -451,6 +456,6 @@ export class HtmlBehaviorResource {
// We don't need to call .defineOn() for those properties because it was done
// on the parent prototype during initialization.
new BindableProperty(prop).registerWith(derived, this);
}
}
}
}
5 changes: 5 additions & 0 deletions src/view-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ function setAttribute(name, value) {
this._element.setAttribute(name, value);
}

function removeAttribute(name) {
this._element.removeAttribute(name);
}

function makeElementIntoAnchor(element, elementInstruction) {
let anchor = DOM.createComment('anchor');

Expand All @@ -119,6 +123,7 @@ function makeElementIntoAnchor(element, elementInstruction) {
anchor.hasAttribute = hasAttribute;
anchor.getAttribute = getAttribute;
anchor.setAttribute = setAttribute;
anchor.removeAttribute = removeAttribute;
}

DOM.replaceNode(anchor, element);
Expand Down
37 changes: 37 additions & 0 deletions test/bindable-property.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,41 @@ describe('BindableProperty', () => {
expect(new BindableProperty({ name: 'test', defaultBindingMode: null }).defaultBindingMode).toBe(oneWay);
expect(new BindableProperty({ name: 'test', defaultBindingMode: undefined }).defaultBindingMode).toBe(oneWay);
});

describe('reflects to attribute', () => {
let viewModel;
let element;
let observer;
beforeEach(() => {
element = document.createElement('div');
viewModel = { __observers__: { __element__: element } };
});

it('should reflect prop as string', () => {
let prop = new BindableProperty({
reflectToAttribute: 'string',
name: 'prop'
});
prop.owner = {};
observer = prop.createObserver(viewModel);
observer.selfSubscriber('Hello');
expect(element.getAttribute('prop')).toBe('Hello');
});

it('should reflect prop as boolean', () => {
let prop = new BindableProperty({
reflectToAttribute: 'boolean',
name: 'prop'
});
prop.owner = {};
observer = prop.createObserver(viewModel);
observer.selfSubscriber('Hello');
expect(element.getAttribute('prop')).toBe('');
['', NaN, 0, false, null, undefined].forEach(v => {
observer.selfSubscriber(v);
expect(element.hasAttribute('prop')).toBe(false);
element.setAttribute('prop', 'Hello');
});
});
});
});
1 change: 1 addition & 0 deletions test/html-behavior.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Container} from 'aurelia-dependency-injection';
import {ObserverLocator, bindingMode} from 'aurelia-binding';
import {TaskQueue} from 'aurelia-task-queue';
import {HtmlBehaviorResource} from '../src/html-behavior';
import {BindableProperty} from '../src/bindable-property';
import {ViewResources} from '../src/view-resources';

describe('html-behavior', () => {
Expand Down