Skip to content

Commit

Permalink
CHG: fix lifecycle callbacks && add support for custom properties and…
Browse files Browse the repository at this point in the history
… independent properties
  • Loading branch information
farthinker committed May 9, 2019
1 parent ba35351 commit 549876b
Show file tree
Hide file tree
Showing 9 changed files with 2,275 additions and 1,339 deletions.
2 changes: 1 addition & 1 deletion frontend/dist/tao.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/dist/tao.js.map

Large diffs are not rendered by default.

95 changes: 64 additions & 31 deletions frontend/src/component.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getAttributeParser } from './attribute-parser';
import {
dasherize, camelize, domReady, isFunction, componnetReady,
dasherize, camelize, domReady, isFunction, componnetReady, componnetConnected,
} from './utils';
import mergeMixins from './merge-mixins';

Expand All @@ -22,7 +22,8 @@ function generateComponentClass(tagName, options = {}) {
static $tao = true;

static observedAttributes = Object.keys(properties).reduce((acc, key) => {
if (isFunction(properties[key].observer)) acc.push(dasherize(key));
const { observer } = properties[key];
if (isFunction(observer) || typeof observer === 'string') acc.push(dasherize(key));
return acc;
}, []);

Expand Down Expand Up @@ -77,38 +78,66 @@ function generateComponentClass(tagName, options = {}) {
Object.keys(this.properties).forEach((key) => {
const property = this.properties[key];
const attributeName = dasherize(key);
const attributeParser = getAttributeParser(property.type);

Object.defineProperty(this, key, {
configurable: true,
get() {
if (property.type === Boolean) {
return this.hasAttribute(attributeName);
}
return attributeParser.parse(this.getAttribute(attributeName), {
defaultValue: property.default,
});
},
set(value) {
if (property.type === Boolean) {
if (value) {
this.setAttribute(attributeName, '');

if (isFunction(property.get) || isFunction(property.set)) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: property.get,
set: property.set,
});
} else if (property.syncAttribute === false) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
value: property.default,
writable: true,
});
} else if (property.type) {
const attributeParser = getAttributeParser(property.type);
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
if (property.type === Boolean) {
return this.hasAttribute(attributeName);
}
return attributeParser.parse(this.getAttribute(attributeName), {
defaultValue: property.default,
});
},
set(value) {
if (property.type === Boolean) {
if (value) {
this.setAttribute(attributeName, '');
} else {
this.removeAttribute(attributeName);
}
} else {
this.removeAttribute(attributeName);
this.setAttribute(attributeName, attributeParser.stringify(value));
}
} else {
this.setAttribute(attributeName, attributeParser.stringify(value));
}
},
});
},
});
}
});
}

attributeChangedCallback(attributeName, oldValue, newValue) {
if (this.taoStatus === 'connected' || this.taoStatus === 'ready') {
const property = this.properties[camelize(attributeName)];
if (property && property.observer) {
property.observer.call(this, newValue, oldValue);
const attributeParser = getAttributeParser(property.type);
const parsedNewValue = attributeParser.parse(newValue, {
defaultValue: property.default,
});
const parsedOldValue = attributeParser.parse(oldValue, {
defaultValue: property.default,
});
if (isFunction(property.observer)) {
property.observer.call(this, parsedNewValue, parsedOldValue);
} else if (typeof property.observer === 'string' && isFunction(this[property.observer])) {
this[property.observer].call(this, parsedNewValue, parsedOldValue);
}
}
}
}
Expand All @@ -127,18 +156,22 @@ function generateComponentClass(tagName, options = {}) {
this.taoStatus = 'connected';
this.namespacedTrigger('connected');
this.childrenReady().then(() => {
this.ready();
this.taoStatus = 'ready';
this.namespacedTrigger('ready');
if (this.taoStatus === 'connected') { // make sure current status is still connected
this.ready();
this.taoStatus = 'ready';
this.namespacedTrigger('ready');
}
});
});
}

disconnectedCallback() {
domReady().then(() => {
this.disconnected();
this.taoStatus = null;
this.namespacedTrigger('disconnected');
componnetConnected(this).then(() => {
this.disconnected();
this.taoStatus = null;
this.namespacedTrigger('disconnected');
});
});
}

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/merge-mixins.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isPlainObject } from './utils';

