wallace 0.4.0 → 0.6.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/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Wallace
2
+
3
+ This package contains the library for the [Wallace](https://github.com/wallace-js/wallace) framework, which you import into your source files:
4
+
5
+ ```jsx
6
+ import { mount } from "wallace";
7
+
8
+ const MyComponent = () => <div>Hello world</div>;
9
+
10
+ mount("main", Component);
11
+ ```
12
+
13
+ It requires the [babel-plugin-wallace](https://www.npmjs.com/package/babel-plugin-wallace) to work, which is a dependency of this package, always at the same version.
14
+
15
+ Although you can install these packages with:
16
+
17
+ ```
18
+ npm i wallace -D
19
+ ```
20
+
21
+ You are better off creating an empty project with:
22
+
23
+ ```
24
+ npx create-wallace-app
25
+ ```
26
+
27
+ As that sets up your babel and webpack configurations for you.
28
+
29
+ For more detailed documentation see the [Wallace repository on github](https://github.com/wallace-js/wallace).
package/lib/component.js CHANGED
@@ -1,154 +1,170 @@
1
+ const throwAway = document.createElement("template");
1
2
  const NO_LOOKUP = "__";
2
3
 
3
- /**
4
- * The base component.
5
- */
6
- export function Component() {
7
- this.ctrl = undefined; // The controller.
8
- this.props = undefined; // The props passed to the component. May be changed.
9
- this.el = null; // the element - will be set during build.
10
- this.ref = {}; // user set references to elements or components.
11
- // Internal state objects
12
- this._e = {}; // The dynamic elements in the DOM.
13
- this._s = []; // A stash for misc objects.
14
- this._p = {}; // The previous values for watches to compare against.
15
- this._r = {}; // The current values read during an update.
16
- }
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
+ },
17
15
 
18
- Component.stubs = {};
16
+ /**
17
+ * Updates the DOM and renders nested components.
18
+ */
19
+ update: function () {
20
+ this._u(0, this._l);
21
+ },
19
22
 
20
- 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;
21
35
 
22
- Object.defineProperty(proto, "hidden", {
23
- set: function (value) {
24
- this.el.hidden = value;
25
- }
26
- });
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)
27
44
 
28
- proto._gs = function (name) {
29
- return this.constructor.stubs[name];
30
- };
45
+ The callback is an object whose key is the lookup and value is a function which
46
+ applies the effect.
31
47
 
32
- /**
33
- * Reads a query during update, returns an array with (oldValue, newValue, changed)
34
- * and saves the old value. Must reset this._r before each run.
35
- */
36
- proto._rq = function (props, key) {
37
- const run = this._r;
38
- if (run[key] === undefined) {
39
- let oldValue = this._p[key];
40
- const newValue = this._q[key](props, this);
41
- this._p[key] = newValue;
42
- const rtn = [newValue, oldValue, newValue !== oldValue];
43
- run[key] = rtn;
44
- return rtn;
45
- }
46
- return run[key];
47
- };
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
48
53
 
49
- /**
50
- * Applies the callbacks.
51
- */
52
- proto._ac = function (props, element, callbacks) {
53
- for (let key in callbacks) {
54
- let callback = callbacks[key];
55
- if (key === NO_LOOKUP) {
56
- callback(element, props, this);
57
- } else {
58
- const result = this._rq(props, key);
59
- if (result[2]) {
60
- 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++;
61
109
  }
62
110
  }
63
111
  }
64
112
  };
65
113
 
66
- /**
67
- * The render function that gets called by parent components.
68
- */
69
- proto.render = function (props, ctrl) {
70
- this.props = props;
71
- this.ctrl = ctrl;
72
- this.update();
73
- };
114
+ Object.defineProperty(ComponentBase.prototype, "hidden", {
115
+ set: function (value) {
116
+ this.el.hidden = value;
117
+ }
118
+ });
74
119
 
75
120
  /**
76
- * Updates the DOM.
77
- * 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.
78
125
  */
79
- proto.update = function () {
80
- let i = 0,
81
- watch,
82
- element,
83
- parent,
84
- displayToggle,
85
- detacher,
86
- lookupTrue,
87
- shouldBeVisible,
88
- detachedElements,
89
- detachedElement,
90
- index,
91
- adjustedIndex,
92
- thisElement;
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
+ };
93
138
 
94
- const watches = this._w;
95
- const props = this.props;
96
- const il = watches.length;
97
- this._r = {};
98
- /*
99
- Watches is an array of objects with keys:
100
- e: the element reference (string)
101
- c: the callbacks (object)
102
- ?v: visibility toggle (object)
103
-
104
- The display toggle has keys:
105
- q: the query key in lookup
106
- s: the number of watches to skip as their node is underneath
107
- r: reversed
108
- ?d: detacher
109
-
110
- The detacher has keys:
111
- i: the initial element index
112
- s: the stash key of the detacher (plain object)
113
- e: the parent element key
114
- */
115
- while (i < il) {
116
- watch = watches[i];
117
- element = this._e[watch.e];
118
- displayToggle = watch.v;
119
- i++;
120
- shouldBeVisible = true;
121
- if (displayToggle) {
122
- lookupTrue = !!this._rq(props, displayToggle.q)[0];
123
- shouldBeVisible = displayToggle.r ? lookupTrue : !lookupTrue;
124
- detacher = displayToggle.d;
125
- if (detacher) {
126
- index = detacher.i;
127
- parent = this._e[detacher.e];
128
- detachedElements = this._s[detacher.s];
129
- detachedElement = detachedElements[index];
130
- if (shouldBeVisible && detachedElement) {
131
- adjustedIndex =
132
- index -
133
- Object.keys(detachedElements).filter(function (k) {
134
- return k < index && detachedElements[k];
135
- }).length;
136
- parent.insertBefore(detachedElement, parent.childNodes[adjustedIndex]);
137
- detachedElements[index] = null;
138
- } else if (!shouldBeVisible && !detachedElement) {
139
- thisElement = this._e[watch.e];
140
- parent.removeChild(thisElement);
141
- detachedElements[index] = thisElement;
142
- }
143
- } else {
144
- element.hidden = !shouldBeVisible;
145
- }
146
- if (!shouldBeVisible) {
147
- i += displayToggle.s;
148
- }
139
+ const proto = (Component.prototype = Object.create(baseComponent.prototype, {
140
+ constructor: {
141
+ value: Component
149
142
  }
150
- if (shouldBeVisible) {
151
- 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;
152
152
  }
153
- }
154
- };
153
+ });
154
+
155
+ Component.stubs = {} && baseComponent.stubs;
156
+ return Component;
157
+ }
158
+
159
+ export function defineComponent(html, watches, queries, buildFunction, inheritFrom) {
160
+ const ComponentDefinition = createConstructor(inheritFrom || ComponentBase);
161
+ const proto = ComponentDefinition.prototype;
162
+ throwAway.innerHTML = html;
163
+ //Ensure these do not clash with fields on the component itself.
164
+ proto._w = watches;
165
+ proto._q = queries;
166
+ proto._b = buildFunction;
167
+ proto._n = throwAway.content.firstChild;
168
+ proto.base = ComponentBase.prototype;
169
+ return ComponentDefinition;
170
+ }
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,31 +1,10 @@
1
- import { mount, watch } from "./utils";
2
- import { Component } from "./component";
3
- import { KeyedRepeater, SequentialRepeater } from "./repeaters";
4
- import {
5
- extendComponent,
6
- defineComponent,
7
- findElement,
8
- getKeyedRepeater,
9
- getSequentialRepeater,
10
- onEvent,
11
- nestComponent,
12
- saveRef,
13
- stashMisc
14
- } from "./initCalls";
15
-
16
- export {
17
- Component,
18
- defineComponent,
19
- extendComponent,
20
- findElement,
21
- getKeyedRepeater,
22
- getSequentialRepeater,
23
- KeyedRepeater,
24
- mount,
25
- nestComponent,
26
- onEvent,
27
- saveRef,
28
- SequentialRepeater,
29
- stashMisc,
30
- watch
31
- };
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 { getStub } from "./stubs";
9
+ export { findElement, onEvent, stashMisc } from "./utils";
10
+ 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,4 +1,4 @@
1
- import { buildComponent } from "./utils";
1
+ import { trimChildren } from "../utils";
2
2
 
3
3
  /*
4
4
  * Gets a component from the pool.
@@ -8,28 +8,13 @@ function getComponent(pool, componentDefinition, ctrl, key, props) {
8
8
  if (pool.hasOwnProperty(key)) {
9
9
  component = pool[key];
10
10
  } else {
11
- component = buildComponent(componentDefinition);
11
+ component = new componentDefinition();
12
12
  pool[key] = component;
13
13
  }
14
14
  component.render(props, ctrl);
15
15
  return component;
16
16
  }
17
17
 
18
- /**
19
- * Trims the unwanted child elements from the end.
20
- *
21
- * @param {Node} e
22
- * @param {Array} childNodes
23
- * @param {Int} itemsLength
24
- */
25
- function trimChildren(e, childNodes, itemsLength) {
26
- let lastIndex = childNodes.length - 1;
27
- let keepIndex = itemsLength - 1;
28
- for (let i = lastIndex; i > keepIndex; i--) {
29
- e.removeChild(childNodes[i]);
30
- }
31
- }
32
-
33
18
  /**
34
19
  * Pulls an item forward in an array, to replicate insertBefore.
35
20
  * @param {Array} arr
@@ -103,48 +88,3 @@ proto.patch = function (e, items, ctrl) {
103
88
  this.keys = newKeys;
104
89
  trimChildren(e, childNodes, itemsLength);
105
90
  };
106
-
107
- /**
108
- * Repeats nested components, yielding from its pool sequentially.
109
- *
110
- * @param {componentDefinition} componentDefinition - The class ComponentDefinition to create.
111
- */
112
- export function SequentialRepeater(componentDefinition) {
113
- this.def = componentDefinition;
114
- this.pool = []; // pool of component instances
115
- this.count = 0; // Child element count
116
- }
117
-
118
- /**
119
- * Updates the element's childNodes to match the items.
120
- * Performance is important.
121
- *
122
- * @param {DOMElement} e - The DOM element to patch.
123
- * @param {Array} items - Array of items which will be passed as props.
124
- * @param {any} ctrl - The parent item's controller.
125
- */
126
- SequentialRepeater.prototype.patch = function (e, items, ctrl) {
127
- const pool = this.pool;
128
- const componentDefinition = this.def;
129
- const childNodes = e.childNodes;
130
- const itemsLength = items.length;
131
- let component,
132
- poolCount = pool.length,
133
- childElementCount = this.count;
134
-
135
- for (let i = 0; i < itemsLength; i++) {
136
- if (i < poolCount) {
137
- component = pool[i];
138
- } else {
139
- component = buildComponent(componentDefinition);
140
- pool.push(component);
141
- poolCount++;
142
- }
143
- component.render(items[i], ctrl);
144
- if (i >= childElementCount) {
145
- e.appendChild(component.el);
146
- }
147
- }
148
- this.count = itemsLength;
149
- trimChildren(e, childNodes, itemsLength);
150
- };
@@ -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/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
@@ -16,7 +16,7 @@
16
16
  6. Inheritance
17
17
  7. Stubs
18
18
  8. TypeScript
19
- 9. Utility functions
19
+ 9. Helpers
20
20
 
21
21
  For more detailed documentation go to https://github.com/wallace-js/wallace
22
22
 
@@ -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
 
@@ -71,7 +73,7 @@ The arguments are:
71
73
  3. props for the element (optional)
72
74
  4. controller (optional)
73
75
 
74
- `mount` returns the component instance, allowing you to call it methods:
76
+ `mount` returns the component instance, allowing you to call its methods:
75
77
 
76
78
  ```tsx
77
79
  root.update();
@@ -100,11 +102,11 @@ places.
100
102
 
101
103
  #### Overriding
102
104
 
103
- You can override these methods, and add new ones using `methods` directly on the
105
+ You can override these methods, and add new ones using `methods` property of the
104
106
  component definition:
105
107
 
106
108
  ```tsx
107
- MyComponent.methods({
109
+ MyComponent.methods = {
108
110
  render(props) {
109
111
  this.ctrl = new MyController(this, props);
110
112
  this.update();
@@ -112,7 +114,7 @@ MyComponent.methods({
112
114
  getName() {
113
115
  return 'wallace';
114
116
  }
115
- });
117
+ };
116
118
  ```
117
119
 
118
120
  This has the same effect as setting them on the prototype:
@@ -124,11 +126,11 @@ MyComponent.prototype.render = function () {};
124
126
  You can use `this.base` to access methods on the base `Component` class:
125
127
 
126
128
  ```tsx
127
- MyComponent.methods({
129
+ MyComponent.methods = {
128
130
  render(props) {
129
131
  this.base.render.call(this, props, ctrl);
130
132
  }
131
- });
133
+ };
132
134
  ```
133
135
 
134
136
  Note that `base` is not the same as `super` in classes which access the lowest override.
@@ -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`.
@@ -201,27 +202,16 @@ Notes:
201
202
 
202
203
  ## 4. Directives
203
204
 
204
- Directives are attributes with special behaviours, as listed below. Each has more
205
- detailed information on its JSDoc, which should display as a tool tip\* when you hover
206
- over it in your IDE.
205
+ Directives are attributes with special behaviours.
207
206
 
208
- You can also display this list by hovering over any JSX element, like `div`.
207
+ You can see the list of available directives by hovering over any JSX element, like
208
+ a `div`
209
209
 
210
- - `apply` runs a callback to modify an element.
211
- - `bind` updates a value when an input is changed.
212
- - `class:xyz` defines a set of classes to be toggled.
213
- - `hide` sets an element or component's hidden property.
214
- - `html` Set the element's `innnerHTML` property.
215
- - `if` excludes an element from the DOM.
216
- - `on[EventName]` creates an event handler (note the code is copied)
217
- - `props` specifes props for a nested or repeated component, in which case it must be
218
- an array.
219
- - `ref` saves a reference to an element or nested component.
220
- - `show` sets and element or component's hidden property.
221
- - `style:xyz` sets a specific style property.
222
- - `toggle:xyz` toggles `xyz` as defined by `class:xyz` on same element, or class `xyz`.
210
+ You will get more details by hovering on the directive itself, but unfortunetely the
211
+ tool tip won't display when you use a qualifier, like `class:danger`. To see it you can
212
+ temporarily change it to something `class x:danger`.
223
213
 
224
- \* The tool tip won't display when you use a qualifier, like `class:danger`.
214
+ You can define your own directives in your babel config.
225
215
 
226
216
  ## 5. Controllers
227
217
 
@@ -266,12 +256,12 @@ const TaskList = (_, {ctrl}) => (
266
256
  </div>
267
257
  );
268
258
 
269
- TaskList.methods({
259
+ TaskList.methods = {
270
260
  render(_, ctrl) {
271
261
  this.ctrl = new TaskController(this, ctrl);
272
262
  this.update();
273
263
  }
274
- });
264
+ };
275
265
  ```
276
266
 
277
267
  ## 6. Inheritance
@@ -392,12 +382,12 @@ const Task: Uses<null, null, TaskMethods> = (_, { self }) => (
392
382
  <div>{self.getName()}</div>
393
383
  ));
394
384
 
395
- Task.methods({
385
+ Task.methods = {
396
386
  getName() { return 'wallace' },
397
387
  render(props, ctrl) { // types are already known
398
388
  this.props = { ...props, notallowed: 1 }; // type error
399
389
  }
400
- });
390
+ };
401
391
  ```
402
392
 
403
393
  The type will pass into the object passed into `methods` so it recognises custom methods
@@ -445,7 +435,7 @@ Wallace defines some other types you may use:
445
435
  constructor, not a class)
446
436
  - `ComponentInstance<Props, Controller, Methods>` - a component instance.
447
437
 
448
- ## Utility functions
438
+ ## 9. Helpers
449
439
 
450
440
  Each of these has their own JSDoc, we just lsit them here.
451
441
 
@@ -469,11 +459,24 @@ mount("elementId", MyComponent, props, ctrl);
469
459
 
470
460
  ### watch
471
461
 
472
- Returns a Proxy of an object which calls `callback` when keys are set:
462
+ Returns a Proxy of an object which calls `callback` when it, or its nested objects are
463
+ modified:
473
464
 
474
465
  ```
475
- watch(obj, () => console.log('obj modified);
466
+ const watchedObj = watch([], () => console.log('obj modified));
467
+ watchedObj[0] = 'foo; // Calls callback.
476
468
  ```
469
+
470
+ ### protect
471
+
472
+ Returns a Proxy of an object which throws an error if it, or its nested objects are
473
+ modified.
474
+
475
+ ```
476
+ const protectedObj = protect([]);
477
+ watchedObj[0] = 'foo'; // throws error.
478
+ ```
479
+
477
480
  ---
478
481
  Report any issues to https://github.com/wallace-js/wallace (and please give it a ★)
479
482
 
@@ -493,11 +496,10 @@ declare module "wallace" {
493
496
  props: Props,
494
497
  xargs?: {
495
498
  ctrl: Controller;
499
+ props: Props;
496
500
  self: ComponentInstance<Props, Controller, Methods>;
497
501
  event: Event;
498
- ev: Event;
499
502
  element: HTMLElement;
500
- el: HTMLElement;
501
503
  }
502
504
  ): JSX.Element;
503
505
  nest?({
@@ -518,10 +520,8 @@ declare module "wallace" {
518
520
  show?: boolean;
519
521
  hide?: boolean;
520
522
  }): JSX.Element;
521
- methods?(
522
- object: ComponenMethods<Props, Controller> &
523
- ThisType<ComponentInstance<Props, Controller, Methods>>
524
- ): void;
523
+ methods?: ComponenMethods<Props, Controller> &
524
+ ThisType<ComponentInstance<Props, Controller, Methods>>;
525
525
  readonly prototype?: ComponenMethods<Props, Controller> &
526
526
  ThisType<ComponentInstance<Props, Controller, Methods>>;
527
527
  // Methods will not be available on nested component, so omit.
@@ -555,6 +555,11 @@ declare module "wallace" {
555
555
  Methods extends object = {}
556
556
  > = ComponentFunction<Props, Controller, Methods>;
557
557
 
558
+ export interface Ref {
559
+ node: HTMLElement | ComponentInstance;
560
+ update(): void;
561
+ }
562
+
558
563
  /**
559
564
  * The type for a component instance.
560
565
  */
@@ -566,7 +571,7 @@ declare module "wallace" {
566
571
  el: HTMLElement;
567
572
  props: Props;
568
573
  ctrl: Controller;
569
- ref: { [key: string]: HTMLElement };
574
+ refs: { [key: string]: Ref };
570
575
  base: Component<Props, Controller>;
571
576
  } & Component<Props, Controller> &
572
577
  Methods;
@@ -643,8 +648,21 @@ declare module "wallace" {
643
648
  ): ComponentInstance<Props, Controller, Methods>;
644
649
 
645
650
  /**
646
- * Returns a Proxy of an object which calls `callback` when keys are set, and this
647
- * extends to nested objects:
651
+ * Returns a Proxy of an object which throws an error when it, or its nested objects
652
+ * are modified:
653
+ *
654
+ * ```js
655
+ * const protectedObj = protect([]);
656
+ * watchedObj[0] = 'foo'; // throws error.
657
+ * ```
658
+ */
659
+ export function protect<T>(target: T): T;
660
+
661
+ type WatchCallback = (target: any, key: string, value: any) => void;
662
+
663
+ /**
664
+ * Returns a Proxy of an object which calls `callback` when it, or its nested objects
665
+ * are modified:
648
666
  *
649
667
  * ```js
650
668
  * ar = watch([], callback)
@@ -656,18 +674,23 @@ declare module "wallace" {
656
674
  * obj.y = {}
657
675
  * obj.y.z = 1000
658
676
  * ```
659
- * The callback does not indicate the data has changed, only that a key was set.
677
+ * The callback accepts parameters:
678
+ *
679
+ * - `target` - the object which is being modified.
680
+ * - `key` - the key being set.
681
+ * - `value` - the value it is being set to.
682
+ *
683
+ * The callback is called after the modification has occured.
660
684
  *
661
685
  * Some methods like `Array.push` set the index and then the length immediately after,
662
686
  * so we use a timeout period to avoid calling the callback twice for what is really a
663
687
  * single operation.
664
688
  *
665
689
  * @param {*} target - Any object, including arrays.
666
- * @param {*} timeout - Any value in ms. Defaults to 100.
667
690
  * @param {*} callback - A callback function.
668
691
  * @returns a Proxy of the object.
669
692
  */
670
- export function watch<T>(target: T, callback: CallableFunction, timeout?: number): T;
693
+ export function watch<T>(target: T, callback: WatchCallback): T;
671
694
  }
672
695
 
673
696
  type MustBeExpression = Exclude<any, string>;
@@ -739,8 +762,6 @@ interface DirectiveAttributes extends AllDomEvents {
739
762
  * );
740
763
  * ```
741
764
  *
742
- * Unfortunately you lose the tooltip in that format.
743
- *
744
765
  * Note that destructured props are converted to member expressions, so these examples
745
766
  * work even though it looks like you're setting a local variable.
746
767
  */
@@ -760,11 +781,34 @@ interface DirectiveAttributes extends AllDomEvents {
760
781
  * ```
761
782
  * <div class:danger="danger red" toggle:danger={expr}></div>
762
783
  * ```
763
- *
764
- * Unfortunately you lose the tooltip in that format.
765
784
  */
766
785
  class?: any;
767
786
 
787
+ /**
788
+ * ## Wallace directive: css
789
+ *
790
+ * Shorthand for `fixed:class`:
791
+ *
792
+ * ```
793
+ * <div css={foo} ></div>
794
+ * ```
795
+ */
796
+ css?: string;
797
+
798
+ /**
799
+ * ## Wallace directive: fixed
800
+ *
801
+ * Sets the value of an attribute from an expression at point of component definition,
802
+ * as such the expression may not access props or xargs. See also `css` directive.
803
+ *
804
+ * Requires a qualifer, which is the name of the attribute to set.
805
+ *
806
+ * ```
807
+ * <div fixed:class={foo} ></div>
808
+ * ```
809
+ */
810
+ fixed?: string;
811
+
768
812
  /** ## Wallace directive: hide
769
813
  *
770
814
  * Set the element's `hidden` property and if true, does not render dynamic elements
@@ -806,19 +850,25 @@ interface DirectiveAttributes extends AllDomEvents {
806
850
  /**
807
851
  * ## Wallace directive: ref
808
852
  *
809
- * Saves a reference to the element on the component, allowing it to be accessed.
853
+ * Saves a reference to a node allowing you to:
854
+ *
855
+ * 1. Access the element or component as `ref.node`
856
+ * 2. Update the all nested elements using `ref.update()`.
810
857
  *
811
858
  * ```
812
- * <div ref:title></div>
859
+ * <div ref:title>
860
+ * {name}
861
+ * </div>
813
862
  * ```
814
863
  *
815
864
  * ```
816
- * component.ref.title.textContent = 'hello';
865
+ * component.refs.title.update();
866
+ * component.refs.title.node.textContent = 'hello';
817
867
  * ```
818
868
  *
819
869
  * Requires a qualifier, but you lose the tooltip in that format.
820
870
  */
821
- ref?: string;
871
+ ref?: null;
822
872
 
823
873
  /** ## Wallace directive: show
824
874
  *
@@ -876,25 +926,30 @@ declare namespace JSX {
876
926
  * <MyComponent.nest props={singleProps} />
877
927
  * <MyComponent.repeat items={arrayOfProps} />
878
928
  * ```
879
- * But note that repeat must not have siblings.
929
+ * Note that repeated components must not have siblings.
880
930
  *
881
931
  * Available Wallace directives:
882
932
  *
883
933
  * - `apply` runs a callback to modify an element.
884
934
  * - `bind` updates a value when an input is changed.
885
935
  * - `class:xyz` defines a set of classes to be toggled.
936
+ * - `css` shorthand for `fixed:class`.
937
+ * - `fixed:xyz` sets a attribute from an expression at definition.
886
938
  * - `hide` sets an element or component's hidden property.
887
939
  * - `html` Set the element's `innnerHTML` property.
888
940
  * - `if` excludes an element from the DOM.
889
941
  * - `items` set items for repeated component, must be an array of props.
890
942
  * - `on[EventName]` creates an event handler (note the code is copied)
891
943
  * - `props` specifes props for a nested components.
892
- * - `ref` saves a reference to an element or nested component.
944
+ * - `ref:xyz` saves a reference to node, allowing partial updates or access to element/component.
893
945
  * - `show` sets and element or component's hidden property.
894
946
  * - `style:xyz` sets a specific style property.
895
- * - `toggle:xyz` toggles `xyz` as defined by `class:xyz` on same element, or class `xyz`.
947
+ * - `toggle:xyz` toggles `xyz` as defined by `class:xyz` on same element, or class
948
+ * `xyz`.
896
949
  *
897
- * Note the tool tip won't display when you use a qualifier, like `class:danger`.
950
+ * You will get more details by hovering on the directive itself, but unfortunetely
951
+ * the tool tip won't display when you use a qualifier, like `class:danger`. To see
952
+ * it you cantemporarily change it to something `class x:danger`.
898
953
  */
899
954
  [elemName: string]: DirectiveAttributes & Record<string, any>;
900
955
  }
package/lib/utils.js CHANGED
@@ -1,73 +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 = buildComponent(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;
23
- }
24
-
25
9
  /**
26
- * Builds a component's initial DOM.
10
+ * Trims the unwanted child elements from the end.
11
+ *
12
+ * @param {Node} e
13
+ * @param {Array} childNodes
14
+ * @param {Int} itemsLength
27
15
  */
28
- export function buildComponent(componentDefinition) {
29
- // TODO: add a dev warning here:
30
- // if "componentDefinition is not a constructor" then we're probably missing a stub.
31
- const component = new componentDefinition();
32
- const proto = componentDefinition.prototype;
33
- const dom = proto._n.cloneNode(true);
34
- component.el = dom;
35
- proto._b(component, dom);
36
- return component;
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
+ }
37
22
  }
38
23
 
39
24
  /**
40
- * See types for docs. Set grace to 0 for testing.
25
+ * Stash something on the component. Returns the element.
26
+ * The generated code is expected to keep track of the position.
41
27
  */
42
- export function watch(target, callback, grace) {
43
- let active = false;
44
- if (grace === undefined) grace = 100;
45
- const handler = {
46
- get(target, key) {
47
- if (key == "isProxy") return true;
48
- const prop = target[key];
49
- if (typeof prop == "undefined") return;
50
- // set value as proxy if object
51
- if (!prop.isProxy && typeof prop === "object")
52
- target[key] = new Proxy(prop, handler);
53
- return target[key];
54
- },
28
+ export function stashMisc(element, stash, object) {
29
+ stash.push(object);
30
+ return element;
31
+ }
55
32
 
56
- set(target, key, value) {
57
- target[key] = value;
58
- if (grace) {
59
- if (!active) {
60
- setTimeout(() => {
61
- active = false;
62
- }, grace);
63
- active = true;
64
- callback();
65
- }
66
- } else {
67
- callback();
68
- }
69
- return true;
70
- }
71
- };
72
- return new Proxy(target, handler);
33
+ export function onEvent(element, eventName, callback) {
34
+ element.addEventListener(eventName, callback);
35
+ return element;
73
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.4.0",
3
+ "version": "0.6.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.4.0",
17
+ "babel-plugin-wallace": "^0.6.0",
17
18
  "browserify": "^17.0.1"
18
19
  },
19
- "gitHead": "286a00b51777c4b7fed9a778b93e9727ed905e2a"
20
+ "gitHead": "451efab81055751fbda64eb80252ec150460e375"
20
21
  }
package/lib/initCalls.js DELETED
@@ -1,108 +0,0 @@
1
- /**
2
- * Everything in here is used by or modified by the Babel plugin.
3
- */
4
- import { Component } from "./component";
5
- import { buildComponent, replaceNode } from "./utils";
6
- import { KeyedRepeater, SequentialRepeater } from "./repeaters";
7
-
8
- const throwAway = document.createElement("template");
9
-
10
- /**
11
- * A utility function that has to be in here because it needs _createConstructor and
12
- * we'd otherwise get cirular inmports.
13
- *
14
- * Calls to this function which provide the 2nd argument:
15
- *
16
- * const Foo = extendComponent(Bar, () => <div></div>))
17
- *
18
- * Are modified by the Babel plugin to become this:
19
- *
20
- * const Foo = defineComponent(,,,,Bar);
21
- *
22
- * So it should never be called with 2nd arg in real life.
23
- */
24
- export function extendComponent(base, componentDef) {
25
- // This function call will have been replaced if 2nd arg is a valid component func.
26
- // and therefor we would not receive it.
27
- if (componentDef)
28
- throw new Error("2nd arg to extendComponent must be a JSX arrow function");
29
- return _createConstructor(base);
30
- }
31
-
32
- /*
33
- Everything after this is used by code generated by the Babel plugin.
34
- */
35
-
36
- export function findElement(rootElement, path) {
37
- return path.reduce((acc, index) => acc.childNodes[index], rootElement);
38
- }
39
-
40
- export function nestComponent(rootElement, path, cls) {
41
- const el = findElement(rootElement, path),
42
- child = buildComponent(cls);
43
- replaceNode(el, child.el);
44
- return child;
45
- }
46
-
47
- /**
48
- * Saves a reference to element or nested component. Returns the element.
49
- */
50
- export function saveRef(element, component, name) {
51
- component.ref[name] = element;
52
- return element;
53
- }
54
-
55
- /**
56
- * Stash something on the component. Returns the element.
57
- * The generated code is expected to keep track of the position.
58
- */
59
- export function stashMisc(element, component, object) {
60
- component._s.push(object);
61
- return element;
62
- }
63
-
64
- export function onEvent(element, eventName, callback) {
65
- element.addEventListener(eventName, callback);
66
- return element;
67
- }
68
-
69
- export function getKeyedRepeater(cls, keyFn) {
70
- return new KeyedRepeater(cls, keyFn);
71
- }
72
-
73
- export function getSequentialRepeater(cls) {
74
- return new SequentialRepeater(cls);
75
- }
76
-
77
- export function defineComponent(html, watches, queries, buildFunction, inheritFrom) {
78
- const ComponentDefinition = _createConstructor(inheritFrom || Component);
79
- const prototype = ComponentDefinition.prototype;
80
- throwAway.innerHTML = html;
81
- //Ensure these do not clash with fields on the component itself.
82
- prototype._w = watches;
83
- prototype._q = queries;
84
- prototype._b = buildFunction;
85
- prototype._n = throwAway.content.firstChild;
86
- return ComponentDefinition;
87
- }
88
-
89
- function _createConstructor(base) {
90
- const ComponentDefinition = function () {
91
- base.call(this);
92
- };
93
- ComponentDefinition.stubs = {};
94
- Object.assign(ComponentDefinition.stubs, base.stubs);
95
- // This is a helper function for the user.
96
- ComponentDefinition.methods = function (obj) {
97
- Object.assign(ComponentDefinition.prototype, obj);
98
- };
99
- ComponentDefinition.prototype = Object.create(base && base.prototype, {
100
- constructor: {
101
- value: ComponentDefinition,
102
- writable: true,
103
- configurable: true
104
- }
105
- });
106
- ComponentDefinition.prototype.base = Component.prototype;
107
- return ComponentDefinition;
108
- }