wallace 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/component.js CHANGED
@@ -1,158 +1,174 @@
1
+ const throwAway = document.createElement("template");
1
2
  const NO_LOOKUP = "__";
2
3
 
3
- /**
4
- * The base component constructor.
5
- */
6
- export function Component() {
7
- this.ctrl = undefined;
8
- this.props = undefined;
9
- // Internal state objects (_e is created during build)
10
- this._s = []; // A stash for misc objects like repeaters.
11
- this._p = {}; // The previous values for watches to compare against.
12
- this._r = {}; // The current values read during an update.
13
- const root = this._n.cloneNode(true);
14
- this.el = root;
15
- this.ref = {};
16
- this._b(this, root);
17
- }
4
+ const ComponentBase = {
5
+ stubs: {},
6
+ prototype: {
7
+ /**
8
+ * The render function that gets called by parent components.
9
+ */
10
+ render: function (props, ctrl) {
11
+ this.props = props;
12
+ this.ctrl = ctrl;
13
+ this.update();
14
+ },
18
15
 
19
- Component.stubs = {};
16
+ /**
17
+ * Updates the DOM and renders nested components.
18
+ */
19
+ update: function () {
20
+ this._u(0, this._l);
21
+ },
20
22
 
21
- var proto = Component.prototype;
23
+ _u: function (i, il) {
24
+ let watch,
25
+ element,
26
+ parent,
27
+ displayToggle,
28
+ detacher,
29
+ lookupTrue,
30
+ shouldBeVisible,
31
+ detachedElements,
32
+ detachedElement,
33
+ index,
34
+ adjustedIndex;
22
35
 
23
- Object.defineProperty(proto, "hidden", {
24
- set: function (value) {
25
- this.el.hidden = value;
26
- }
27
- });
36
+ const watches = this._w,
37
+ props = this.props,
38
+ previous = this._p;
39
+ /*
40
+ Watches is an array of objects with keys:
41
+ e: the element index (number)
42
+ c: the callbacks (object)
43
+ ?v: visibility toggle (object)
28
44
 
29
- /**
30
- * Gets a stub by name.
31
- */
32
- proto._gs = function (name) {
33
- return this.constructor.stubs[name];
34
- };
45
+ The callback is an object whose key is the lookup and value is a function which
46
+ applies the effect.
35
47
 
36
- /**
37
- * Reads a query during update, returns an array with (oldValue, newValue, changed)
38
- * and saves the old value. Must reset this._r before each run.
39
- */
40
- proto._rq = function (props, key) {
41
- const run = this._r;
42
- if (run[key] === undefined) {
43
- let oldValue = this._p[key];
44
- const newValue = this._q[key](props, this);
45
- this._p[key] = newValue;
46
- const rtn = [newValue, oldValue, newValue !== oldValue];
47
- run[key] = rtn;
48
- return rtn;
49
- }
50
- return run[key];
51
- };
48
+ The visibility toggle has keys:
49
+ q: the query key in lookup
50
+ s: the number of watches to skip as their node is underneath
51
+ r: reversed
52
+ ?d: detacher
52
53
 
53
- /**
54
- * Applies the callbacks.
55
- */
56
- proto._ac = function (props, element, callbacks) {
57
- for (let key in callbacks) {
58
- let callback = callbacks[key];
59
- if (key === NO_LOOKUP) {
60
- callback(element, props, this);
61
- } else {
62
- const result = this._rq(props, key);
63
- if (result[2]) {
64
- callback(element, props, this, result[0]);
54
+ The detacher has keys:
55
+ i: the initial element index
56
+ s: the stash key of the detacher (plain object)
57
+ e: the parent element key
58
+ */
59
+ while (i < il) {
60
+ watch = watches[i];
61
+ element = this._e[watch.e];
62
+ displayToggle = watch.v;
63
+ shouldBeVisible = true;
64
+ if (displayToggle) {
65
+ lookupTrue = !!this._q[displayToggle.q](props, this);
66
+ shouldBeVisible = displayToggle.r ? lookupTrue : !lookupTrue;
67
+ detacher = displayToggle.d;
68
+ if (detacher) {
69
+ index = detacher.i;
70
+ parent = this._e[detacher.e];
71
+ detachedElements = this._s[detacher.s];
72
+ detachedElement = detachedElements[index];
73
+ if (shouldBeVisible && detachedElement) {
74
+ adjustedIndex =
75
+ index -
76
+ Object.keys(detachedElements).filter(function (k) {
77
+ return k < index && detachedElements[k];
78
+ }).length;
79
+ parent.insertBefore(detachedElement, parent.childNodes[adjustedIndex]);
80
+ detachedElements[index] = null;
81
+ } else if (!shouldBeVisible && !detachedElement) {
82
+ parent.removeChild(element);
83
+ detachedElements[index] = element;
84
+ }
85
+ } else {
86
+ element.hidden = !shouldBeVisible;
87
+ }
88
+ if (!shouldBeVisible) {
89
+ i += displayToggle.s;
90
+ }
91
+ }
92
+ if (shouldBeVisible) {
93
+ const prev = previous[i],
94
+ callbacks = watch.c;
95
+ for (let key in callbacks) {
96
+ if (key === NO_LOOKUP) {
97
+ callbacks[key](element, props, this);
98
+ } else {
99
+ const oldValue = prev[key],
100
+ newValue = this._q[key](props, this);
101
+ if (oldValue !== newValue) {
102
+ callbacks[key](element, props, this, newValue);
103
+ prev[key] = newValue;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ i++;
65
109
  }
66
110
  }
67
111
  }
68
112
  };
69
113
 
70
- /**
71
- * The render function that gets called by parent components.
72
- */
73
- proto.render = function (props, ctrl) {
74
- this.props = props;
75
- this.ctrl = ctrl;
76
- this.update();
77
- };
114
+ Object.defineProperty(ComponentBase.prototype, "hidden", {
115
+ set: function (value) {
116
+ this.el.hidden = value;
117
+ }
118
+ });
78
119
 
79
120
  /**
80
- * Updates the DOM.
81
- * Loops over watches, skipping n watches if elements are hidden.
121
+ * Creates the constructor function for a component definition.
122
+ *
123
+ * @param {*} baseComponent - a component definition to inherit from.
124
+ * @returns the newly created component definition function.
82
125
  */
83
- proto.update = function () {
84
- let i = 0,
85
- watch,
86
- element,
87
- parent,
88
- displayToggle,
89
- detacher,
90
- lookupTrue,
91
- shouldBeVisible,
92
- detachedElements,
93
- detachedElement,
94
- index,
95
- adjustedIndex,
96
- thisElement;
97
-
98
- const watches = this._w;
99
- const props = this.props;
100
- const il = watches.length;
101
- this._r = {};
102
- /*
103
- Watches is an array of objects with keys:
104
- e: the element reference (string)
105
- c: the callbacks (object)
106
- ?v: visibility toggle (object)
107
-
108
- The display toggle has keys:
109
- q: the query key in lookup
110
- s: the number of watches to skip as their node is underneath
111
- r: reversed
112
- ?d: detacher
126
+ export function createConstructor(baseComponent) {
127
+ const Component = function () {
128
+ const root = (this.el = this._n.cloneNode(true)),
129
+ dynamicElements = (this._e = []),
130
+ stash = (this._s = []),
131
+ previous = (this._p = []),
132
+ refs = (this.refs = {});
133
+ this.ctrl = {};
134
+ this.props = {};
135
+ this._l = this._w.length;
136
+ this._b(this, root, dynamicElements, stash, previous, refs);
137
+ };
113
138
 
114
- The detacher has keys:
115
- i: the initial element index
116
- s: the stash key of the detacher (plain object)
117
- e: the parent element key
118
- */
119
- while (i < il) {
120
- watch = watches[i];
121
- element = this._e[watch.e];
122
- displayToggle = watch.v;
123
- i++;
124
- shouldBeVisible = true;
125
- if (displayToggle) {
126
- lookupTrue = !!this._rq(props, displayToggle.q)[0];
127
- shouldBeVisible = displayToggle.r ? lookupTrue : !lookupTrue;
128
- detacher = displayToggle.d;
129
- if (detacher) {
130
- index = detacher.i;
131
- parent = this._e[detacher.e];
132
- detachedElements = this._s[detacher.s];
133
- detachedElement = detachedElements[index];
134
- if (shouldBeVisible && detachedElement) {
135
- adjustedIndex =
136
- index -
137
- Object.keys(detachedElements).filter(function (k) {
138
- return k < index && detachedElements[k];
139
- }).length;
140
- parent.insertBefore(detachedElement, parent.childNodes[adjustedIndex]);
141
- detachedElements[index] = null;
142
- } else if (!shouldBeVisible && !detachedElement) {
143
- thisElement = this._e[watch.e];
144
- parent.removeChild(thisElement);
145
- detachedElements[index] = thisElement;
146
- }
147
- } else {
148
- element.hidden = !shouldBeVisible;
149
- }
150
- if (!shouldBeVisible) {
151
- i += displayToggle.s;
152
- }
139
+ const proto = (Component.prototype = Object.create(baseComponent.prototype, {
140
+ constructor: {
141
+ value: Component
153
142
  }
154
- if (shouldBeVisible) {
155
- this._ac(props, element, watch.c);
143
+ }));
144
+
145
+ // This lets us assign to prototype without replacing it.
146
+ Object.defineProperty(Component, "methods", {
147
+ set: function (value) {
148
+ Object.assign(proto, value);
149
+ },
150
+ get: function () {
151
+ return proto;
156
152
  }
157
- }
158
- };
153
+ });
154
+ Component.create = function (props, ctrl) {
155
+ const component = new Component();
156
+ component.render(props, ctrl);
157
+ return component;
158
+ };
159
+ Component.stubs = {} && baseComponent.stubs;
160
+ return Component;
161
+ }
162
+
163
+ export function defineComponent(html, watches, queries, buildFunction, inheritFrom) {
164
+ const ComponentDefinition = createConstructor(inheritFrom || ComponentBase);
165
+ const proto = ComponentDefinition.prototype;
166
+ throwAway.innerHTML = html;
167
+ //Ensure these do not clash with fields on the component itself.
168
+ proto._w = watches;
169
+ proto._q = queries;
170
+ proto._b = buildFunction;
171
+ proto._n = throwAway.content.firstChild;
172
+ proto.base = ComponentBase.prototype;
173
+ return ComponentDefinition;
174
+ }
package/lib/extend.js ADDED
@@ -0,0 +1,20 @@
1
+ import { createConstructor } from "./component";
2
+
3
+ /**
4
+ * Calls to this function which provide the 2nd argument:
5
+ *
6
+ * const Foo = extendComponent(Bar, () => <div></div>))
7
+ *
8
+ * Are modified by the Babel plugin to become this:
9
+ *
10
+ * const Foo = defineComponent(,,,,Bar);
11
+ *
12
+ * So it should never be called with 2nd arg in real life.
13
+ */
14
+ export function extendComponent(base, componentDef) {
15
+ // This function call will have been replaced if 2nd arg is a valid component func.
16
+ // and therefore we would not receive it.
17
+ if (componentDef)
18
+ throw new Error("2nd arg to extendComponent must be a JSX arrow function");
19
+ return createConstructor(base);
20
+ }
package/lib/index.js CHANGED
@@ -1,28 +1,11 @@
1
- import { mount, watch, protect } from "./utils";
2
- import { Component } from "./component";
3
- import { KeyedRepeater, SequentialRepeater } from "./repeaters";
4
- import {
5
- extendComponent,
6
- defineComponent,
7
- findElement,
8
- onEvent,
9
- nestComponent,
10
- saveRef,
11
- stashMisc
12
- } from "./initCalls";
13
-
14
- export {
15
- Component,
16
- defineComponent,
17
- extendComponent,
18
- findElement,
19
- KeyedRepeater,
20
- mount,
21
- nestComponent,
22
- onEvent,
23
- protect,
24
- saveRef,
25
- SequentialRepeater,
26
- stashMisc,
27
- watch
28
- };
1
+ export { defineComponent } from "./component";
2
+ export { extendComponent } from "./extend";
3
+ export { mount } from "./mount";
4
+ export { nestComponent } from "./nest";
5
+ export { saveRef } from "./refs";
6
+ export { KeyedRepeater } from "./repeaters/keyed";
7
+ export { SequentialRepeater } from "./repeaters/sequential";
8
+ export { Router, route } from "./router";
9
+ export { getStub } from "./stubs";
10
+ export { findElement, onEvent, stashMisc } from "./utils";
11
+ export { watch, protect } from "./watch";
package/lib/mount.js ADDED
@@ -0,0 +1,17 @@
1
+ import { replaceNode } from "./utils";
2
+
3
+ /**
4
+ * Creates and mounts a component onto an element.
5
+ *
6
+ * @param {any} elementOrId Either a string representing an id, or an element.
7
+ * @param {callable} componentDefinition The class of Component to create
8
+ * @param {object} props The props to pass to the component (optional)
9
+ */
10
+ export function mount(elementOrId, componentDefinition, props, ctrl) {
11
+ const component = new componentDefinition();
12
+ component.render(props, ctrl);
13
+ const element =
14
+ typeof elementOrId === "string" ? document.getElementById(elementOrId) : elementOrId;
15
+ replaceNode(element, component.el);
16
+ return component;
17
+ }
package/lib/nest.js ADDED
@@ -0,0 +1,8 @@
1
+ import { findElement, replaceNode } from "./utils";
2
+
3
+ export function nestComponent(rootElement, path, componentDefinition) {
4
+ const el = findElement(rootElement, path),
5
+ child = new componentDefinition();
6
+ replaceNode(el, child.el);
7
+ return child;
8
+ }
package/lib/refs.js ADDED
@@ -0,0 +1,19 @@
1
+ function Ref(component, node, start, end) {
2
+ this.node = node;
3
+ this._c = component;
4
+ this._s = start;
5
+ this._e = end;
6
+ }
7
+
8
+ Ref.prototype.update = function () {
9
+ this._c._u(this._s, this._e);
10
+ };
11
+
12
+ /**
13
+ * Saves a reference to a node (element or nested component)
14
+ * Returns the node.
15
+ */
16
+ export function saveRef(node, component, refs, name, start, end) {
17
+ refs[name] = new Ref(component, node, start, end);
18
+ return node;
19
+ }
@@ -1,3 +1,5 @@
1
+ import { trimChildren } from "../utils";
2
+
1
3
  /*
2
4
  * Gets a component from the pool.
3
5
  */
@@ -13,21 +15,6 @@ function getComponent(pool, componentDefinition, ctrl, key, props) {
13
15
  return component;
14
16
  }
15
17
 
16
- /**
17
- * Trims the unwanted child elements from the end.
18
- *
19
- * @param {Node} e
20
- * @param {Array} childNodes
21
- * @param {Int} itemsLength
22
- */
23
- function trimChildren(e, childNodes, itemsLength) {
24
- let lastIndex = childNodes.length - 1;
25
- let keepIndex = itemsLength - 1;
26
- for (let i = lastIndex; i > keepIndex; i--) {
27
- e.removeChild(childNodes[i]);
28
- }
29
- }
30
-
31
18
  /**
32
19
  * Pulls an item forward in an array, to replicate insertBefore.
33
20
  * @param {Array} arr
@@ -101,48 +88,3 @@ proto.patch = function (e, items, ctrl) {
101
88
  this.keys = newKeys;
102
89
  trimChildren(e, childNodes, itemsLength);
103
90
  };
104
-
105
- /**
106
- * Repeats nested components, yielding from its pool sequentially.
107
- *
108
- * @param {componentDefinition} componentDefinition - The class ComponentDefinition to create.
109
- */
110
- export function SequentialRepeater(componentDefinition) {
111
- this.def = componentDefinition;
112
- this.pool = []; // pool of component instances
113
- this.count = 0; // Child element count
114
- }
115
-
116
- /**
117
- * Updates the element's childNodes to match the items.
118
- * Performance is important.
119
- *
120
- * @param {DOMElement} e - The DOM element to patch.
121
- * @param {Array} items - Array of items which will be passed as props.
122
- * @param {any} ctrl - The parent item's controller.
123
- */
124
- SequentialRepeater.prototype.patch = function (e, items, ctrl) {
125
- const pool = this.pool;
126
- const componentDefinition = this.def;
127
- const childNodes = e.childNodes;
128
- const itemsLength = items.length;
129
- let component,
130
- poolCount = pool.length,
131
- childElementCount = this.count;
132
-
133
- for (let i = 0; i < itemsLength; i++) {
134
- if (i < poolCount) {
135
- component = pool[i];
136
- } else {
137
- component = new componentDefinition();
138
- pool.push(component);
139
- poolCount++;
140
- }
141
- component.render(items[i], ctrl);
142
- if (i >= childElementCount) {
143
- e.appendChild(component.el);
144
- }
145
- }
146
- this.count = itemsLength;
147
- trimChildren(e, childNodes, itemsLength);
148
- };
@@ -0,0 +1,46 @@
1
+ import { trimChildren } from "../utils";
2
+
3
+ /**
4
+ * Repeats nested components, yielding from its pool sequentially.
5
+ *
6
+ * @param {componentDefinition} componentDefinition - The ComponentDefinition to create.
7
+ */
8
+ export function SequentialRepeater(componentDefinition) {
9
+ this.def = componentDefinition;
10
+ this.pool = []; // pool of component instances
11
+ this.count = 0; // Child element count
12
+ }
13
+
14
+ /**
15
+ * Updates the element's childNodes to match the items.
16
+ * Performance is important.
17
+ *
18
+ * @param {DOMElement} e - The DOM element to patch.
19
+ * @param {Array} items - Array of items which will be passed as props.
20
+ * @param {any} ctrl - The parent item's controller.
21
+ */
22
+ SequentialRepeater.prototype.patch = function (e, items, ctrl) {
23
+ const pool = this.pool;
24
+ const componentDefinition = this.def;
25
+ const childNodes = e.childNodes;
26
+ const itemsLength = items.length;
27
+ let component,
28
+ poolCount = pool.length,
29
+ childElementCount = this.count;
30
+
31
+ for (let i = 0; i < itemsLength; i++) {
32
+ if (i < poolCount) {
33
+ component = pool[i];
34
+ } else {
35
+ component = new componentDefinition();
36
+ pool.push(component);
37
+ poolCount++;
38
+ }
39
+ component.render(items[i], ctrl);
40
+ if (i >= childElementCount) {
41
+ e.appendChild(component.el);
42
+ }
43
+ }
44
+ this.count = itemsLength;
45
+ trimChildren(e, childNodes, itemsLength);
46
+ };
package/lib/router.jsx ADDED
@@ -0,0 +1,120 @@
1
+ /*
2
+ The Router is a component which mounts other components based on hash (URL bit after #).
3
+
4
+ It currently expects the route to be made of chunks separated by / which are either
5
+ text or placeholders:
6
+
7
+ /todos/detail/{id:int}/notes
8
+
9
+ It performs no validation yet, and may change to use regex in future.
10
+ */
11
+
12
+ const events = ["load", "hashchange"];
13
+ const noMod = x => x;
14
+ const converters = {
15
+ int: v => parseInt(v),
16
+ float: v => parseFloat(v),
17
+ date: v => new Date(v)
18
+ };
19
+
20
+ export function route(path, componentDef, converter, cleanup) {
21
+ return new Route(path, componentDef, converter, cleanup);
22
+ }
23
+
24
+ export const Router = () => <div></div>;
25
+
26
+ Router.methods = {
27
+ render(props, ctrl) {
28
+ const defaultError = (error, router) => (router.el.innerHTML = error.message);
29
+ this.error = props.error || defaultError;
30
+ this.current = null;
31
+ events.forEach(e => window.addEventListener(e, () => this.onHashChange()));
32
+ if (props.atts) {
33
+ Object.keys(props.atts).forEach(k => {
34
+ this.el.setAttribute(k, props.atts[k]);
35
+ });
36
+ }
37
+ this.base.render.call(this, props, ctrl);
38
+ },
39
+ async onHashChange() {
40
+ const path = location.hash.slice(1) || "",
41
+ routes = this.props.routes,
42
+ len = routes.length;
43
+ let i = 0,
44
+ routeData;
45
+ try {
46
+ while (i < len) {
47
+ let route = routes[i];
48
+ if ((routeData = route.match(path))) {
49
+ const component = await route.getComponent(routeData, this.ctrl);
50
+ this.current && this.current.cleanup();
51
+ this.mount(component);
52
+ this.current = route;
53
+ return;
54
+ }
55
+ i++;
56
+ }
57
+ throw new Error(`Router unable to match path "${path}"`);
58
+ } catch (error) {
59
+ this.error(error, this);
60
+ }
61
+ },
62
+ mount(component) {
63
+ this.el.innerHTML = "";
64
+ this.el.appendChild(component.el);
65
+ }
66
+ };
67
+
68
+ export function Route(path, def, convert, cleanup) {
69
+ this.chunks = path
70
+ .split("/")
71
+ .map(s => (s.startsWith("{") ? new RouteArg(s.slice(1, -1)) : s));
72
+ this.def = def;
73
+ this._convert = convert || noMod;
74
+ this._cleanup = cleanup || noMod;
75
+ this.component = null;
76
+ }
77
+
78
+ Route.prototype = {
79
+ match(url) {
80
+ const parts = url.split("?", 2),
81
+ hash = parts[0],
82
+ query = parts[1],
83
+ args = {},
84
+ definedChunksCount = this.chunks.length,
85
+ foundChunks = hash.split("/");
86
+ if (definedChunksCount !== foundChunks.length) return;
87
+ let i = 0;
88
+ while (i < definedChunksCount) {
89
+ let definedChunk = this.chunks[i];
90
+ let foundChunk = foundChunks[i];
91
+ if (definedChunk instanceof RouteArg) {
92
+ args[definedChunk.name] = definedChunk.convert(foundChunk);
93
+ } else if (definedChunk != foundChunk) {
94
+ return;
95
+ }
96
+ i++;
97
+ }
98
+ return { args, params: new URLSearchParams(query), url };
99
+ },
100
+ async getComponent(routeData, ctrl) {
101
+ if (!this.component) {
102
+ this.component = new this.def();
103
+ }
104
+ const props = await this._convert(routeData);
105
+ this.component.render(props, ctrl);
106
+ return this.component;
107
+ },
108
+ /**
109
+ * Allows user to delete component or perform other cleanup.
110
+ */
111
+ cleanup() {
112
+ this._cleanup(this);
113
+ }
114
+ };
115
+
116
+ function RouteArg(str) {
117
+ let chunks = str.split(":");
118
+ this.name = chunks[0];
119
+ this.convert = converters[chunks[1]] || noMod;
120
+ }
package/lib/stubs.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Gets a stub by name.
3
+ */
4
+ export function getStub(component, name) {
5
+ return component.constructor.stubs[name];
6
+ }
package/lib/types.d.ts CHANGED
@@ -47,9 +47,11 @@ const MyComponent = ({title}, {ctrl, event}) => (
47
47
 
48
48
  The **xargs** contains:
49
49
 
50
- - `ctrl` a reference to a controller object.
51
- - `self` a reference to the component instance (as `this` is not allowed).
52
- - `event` for use in event callbacks, signifies the event.
50
+ - `ctrl` refers to the controller.
51
+ - `props` refers to the props, in case you want the non-destructured version too.
52
+ - `self` refers to the component instance (as `this` is not allowed).
53
+ - `event` refers to the event in an event callback.
54
+ - `element` refers to the element in an event callback, or in `apply`.
53
55
 
54
56
  The function will be replaced by a very different one during compilation, therefore:
55
57
 
@@ -141,10 +143,9 @@ so use `self` from the **xargs** in component functions.
141
143
 
142
144
  Component instances have the following fields:
143
145
 
144
- - `props` the data for this component instance, stored as reference to original, not a
145
- copy.
146
- - `ctrl` an object to help coordinate things, also stored as reference.
147
- - `ref` a dictionary of named elements.
146
+ - `props` the data for this component instance.
147
+ - `ctrl` the controller object.
148
+ - `refs` an object with references to node.
148
149
  - `el` the component instance's root element.
149
150
 
150
151
  Both `props` and `ctrl` are set during the `render` method before calling `update`.
@@ -212,27 +213,6 @@ temporarily change it to something `class x:danger`.
212
213
 
213
214
  You can define your own directives in your babel config.
214
215
 
215
- Each has more
216
- detailed information on its JSDoc, which should display as a tool tip\* when you hover
217
- over it in your IDE.
218
-
219
- You can also
220
-
221
- - `apply` runs a callback to modify an element.
222
- - `bind` updates a value when an input is changed.
223
- - `class:xyz` defines a set of classes to be toggled.
224
- - `hide` sets an element or component's hidden property.
225
- - `html` Set the element's `innnerHTML` property.
226
- - `if` excludes an element from the DOM.
227
- - `on[EventName]` creates an event handler (note the code is copied)
228
- - `props` specifes props for a nested or repeated component, in which case it must be
229
- an array.
230
- - `ref` saves a reference to an element or nested component.
231
- - `show` sets and element or component's hidden property.
232
- - `style:xyz` sets a specific style property.
233
- - `toggle:xyz` toggles `xyz` as defined by `class:xyz` on same element, or class `xyz`.
234
-
235
-
236
216
  ## 5. Controllers
237
217
 
238
218
  A controller is just an object you create which gets passed down to every nested
@@ -516,6 +496,7 @@ declare module "wallace" {
516
496
  props: Props,
517
497
  xargs?: {
518
498
  ctrl: Controller;
499
+ props: Props;
519
500
  self: ComponentInstance<Props, Controller, Methods>;
520
501
  event: Event;
521
502
  element: HTMLElement;
@@ -539,12 +520,13 @@ declare module "wallace" {
539
520
  show?: boolean;
540
521
  hide?: boolean;
541
522
  }): JSX.Element;
542
- // methods?: ComponenMethods<Props, Controller> &
543
- // ThisType<ComponentInstance<Props, Controller, Methods>>;
523
+ methods?: ComponenMethods<Props, Controller> &
524
+ ThisType<ComponentInstance<Props, Controller, Methods>>;
544
525
  readonly prototype?: ComponenMethods<Props, Controller> &
545
526
  ThisType<ComponentInstance<Props, Controller, Methods>>;
546
527
  // Methods will not be available on nested component, so omit.
547
528
  readonly stubs?: Record<string, ComponentFunction<Props, Controller>>;
529
+ create?(props: Props): ComponentInstance<Props, Controller, Methods>;
548
530
  }
549
531
 
550
532
  type ComponenMethods<Props, Controller> = {
@@ -574,6 +556,11 @@ declare module "wallace" {
574
556
  Methods extends object = {}
575
557
  > = ComponentFunction<Props, Controller, Methods>;
576
558
 
559
+ export interface Ref {
560
+ node: HTMLElement | ComponentInstance;
561
+ update(): void;
562
+ }
563
+
577
564
  /**
578
565
  * The type for a component instance.
579
566
  */
@@ -585,7 +572,7 @@ declare module "wallace" {
585
572
  el: HTMLElement;
586
573
  props: Props;
587
574
  ctrl: Controller;
588
- ref: { [key: string]: HTMLElement };
575
+ refs: { [key: string]: Ref };
589
576
  base: Component<Props, Controller>;
590
577
  } & Component<Props, Controller> &
591
578
  Methods;
@@ -594,7 +581,6 @@ declare module "wallace" {
594
581
  * The component constructor function (typed as a class, but isn't).
595
582
  */
596
583
  export class Component<Props = any, Controller = any> {
597
- render(props: Props, ctrl?: Controller): void;
598
584
  /**
599
585
  * The base render method looks like this:
600
586
  *
@@ -705,6 +691,32 @@ declare module "wallace" {
705
691
  * @returns a Proxy of the object.
706
692
  */
707
693
  export function watch<T>(target: T, callback: WatchCallback): T;
694
+
695
+ export type RouteData = {
696
+ args: { [key: string]: any };
697
+ params: URLSearchParams;
698
+ url: string;
699
+ };
700
+
701
+ export function route<Props>(
702
+ path: string,
703
+ componentDef: ComponentFunction<Props>,
704
+ converter: RouteConverter<Props>
705
+ ): Route<Props>;
706
+
707
+ type RouteConverter<Props> = (routedata: RouteData) => Props;
708
+
709
+ export type Route<Props> = [string, ComponentFunction<Props>, RouteConverter<Props>?];
710
+ export type RouterProps = {
711
+ routes: readonly Route<unknown>[];
712
+ atts?: Record<string, unknown>;
713
+ error?: (error: Error, router: Router) => void;
714
+ };
715
+
716
+ export class Router extends Component {
717
+ static nest?({ props }: { props?: RouterProps }): JSX.Element;
718
+ mount(component: Component<any>): void;
719
+ }
708
720
  }
709
721
 
710
722
  type MustBeExpression = Exclude<any, string>;
@@ -864,19 +876,25 @@ interface DirectiveAttributes extends AllDomEvents {
864
876
  /**
865
877
  * ## Wallace directive: ref
866
878
  *
867
- * Saves a reference to the element on the component, allowing it to be accessed.
879
+ * Saves a reference to a node allowing you to:
880
+ *
881
+ * 1. Access the element or component as `ref.node`
882
+ * 2. Update the all nested elements using `ref.update()`.
868
883
  *
869
884
  * ```
870
- * <div ref:title></div>
885
+ * <div ref:title>
886
+ * {name}
887
+ * </div>
871
888
  * ```
872
889
  *
873
890
  * ```
874
- * component.ref.title.textContent = 'hello';
891
+ * component.refs.title.update();
892
+ * component.refs.title.node.textContent = 'hello';
875
893
  * ```
876
894
  *
877
895
  * Requires a qualifier, but you lose the tooltip in that format.
878
896
  */
879
- ref?: string;
897
+ ref?: null;
880
898
 
881
899
  /** ## Wallace directive: show
882
900
  *
@@ -949,7 +967,7 @@ declare namespace JSX {
949
967
  * - `items` set items for repeated component, must be an array of props.
950
968
  * - `on[EventName]` creates an event handler (note the code is copied)
951
969
  * - `props` specifes props for a nested components.
952
- * - `ref` saves a reference to an element or nested component.
970
+ * - `ref:xyz` saves a reference to node, allowing partial updates or access to element/component.
953
971
  * - `show` sets and element or component's hidden property.
954
972
  * - `style:xyz` sets a specific style property.
955
973
  * - `toggle:xyz` toggles `xyz` as defined by `class:xyz` on same element, or class
package/lib/utils.js CHANGED
@@ -1,66 +1,36 @@
1
- /**
2
- * Creates and mounts a component onto an element.
3
- *
4
- * @param {any} elementOrId Either a string representing an id, or an element.
5
- * @param {callable} componentDefinition The class of Component to create
6
- * @param {object} props The props to pass to the component (optional)
7
- */
8
- export function mount(elementOrId, componentDefinition, props, ctrl) {
9
- const component = new componentDefinition();
10
- component.render(props, ctrl);
11
- replaceNode(getElement(elementOrId), component.el);
12
- return component;
1
+ export function findElement(rootElement, path) {
2
+ return path.reduce((acc, index) => acc.childNodes[index], rootElement);
13
3
  }
14
4
 
15
5
  export function replaceNode(nodeToReplace, newNode) {
16
6
  nodeToReplace.parentNode.replaceChild(newNode, nodeToReplace);
17
7
  }
18
8
 
19
- export function getElement(elementOrId) {
20
- return typeof elementOrId === "string"
21
- ? document.getElementById(elementOrId)
22
- : elementOrId;
9
+ /**
10
+ * Trims the unwanted child elements from the end.
11
+ *
12
+ * @param {Node} e
13
+ * @param {Array} childNodes
14
+ * @param {Int} itemsLength
15
+ */
16
+ export function trimChildren(e, childNodes, itemsLength) {
17
+ let lastIndex = childNodes.length - 1;
18
+ let keepIndex = itemsLength - 1;
19
+ for (let i = lastIndex; i > keepIndex; i--) {
20
+ e.removeChild(childNodes[i]);
21
+ }
23
22
  }
24
23
 
25
- const MUTATING_METHODS = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
26
24
  /**
27
- * Returns a proxy which calls a callback when the object or its nested objects are
28
- * modified.
29
- *
30
- * Note that proxies have property `isProxy` set to true.
25
+ * Stash something on the component. Returns the element.
26
+ * The generated code is expected to keep track of the position.
31
27
  */
32
- export function watch(target, callback) {
33
- const handler = {
34
- get(target, key) {
35
- if (key == "isProxy") return true;
36
- const prop = target[key];
37
- if (typeof prop == "undefined") return;
38
- if (typeof prop === "object") return new Proxy(prop, handler);
39
- if (
40
- Array.isArray(target) &&
41
- typeof target[key] === "function" &&
42
- MUTATING_METHODS.includes(key)
43
- ) {
44
- return (...args) => {
45
- const result = target[key](...args);
46
- callback(target, key, args);
47
- return result;
48
- };
49
- }
50
- return prop;
51
- },
52
- set(target, key, value) {
53
- target[key] = value;
54
- callback(target, key, value);
55
- return true;
56
- }
57
- };
58
- return new Proxy(target, handler);
28
+ export function stashMisc(element, stash, object) {
29
+ stash.push(object);
30
+ return element;
59
31
  }
60
32
 
61
- export function protect(obj) {
62
- return watch(obj, (target, key, value) => {
63
- console.log("target", target, "key", key, "value", value);
64
- throw new Error("Attempted to modify protected object");
65
- });
33
+ export function onEvent(element, eventName, callback) {
34
+ element.addEventListener(eventName, callback);
35
+ return element;
66
36
  }
package/lib/watch.js ADDED
@@ -0,0 +1,42 @@
1
+ const MUTATING_METHODS = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
2
+ /**
3
+ * Returns a proxy which calls a callback when the object or its nested objects are
4
+ * modified.
5
+ *
6
+ * Note that proxies have property `isProxy` set to true.
7
+ */
8
+ export function watch(target, callback) {
9
+ const handler = {
10
+ get(target, key) {
11
+ if (key == "isProxy") return true;
12
+ const prop = target[key];
13
+ if (typeof prop == "undefined") return;
14
+ if (typeof prop === "object") return new Proxy(prop, handler);
15
+ if (
16
+ Array.isArray(target) &&
17
+ typeof target[key] === "function" &&
18
+ MUTATING_METHODS.includes(key)
19
+ ) {
20
+ return (...args) => {
21
+ const result = target[key](...args);
22
+ callback(target, key, args);
23
+ return result;
24
+ };
25
+ }
26
+ return prop;
27
+ },
28
+ set(target, key, value) {
29
+ target[key] = value;
30
+ callback(target, key, value);
31
+ return true;
32
+ }
33
+ };
34
+ return new Proxy(target, handler);
35
+ }
36
+
37
+ export function protect(obj) {
38
+ return watch(obj, (target, key, value) => {
39
+ console.log("target", target, "key", key, "value", value);
40
+ throw new Error("Attempted to modify protected object");
41
+ });
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallace",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "author": "Andrew Buchan",
5
5
  "description": "The framework that brings you FREEDOM!!",
6
6
  "license": "ISC",
@@ -8,13 +8,14 @@
8
8
  "files": [
9
9
  "/lib"
10
10
  ],
11
+ "sideEffects": false,
11
12
  "types": "./lib/types.d.ts",
12
13
  "scripts": {
13
14
  "test": "jest --clearCache && jest"
14
15
  },
15
16
  "dependencies": {
16
- "babel-plugin-wallace": "^0.5.0",
17
+ "babel-plugin-wallace": "^0.7.0",
17
18
  "browserify": "^17.0.1"
18
19
  },
19
- "gitHead": "12194040c835faf9d4f29e105dd908bacdbbb944"
20
+ "gitHead": "6cd84a9e6135e31d01880a82e6294ae0034bca59"
20
21
  }
package/lib/initCalls.js DELETED
@@ -1,116 +0,0 @@
1
- /**
2
- * Everything in here is used by or modified by the Babel plugin.
3
- */
4
- import { Component } from "./component";
5
- import { replaceNode } from "./utils";
6
-
7
- const throwAway = document.createElement("template");
8
-
9
- /**
10
- * A utility function that has to be in here because it needs _createConstructor and
11
- * we'd otherwise get cirular inmports.
12
- *
13
- * Calls to this function which provide the 2nd argument:
14
- *
15
- * const Foo = extendComponent(Bar, () => <div></div>))
16
- *
17
- * Are modified by the Babel plugin to become this:
18
- *
19
- * const Foo = defineComponent(,,,,Bar);
20
- *
21
- * So it should never be called with 2nd arg in real life.
22
- */
23
- export function extendComponent(base, componentDef) {
24
- // This function call will have been replaced if 2nd arg is a valid component func.
25
- // and therefor we would not receive it.
26
- if (componentDef)
27
- throw new Error("2nd arg to extendComponent must be a JSX arrow function");
28
- return _createConstructor(base);
29
- }
30
-
31
- /*
32
- Everything after this is used by code generated by the Babel plugin.
33
- */
34
-
35
- export function findElement(rootElement, path) {
36
- return path.reduce((acc, index) => acc.childNodes[index], rootElement);
37
- }
38
-
39
- export function nestComponent(rootElement, path, componentDefinition) {
40
- const el = findElement(rootElement, path),
41
- child = new componentDefinition();
42
- replaceNode(el, child.el);
43
- return child;
44
- }
45
-
46
- /**
47
- * Saves a reference to element or nested component. Returns the element.
48
- */
49
- export function saveRef(element, component, name) {
50
- component.ref[name] = element;
51
- return element;
52
- }
53
-
54
- /**
55
- * Stash something on the component. Returns the element.
56
- * The generated code is expected to keep track of the position.
57
- */
58
- export function stashMisc(element, component, object) {
59
- component._s.push(object);
60
- return element;
61
- }
62
-
63
- export function onEvent(element, eventName, callback) {
64
- element.addEventListener(eventName, callback);
65
- return element;
66
- }
67
-
68
- export function defineComponent(html, watches, queries, buildFunction, inheritFrom) {
69
- const ComponentDefinition = _createConstructor(inheritFrom || Component);
70
- const prototype = ComponentDefinition.prototype;
71
- throwAway.innerHTML = html;
72
- //Ensure these do not clash with fields on the component itself.
73
- prototype._w = watches;
74
- prototype._q = queries;
75
- prototype._b = buildFunction;
76
- prototype._n = throwAway.content.firstChild;
77
- return ComponentDefinition;
78
- }
79
-
80
- /**
81
- * Creates a new component definition.
82
- *
83
- * @param {*} base - a component definition to inherit from - defaults to Component.
84
- * @returns the newly created component definition function.
85
- */
86
- function _createConstructor(base) {
87
- const ComponentDefinition = function () {
88
- base.call(this);
89
- };
90
- const proto = Object.create(base && base.prototype, {
91
- constructor: {
92
- value: ComponentDefinition,
93
- writable: true,
94
- configurable: true
95
- }
96
- });
97
- ComponentDefinition.prototype = proto;
98
-
99
- // methods lets us assign to prototype without replacing it.
100
- Object.defineProperty(ComponentDefinition, "methods", {
101
- set: function (value) {
102
- Object.assign(proto, value);
103
- },
104
- get: function () {
105
- return proto;
106
- }
107
- });
108
-
109
- // Set up stubs
110
- ComponentDefinition.stubs = {};
111
- Object.assign(ComponentDefinition.stubs, base.stubs);
112
-
113
- // Helper to access base prototype.
114
- proto.base = Component.prototype;
115
- return ComponentDefinition;
116
- }