const LIFECYCLE_METHODS = 'created init connected ready disconnected'.split(' ');

function mergeData(data1, data2) {
Expand All @@ -20,7 +22,7 @@ function mergeLifecycleMethod(method1, method2) {

function normalizeProperties(properties) {
return Object.keys(properties).reduce((acc, key) => {
acc[key] = properties[key].type ? properties[key] : {
acc[key] = isPlainObject(properties[key]) ? properties[key] : {
type: properties[key],
};
return acc;
Expand Down
20 changes: 19 additions & 1 deletion frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export function isFunction(obj) {
return typeof obj === 'function';
}

export function isPlainObject(object) {
return object.toString() === '[object Object]';
}

export function domReady() {
return new Promise((resolve) => {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
Expand All @@ -30,7 +34,21 @@ export function componnetReady(el) {
if (el.taoStatus === 'ready') {
resolve();
} else {
el.addEventListener('tao:ready', resolve);
el.addEventListener('tao:ready', resolve, {
once: true,
});
}
});
}

export function componnetConnected(el) {
return new Promise((resolve) => {
if (el.taoStatus === 'connected' || el.taoStatus === 'ready') {
resolve();
} else {
el.addEventListener('tao:connected', resolve, {
once: true,
});
}
});
}
49 changes: 40 additions & 9 deletions frontend/test/component.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ describe('component', () => {

beforeAll(() => {
hooks = {
nameObserver: jasmine.createSpy(),
created: jasmine.createSpy(),
init: jasmine.createSpy(),
connected: jasmine.createSpy(),
ready: jasmine.createSpy(),
disconnected: jasmine.createSpy(),
nameObserver: jasmine.createSpy('nameObserver'),
activeObserver: jasmine.createSpy('activeObserver'),
created: jasmine.createSpy('created'),
init: jasmine.createSpy('init'),
connected: jasmine.createSpy('connected'),
ready: jasmine.createSpy('ready'),
disconnected: jasmine.createSpy('disconnected'),
};
Component('test-component', {
mixins: [{
Expand All @@ -27,16 +28,31 @@ describe('component', () => {
disconnected: hooks.disconnected,
}],
properties: {
active: Boolean,
active: {
type: Boolean,
observer: '_activeChanged',
},
json: {
type: Object,
default: { x: 1 },
},
array: Array,
customProp: {
get() {
console.log(this.fullName, this.age);
return `${this.fullName}_${this.age}`;
},
},
notAttribute: {
type: Boolean,
syncAttribute: false,
default: false,
},
},
created: hooks.created,
init: hooks.init,
connected: hooks.connected,
_activeChanged: hooks.activeObserver,
});
});

Expand Down Expand Up @@ -67,15 +83,15 @@ describe('component', () => {

expect(component.fullName).toBe('farthinker');
expect(component.getAttribute('full-name')).toBe('farthinker');
expect(hooks.nameObserver).toHaveBeenCalledWith('farthinker', null);
expect(hooks.nameObserver).toHaveBeenCalledWith('farthinker', '');

expect(component.active).toBe(false);
expect(component.hasAttribute('active')).toBe(false);
component.active = true;

expect(component.active).toBe(true);
expect(component.hasAttribute('active')).toBe(true);

expect(hooks.activeObserver).toHaveBeenCalledWith(true, false);

expect(component.json).toEqual({ x: 1 });
component.json = { y: 2 };
Expand Down Expand Up @@ -117,4 +133,19 @@ describe('component', () => {
expect(hooks.ready.calls.first().object).toBe(childComponent);
expect(hooks.ready.calls.mostRecent().object).toBe(parentComponent);
});

it('could has custom properties', () => {
component.fullName = 'farthinker';
component.age = 18;

expect(component.customProp).toBe('farthinker_18');
});

it('could has independent properties', () => {
expect(component.notAttribute).toBe(false);
component.notAttribute = true;

expect(component.notAttribute).toBe(true);
expect(component.hasAttribute('not-attribute')).toBe(false);
});
});
Loading

0 comments on commit 549876b

Please sign in to comment.