summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/ui/component.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/ui/component.js')
-rw-r--r--toolkit/jetpack/sdk/ui/component.js182
1 files changed, 182 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/component.js b/toolkit/jetpack/sdk/ui/component.js
new file mode 100644
index 000000000..d1f12c95e
--- /dev/null
+++ b/toolkit/jetpack/sdk/ui/component.js
@@ -0,0 +1,182 @@
+/* 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;