Files
tubestation/addon-sdk/source/lib/sdk/ui/component.js
2015-02-03 09:51:16 -08:00

183 lines
5.6 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Internal properties not exposed to the public.
const cache = Symbol("component/cache");
const writer = Symbol("component/writer");
const isFirstWrite = Symbol("component/writer/first-write?");
const currentState = Symbol("component/state/current");
const pendingState = Symbol("component/state/pending");
const isWriting = Symbol("component/writing?");
const isntNull = x => x !== null;
const Component = function(options, children) {
this[currentState] = null;
this[pendingState] = null;
this[writer] = null;
this[cache] = null;
this[isFirstWrite] = true;
this[Component.construct](options, children);
}
Component.Component = Component;
// Constructs component.
Component.construct = Symbol("component/construct");
// Called with `options` and `children` and must return
// initial state back.
Component.initial = Symbol("component/initial");
// Function patches current `state` with a given update.
Component.patch = Symbol("component/patch");
// Function that replaces current `state` with a passed state.
Component.reset = Symbol("component/reset");
// Function that must return render tree from passed state.
Component.render = Symbol("component/render");
// Path of the component with in the mount point.
Component.path = Symbol("component/path");
Component.isMounted = component => !!component[writer];
Component.isWriting = component => !!component[isWriting];
// Internal method that mounts component to a writer.
// Mounts component to a writer.
Component.mount = (component, write) => {
if (Component.isMounted(component)) {
throw Error("Can not mount already mounted component");
}
component[writer] = write;
Component.write(component);
if (component[Component.mounted]) {
component[Component.mounted]();
}
}
// Unmounts component from a writer.
Component.unmount = (component) => {
if (Component.isMounted(component)) {
component[writer] = null;
if (component[Component.unmounted]) {
component[Component.unmounted]();
}
} else {
console.warn("Unmounting component that is not mounted is redundant");
}
};
// Method invoked once after inital write occurs.
Component.mounted = Symbol("component/mounted");
// Internal method that unmounts component from the writer.
Component.unmounted = Symbol("component/unmounted");
// Function that must return true if component is changed
Component.isUpdated = Symbol("component/updated?");
Component.update = Symbol("component/update");
Component.updated = Symbol("component/updated");
const writeChild = base => (child, index) => Component.write(child, base, index)
Component.write = (component, base, index) => {
if (component === null) {
return component;
}
if (!(component instanceof Component)) {
const path = base ? `${base}${component.key || index}/` : `/`;
return Object.assign({}, component, {
[Component.path]: path,
children: component.children && component.children.
map(writeChild(path)).
filter(isntNull)
});
}
component[isWriting] = true;
try {
const current = component[currentState];
const pending = component[pendingState] || current;
const isUpdated = component[Component.isUpdated];
const isInitial = component[isFirstWrite];
if (isUpdated(current, pending) || isInitial) {
if (!isInitial && component[Component.update]) {
component[Component.update](pending, current)
}
// Note: [Component.update] could have caused more updates so can't use
// `pending` as `component[pendingState]` may have changed.
component[currentState] = component[pendingState] || current;
component[pendingState] = null;
const tree = component[Component.render](component[currentState]);
component[cache] = Component.write(tree, base, index);
if (component[writer]) {
component[writer].call(null, component[cache]);
}
if (!isInitial && component[Component.updated]) {
component[Component.updated](current, pending);
}
}
component[isFirstWrite] = false;
return component[cache];
} finally {
component[isWriting] = false;
}
};
Component.prototype = Object.freeze({
constructor: Component,
[Component.mounted]: null,
[Component.unmounted]: null,
[Component.update]: null,
[Component.updated]: null,
get state() {
return this[pendingState] || this[currentState];
},
[Component.construct](settings, items) {
const initial = this[Component.initial];
const base = initial(settings, items);
const options = Object.assign(Object.create(null), base.options, settings);
const children = base.children || items || null;
const state = Object.assign(Object.create(null), base, {options, children});
this[currentState] = state;
if (this.setup) {
this.setup(state);
}
},
[Component.initial](options, children) {
return Object.create(null);
},
[Component.patch](update) {
this[Component.reset](Object.assign({}, this.state, update));
},
[Component.reset](state) {
this[pendingState] = state;
if (Component.isMounted(this) && !Component.isWriting(this)) {
Component.write(this);
}
},
[Component.isUpdated](before, after) {
return before != after
},
[Component.render](state) {
throw Error("Component must implement [Component.render] member");
}
});
module.exports = Component;