diff --git a/README.md b/README.md
index 3f6fdc6..d849d5d 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,13 @@
## Features
- **Component-Based**: Mosaic components are reusable pieces of code that each keep track of their own state (referred to as "data"), actions, lifecycle functions, and more.
+- **Observable Data Pattern**: Mosaic uses Observables to keep track of changes to a component's data. This means
+that there is no need to call "setState" or anything like that, instead just change the data directly.
- **Virtual DOM**: The use of a virtual dom makes updating web apps very fast.
-- **Written in JSX**: You can use jsx or the "h" function that comes with Mosaic to write a component's view.
+- **JSX**: You can use jsx or the "h" function that comes with Mosaic to write a component's view.
- **Easy to Learn**: The syntax and structure of Mosaic components is meant to make it easy to learn so that it does not require a lot of setup to start using.
-## Usage
+## Installation
The easiest way to use Mosaic is to first install the npm package by using:
```shell
npm install --save @authman2/mosaic
@@ -26,9 +28,7 @@ You will also need to create a .babelrc file so that you can transpile JSX into
```js
{
"presets": ["env"],
- "plugins": [["babel-plugin-transform-react-jsx", {
- "pragma": "h"
- }]]
+ "plugins": [["babel-plugin-transform-react-jsx", { "pragma": "h" }]]
}
```
Now you are ready to use Mosaic!
@@ -63,11 +63,9 @@ const NavButton = new Mosaic({
label: "Default Label",
buttonTitle: "Default Button Title"
},
- actions: function(self) {
- return {
- print: function() {
- console.log(self.data.buttonTitle);
- }
+ actions: {
+ print: function() {
+ console.log(this.data.buttonTitle);
}
},
view: function() {
@@ -75,7 +73,7 @@ const NavButton = new Mosaic({
return (
- );
- },
-
- created: function() {
- // Only put a timer on the second instance.
- if(this.data.instance === 1) {
- setInterval(() => {
- const n = Math.floor(Math.random() * 100);
- this.setData({ count: n });
- }, 1000);
- }
- },
-});
\ No newline at end of file
diff --git a/package.json b/package.json
index f38f6a3..7b7f299 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@authman2/mosaic",
- "version": "0.1.4",
+ "version": "0.1.5",
"description": "A front-end JavaScript library for creating user interfaces",
"main": "src/index.js",
"scripts": {
@@ -23,5 +23,6 @@
"devDependencies": {
"babel-core": "^6.26.3",
"babel-plugin-transform-react-jsx": "^6.24.1"
- }
+ },
+ "dependencies": {}
}
diff --git a/src/index.js b/src/index.js
index eb5f6cc..7e1c330 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,146 +1,125 @@
-const { createElement } = require('./vdom/createElement');
-const { render } = require('./vdom/render');
-const { mount } = require('./vdom/mount');
-const { diff } = require('./vdom/diff');
-const { validateMosaicChildren, randomID } = require('./util');
+import createElement from './vdom/createElement';
+import render from './vdom/render';
+import patch from './vdom/patch';
+import Observable from './observable';
+import { isHTMLElement, findInvalidOptions } from './validations';
-/** The attributes that go along with a Mosaic component. */
+/** The configuration options for a Mosaic component. */
const MosaicOptions = {
- /** The DOM element to use to wrap around a Mosaic component. */
- element: Element || String,
+ /** The HTML element to inject this Mosaic component into. */
+ element: HTMLElement,
- /** The state of this component. */
- data: Object.create(null),
+ /** The state of this component. */
+ data: Object,
- /** The actions that modify this component's data. */
- actions: Function,
+ /** The view that will be rendered on the screen. */
+ view: Function,
- /** The Mosaic children that are nested components of this one. */
- components: Object,
+ /** The actions that can be used on this Mosaic component. */
+ actions: Object,
- /** The view that this component will take on. */
- view: Function,
+ /** The function to run when this component is created and injected into the DOM. */
+ created: Function,
- /** The function to run when this component gets created. */
- created: Function,
+ /** The function to run when this component is about to update its data. */
+ willUpdate: Function,
- /** The function to run when this component is about to update. */
- willUpdate: Function,
+ /** The function to run after this component has been updated. */
+ updated: Function,
+
+ /** The function that runs just before this component gets removed from the DOM. */
+ willDestroy: Function
+};
- /** The function to run when this component gets updated. */
- updated: Function
-}
-/** Creates a new Mosaic component.
-* @param {MosaicOptions} options The configuration options for a Mosaic component. */
+/** Creates a new Mosaic component with configuration options.
+* @param {MosaicOptions} options The configuration options for this Mosaic. */
const Mosaic = function(options) {
- this.key = randomID();
- this.data = options.data || {};
- this.actions = options.actions ? options.actions(this) : ((comp) => {});
- this.view = options.view || ((comp) => {});
- this.created = options.created;
- this.willUpdate = options.willUpdate;
- this.updated = options.updated;
- this.localParent = options.parent || null; // This is the parent directly above this component.
-
- // This is either the root dom node or a wrapper around a component.
- this.$element = typeof options.element === 'string' ? document.createElement(options.element) : options.element;
-
- // Configure the references and the child Mosaic components.
- // Then "place" then into this Mosaic component for easy access.
- let componentStructures = validateMosaicChildren(options.components) ? options.components : {};
- for(var key in componentStructures) {
- this[key] = componentStructures[key].type.copy(this);
- this[key].data = Object.assign({}, this[key].data, componentStructures[key].data);
- if(this[key].created) this[key].created();
- }
-
-
-
- /** Returns a copy of this Mosaic component.
- * @param {Mosaic} parent (Optional) The local parent of this copy.
- * @returns {Mosaic} A copy of this Mosaic component. */
- this.copy = function(parent = null) {
- let cpy = new Mosaic(Object.assign({}, options, {
- parent: parent,
- // key: this.key,
- }));
- cpy.$element = this.$element;
- return cpy;
- }
+ let invalids = findInvalidOptions(options);
+ if(invalids !== undefined) throw new Error(invalids);
+
+ this.element = options.element
+ this.view = options.view;
+ this.actions = options.actions;
+ this.created = options.created;
+ this.willUpdate = options.willUpdate;
+ this.updated = options.updated;
+ this.willDestroy = options.willDestroy;
+ this.data = new Observable(options.data || {}, (oldData) => {
+ if(this.willUpdate) this.willUpdate(oldData);
+ }, () => {
+ patch(this.element, this.view());
+ if(this.updated) this.updated();
+ });
+ this.__isMosaic = true;
+
+ // Bind all actions to this instance.
+ for(var i in this.actions) this.actions[i] = this.actions[i].bind(this);
+
+ return this;
}
-/** A child component that is of type Mosaic.
-* @param {Mosaic} type The Mosaic component to use as a blueprint.
-* @param {Object} data (Optional) Extra data to add to the component. */
-Mosaic.Child = function(type, data = {}) {
- return { type: type, data: data };
+/** "Paints" the Mosaic onto the page by injecting it into its base element. */
+Mosaic.prototype.paint = function() {
+ if(!this.element || !isHTMLElement(this.element)) {
+ throw new Error(`This Mosaic could not be painted because its element property is either not set
+ or is not a valid HTML element.`);
+ }
+ let htree = createElement(this);
+ render(htree, this.element, this);
}
-/** "Paints" your Mosaic onto the screen. Renders a real DOM element for this Mosaic component. */
-Mosaic.prototype.paint = function() {
- if(!this.$element) {
- console.error("This Mosaic could not be painted because no base element was supplied");
- return;
- } else if(typeof this.$element === 'string') {
- console.error("You cannot paint this Mosaic because it's base element must be an already existing DOM element");
- return;
- }
-
- const htree = createElement(this);
- const $element = render(htree);
- const $newRoot = mount($element, this.$element);
-
- this.$element = $newRoot;
- if(this.created) this.created();
-}
-/** Sets the data on this Mosaic component and triggers a rerender.
-* @param {Object} newData The new data to set on this Mosaic component. */
-Mosaic.prototype.setData = function(newData = {}) {
- // When you set data you have to account for two cases:
- // 1.) The component that you're setting the data on is the entry point so the entire
- // dom tree must be updated anyway. This is also fine because it will have a direct reference
- // to the root dom node that is definitely mounted on the page.
- //
- // 2.) The component having its data set is not the entry point but instead an "n-th level" child
- // of the entry point. It should have it's dom element already added to a parent. Look for it and
- // update it.
- if(this.willUpdate) this.willUpdate(this.data);
-
- // First make sure that you have an absolute parent.
- // Also do a bit of caching so the component can quickly find the absolute parent
- // on the next update.
- let lookAt = this;
- if(!this.absoluteParent) {
- while(lookAt.localParent !== null) {
- // console.log("Checking: ", lookAt);
- lookAt = lookAt.localParent;
- }
- this.absoluteParent = lookAt;
- } else {
- lookAt = this.absoluteParent;
- }
- // console.log("Absolute Parent: ", lookAt);
-
- // First things first, get a copy of the current HTree.
- let oldHTree = lookAt.view();
-
- // Then set the new data.
- this.data = Object.assign({}, this.data, newData);
-
- // Get a new HTree for the absolute parent.
- let newHTree = lookAt.view();
- // console.log(this, oldHTree, newHTree);
-
- // Find the patches that need to be done to update the DOM.
- let patches = diff(oldHTree, newHTree);
- lookAt.$element = patches(lookAt.$element);
-
- if(this.updated) this.updated();
+/** Static function for building a new instance of a Mosaic. Basically just takes a given VNode of a Mosaic
+ * and uses it as a blueprint for how to build reusable instances of that component.
+ */
+Mosaic.view = function(vnode, $parent = null) {
+ const props = Object.assign({}, vnode.props, { children: vnode.children });
+ const _data = Object.assign({}, vnode.type.data, props.data ? props.data : {});
+
+ // Render a new instance of this component.
+ if(typeof vnode.type === 'object' && vnode.type.__isMosaic) {
+ const options = {
+ element: vnode.type.element,
+ data: _data,
+ view: vnode.type.view,
+ actions: Object.assign({}, vnode.type.actions),
+ created: vnode.type.created,
+ willUpdate: vnode.type.willUpdate,
+ updated: vnode.type.updated,
+ willDestroy: vnode.type.willDestroy,
+ }
+ const instance = new Mosaic(options);
+ instance.element = render(instance.view(), $parent, instance);
+ instance.element.__mosaicInstance = instance;
+ instance.element.__mosaicKey = vnode.props.key;
+
+ if(instance.created) instance.created();
+ return instance.element;
+ } else {
+ return render(vnode.type.view(props), $parent);
+ }
}
+/** Static function for diffing and patching changes between instances of Mosaics. */
+Mosaic.patch = function($dom, vnode, $parent = $dom.parentNode) {
+ const props = Object.assign({}, vnode.props, { children: vnode.children });
+
+ if($dom.__mosaicInstance && $dom.__mosaicInstance.constructor === vnode.type) {
+ $dom.__mosaicInstance.props = props;
+ return patch($dom, $dom.__mosaicInstance.view(), $parent, $dom.__mosaicInstance);
+ }
+ else if(typeof vnode.type === 'object' && vnode.type.__isMosaic === true) {
+ const $ndom = Mosaic.view(vnode, $parent);
+ return $parent ? ($parent.replaceChild($ndom, $dom) && $ndom) : $ndom;
+ }
+ else if(typeof vnode.type !== 'object' || vnode.type.__isMosaic === false) {
+ return patch($dom, vnode.type.view(props), $parent, $dom.__mosaicInstance);
+ }
+}
+exports.h = createElement;
exports.Mosaic = Mosaic;
-exports.h = createElement;
\ No newline at end of file
+window.h = createElement;
+window.Mosaic = Mosaic;
\ No newline at end of file
diff --git a/src/observable.js b/src/observable.js
new file mode 100644
index 0000000..ba68b4f
--- /dev/null
+++ b/src/observable.js
@@ -0,0 +1,36 @@
+/** Basically an object that can perform a certain function when a property changes.
+* @param {Object} observingObject The object to look for changes in.. */
+const Observable = (observingObject, willChange, didChange) => {
+ const Handler = {
+ get(object, name, receiver) {
+ if(name === '__TARGET') { return Object.assign({}, observingObject); };
+ if(name === '__IS_PROXY') { return true };
+
+ return Reflect.get(object, name, receiver);
+ },
+ set(object, name, value) {
+ // About to update.
+ let old = Object.assign({}, observingObject);
+ if(willChange) willChange(old);
+
+ // Make changes.
+ object[name] = value;
+
+ // Did update.
+ if(didChange) didChange(object);
+
+ return Reflect.set(object, name, value);
+ },
+ defineProperty(object, name, descriptor) {
+ didChange(object);
+ return Reflect.defineProperty(object, name, descriptor);
+ },
+ deleteProperty(object, name) {
+ didChange(object);
+ return Reflect.deleteProperty(object, name);
+ }
+ };
+ return new Proxy(observingObject, Handler);
+}
+
+export default Observable;
\ No newline at end of file
diff --git a/src/util.js b/src/util.js
index 198df95..49228a8 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,43 +1,98 @@
-/** Generates a random id for a component. */
-const randomID = () => {
- return '_' + Math.random().toString(36).substr(2, 9);
+const setAttributes = function($element, key, value, instance = null) {
+ // 1.) Function handler for dom element.
+ if(typeof value === 'function' && key.startsWith('on')) {
+ const event = key.slice(2).toLowerCase();
+ $element.__mosaicHandlers = $element.__mosaicHandlers || {};
+ $element.removeEventListener(event, $element.__mosaicHandlers[event]);
+
+ $element.__mosaicHandlers[event] = value;
+ $element.addEventListener(event, $element.__mosaicHandlers[event]);
+ }
+ // 2.) Particular types of attributes.
+ else if(key === 'checked' || key === 'value' || key === 'className') {
+ $element[key] = value;
+ }
+ // 3.) Style property.
+ else if(key === 'style') {
+ if(typeof value === 'object') Object.assign($element.style, value);
+ else if(typeof value === 'string') $element[key] = value;
+ }
+ // 4.) Check for the reference type.
+ else if(key === 'ref' && typeof value === 'function') {
+ value($element);
+ }
+ // 5.) Support the key property for more efficient rendering.
+ else if(key === 'key') {
+ $element.__mosaicKey = value;
+ }
+ // 6.) Value is a not an object nor a function, so anything else basically.
+ else if(typeof value !== 'object' && typeof value !== 'function') {
+ $element.setAttribute(key, value);
+ }
}
-// /** Traverses a Mosaic component tree and calls the "created" method on all of them in order
-// * of when they show up in the app. So if you have Footer inside of Home inside of App, then you
-// * will run created on Footer first, then on Home, then on App.
-// * @param {Mosaic} start The entry point of the Mosaic app. */
-// const incrementallyCreateMosaics = (start) => {
-// let children = Object.values(start.references);
-// if(children.length === 1) {
-// children[0].created();
-// }
+/** Start with a particular VNode and traverse the entire tree and only return the ones that match
+* the comparator.
+* @param {Object} head The absolute parent VNode.
+* @param {Object} start The starting VNode.
+* @param {Array} array The final array to return.
+* @param {Function} comparator The compare function.
+* @param {Function} action The action to take when the comparator is true. */
+const traverseVDomTree = function(head, start, array, comparator, action) {
+ // console.log("DOM: ", start);
+ if(comparator(head, start, array) === true) {
+ if(action) action(head, start, array);
+ array.push(start);
+ }
-// children.forEach(child => incrementallyCreateMosaics(child));
-// }
+ for(var i in start.children) {
+ traverseVDomTree(head, start.children[i], array, comparator, action);
+ }
+ return array;
+}
-/** Validates the child Mosaic components of a parent Mosaic component to make sure they
-* all follow the same schema.
-* @param {Object} components The components to check for.
-* @returns {Boolean} Whether or not there are no errors in the types of the input components. */
-const validateMosaicChildren = (components) => {
- if(!components) return true;
-
- let children = Object.values(components);
- if(children.length === 0) return true;
+/** Clones a function. */
+const cloneFunction = function() {
+ var that = this;
+ var f = function() { return that.apply(this, arguments); };
+ for(var key in this) {
+ if (this.hasOwnProperty(key)) {
+ f[key] = this[key];
+ }
+ }
+ return f;
+};
- let foundOneWrong = children.find(comp => {
- if(!('type' in comp)) {
- return false;
- } else if('type' in comp) {
- if(!comp['type'].view) {
- return true;
+/** Does a deep clone of an object, also cloning its children.
+ * @param {Object} from The input object to copy from.
+ */
+const deepClone = function(from) {
+ let out = Object.create({});
+ if(typeof from === 'function') {
+ return cloneFunction.call(from);
+ }
+ for(var i in from) {
+ if(from.hasOwnProperty(i)) {
+ if(typeof from[i] === 'object') {
+ // if(from[i].__IS_PROXY) {
+ // let ulo = Object.assign({}, from[i].__TARGET);
+ // let nProx = new Observable(ulo, () => from[i].willChange, () => from[i].didChange);
+ // out[i] = nProx;
+ // } else {
+ out[i] = Object.assign({}, deepClone(from[i]));
+ // }
+ }
+ else if(typeof from[i] === 'function') {
+ out[i] = from[i].bind(out);
+ }
+ else {
+ out[i] = from[i];
}
}
- });
- return foundOneWrong === undefined || foundOneWrong === null;
+ }
+ return out;
}
-exports.randomID = randomID;
-// exports.incrementallyCreateMosaics = incrementallyCreateMosaics;
-exports.validateMosaicChildren = validateMosaicChildren;
\ No newline at end of file
+exports.setAttributes = setAttributes;
+exports.traverseVDomTree = traverseVDomTree;
+exports.deepClone = deepClone;
diff --git a/src/validations.js b/src/validations.js
new file mode 100644
index 0000000..d6a74c9
--- /dev/null
+++ b/src/validations.js
@@ -0,0 +1,54 @@
+function isHTMLElement(obj) {
+ try { return obj instanceof HTMLElement; }
+ catch(e){
+ return (typeof obj === "object") && (obj.nodeType === 1) && (typeof obj.style === "object") &&
+ (typeof obj.ownerDocument ==="object");
+ }
+}
+
+/** Looks at a Mosaic's configuration options and returns undefined if there is nothing wrong, and
+ * returns a descriptor sentence if something is wrong.
+ * @param {MosaicOptions} options The config options.
+ * @returns {undefined} If there is nothing wrong with the input options.
+ * @returns {String} describing the problem.
+ */
+const findInvalidOptions = function(options) {
+ // Element
+ if(options.element && (!isHTMLElement(options.element) || !document.contains(options.element))) {
+ return `The Mosaic could not be created because the "element" property is either not an HTML DOM
+ element or it does not already exist in the DOM. Make sure that the "element" property is an already
+ existing DOM element such as "document.body" or a div with the id of 'root' for example.`;
+ }
+
+ // Data
+ if(options.data && typeof options.data !== 'object') {
+ return `The data property of a Mosaic must be defined as a plain, JavaScript object.`;
+ }
+
+ // View
+ if(!options.view) {
+ return `View is a required property of Mosaic components.`
+ }
+ if(typeof options.view !== 'function') {
+ return `The view property must be a function that returns JSX code or an h-tree.`;
+ }
+
+ // Actions
+ if(options.actions && typeof options.actions !== 'object') {
+ return `Actions must be defined as an object, where each entry is a function.`;
+ }
+
+ // Lifecycle
+ if((options.created && typeof options.created !== 'function') ||
+ (options.willUpdate && typeof options.willUpdate !== 'function') ||
+ (options.updated && typeof options.updated !== 'function') ||
+ (options.willDestory && typeof options.willDestory !== 'function')) {
+ return `All lifecycle methods (created, willUpdate, updated, and willDestroy) must be
+ function types.`;
+ }
+
+ return undefined;
+}
+
+exports.isHTMLElement = isHTMLElement;
+exports.findInvalidOptions = findInvalidOptions;
\ No newline at end of file
diff --git a/src/vdom/createElement.js b/src/vdom/createElement.js
index bb3161f..f1f4ac7 100644
--- a/src/vdom/createElement.js
+++ b/src/vdom/createElement.js
@@ -1,7 +1,8 @@
-const createElement = (nodeName, properties = {}, ...children) => {
- if(typeof nodeName === 'object' && nodeName.view) {
- return nodeName.view();
- }
- return { nodeName, properties: properties || {}, children };
-};
-exports.createElement = createElement;
\ No newline at end of file
+const createElement = function(type, props = {}, ...children) {
+ return {
+ type: type,
+ props: props || {},
+ children,
+ };
+}
+export default createElement;
\ No newline at end of file
diff --git a/src/vdom/diff.js b/src/vdom/diff.js
deleted file mode 100644
index 47b7c13..0000000
--- a/src/vdom/diff.js
+++ /dev/null
@@ -1,190 +0,0 @@
-const { createElement } = require('./createElement');
-const { render, setDomAttributes, setEventHandlers, isEventProperty} = require('./render');
-
-const zip = (xs, ys) => {
- const zipped = [];
- for(let i = 0; i < Math.min(xs.length, ys.length); i++) {
- zipped.push([xs[i], ys[i]]);
- }
- return zipped;
-}
-
-const diffProperties = (oldProps, newProps) => {
- // The array of patches to perform.
- const patches = [];
-
- // Go through the new properties and add on to the list of patch functions that will run later.
- Object.keys(newProps).forEach(nPropName => {
- let nPropVal = newProps[nPropName];
- let _patch = ($node) => {
- if(isEventProperty(nPropName)) {
- setEventHandlers($node, nPropName, nPropVal);
- } else {
- setDomAttributes($node, nPropName, nPropVal);
- }
- return $node;
- }
- patches.push(_patch);
- });
-
- // Go through the old properties and remove the ones that are no longer in the new properties.
- for(var i in oldProps) {
- if(!(i in newProps)) {
- let _patch = ($node) => {
- $node.removeAttribute(i);
- return $node;
- }
- patches.push(_patch);
- }
- }
-
- // Create a patch that just runs all of the accumulated patches.
- // Applies all of the patch operations.
- let patch = ($node) => {
- for(var i in patches) patches[i]($node);
- return $node
- }
- return patch;
-}
-
-const diffChildren = (oldVChildren, newVChildren) => {
- const patches = [];
-
- // Go through the children and add the result of their diffing.
- oldVChildren.forEach((oldVChild, index) => {
- let result = diff(oldVChild, newVChildren[index]);
- patches.push(result);
- });
-
- // Make additional patches for unequal children lengths of the old and new vNodes.
- const additionalPatches = [];
- const sliced = newVChildren.slice(oldVChildren.length);
- for(var i = 0; i < sliced.length; i++) {
- let s = sliced[i];
- let _patch = ($node) => {
- let res = render(s);
- $node.appendChild(res);
- return $node;
- }
- additionalPatches.push(_patch);
- }
-
-
- let patch = ($parent) => {
- for(const [p, $child] of zip(patches, $parent.childNodes)) {
- p($child);
- }
- for(var i in additionalPatches) {
- const p = additionalPatches[i];
- p($parent);
- }
- return $parent;
- }
- return patch;
-}
-
-/** Calculates the difference between different virtual nodes and returns a function
-* to patch them together.
-* @param {Object} oldVNode The old virtual dom node.
-* @param {Object} newVNode The new virtual dom node. */
-const diff = (oldVNode, newVNode) => {
- // console.log(oldVNode, newVNode);
-
- // Case 1: The old virtual node does not exist.
- if(newVNode === undefined) {
- let patch = ($node) => { $node.remove(); return undefined };
- return patch;
- }
-
- // Case 2: They are both strings, so compare them.
- if(typeof oldVNode === 'string' && typeof newVNode === 'string') {
- // Case 2.1: One is a text node and one is an element.
- if(oldVNode !== newVNode) {
- let patch = ($node) => {
- const $newDomNode = render(newVNode);
- $node.replaceWith($newDomNode);
- return $newDomNode;
- }
- return patch;
- }
- // Case 2.2: Both virtual nodes are strings and they match.
- else {
- let patch = ($node) => { return $node; }
- return patch;
- }
- }
-
- // Case 3: They are both numbers, so compare them.
- if(typeof oldVNode === 'number' && typeof newVNode === 'number') {
- // Case 3.1: One is a text node and one is an element.
- if(oldVNode !== newVNode) {
- let patch = ($node) => {
- const $newDomNode = render(newVNode);
- $node.replaceWith($newDomNode);
- return $newDomNode;
- }
- return patch;
- }
- // Case 3.2: Both virtual nodes are strings and they match.
- else {
- let patch = ($node) => { return $node; }
- return patch;
- }
- }
-
- // Case 4: They are both Mosaic components, so diff their views.
- if(typeof oldVNode === 'object' && typeof newVNode === 'object' && (oldVNode.view || newVNode.view)) {
- let patch = diff(oldVNode.view(), newVNode.view());
- return patch;
- // let patch = ($node) => {
- // const $newDomNode = render(newVNode.view());
- // $node.replaceWith($newDomNode);
- // return $newDomNode;
- // }
- // return patch;
- // let patch = ($node) => { return $node; };
- return patch;
- }
-
- // Case 5: They are arrays of elements, so go through each one and diff the objects.
- if(typeof oldVNode === 'object' && typeof newVNode === 'object' && (oldVNode.length || newVNode.length)) {
- // Create a patch for each child.
- let allPatches = [];
- for(var i = 0; i < oldVNode.length; i++) {
- let patch = diff(oldVNode[i], newVNode[i]);
- allPatches.push(patch);
- }
- // Create a final patch that applies all patch changes in the list.
- let finalPatch = ($node) => {
- allPatches.forEach((p, index) => {
- p($node.childNodes[index]);
- });
- return $node;
- }
- return finalPatch;
- }
-
- // Case 6: In order to make the diff algo more efficient, assume that if the trees
- // are of different types then we just replace the entire thing.
- if(oldVNode.nodeName !== newVNode.nodeName) {
- let patch = ($node) => {
- const $newDomNode = render(newVNode);
- $node.replaceWith($newDomNode);
- return $newDomNode;
- }
- return patch;
- }
-
- // Case 7: If we reach this point, it means that the only differences exist in either the
- // properties or the child nodes. Handle these cases separately and return a patch that just
- // updates the node, not neccessarily replaces them.
- const propsPatch = diffProperties(oldVNode.properties, newVNode.properties);
- const childrenPatch = diffChildren(oldVNode.children, newVNode.children);
- let finalPatch = ($node) => {
- propsPatch($node);
- childrenPatch($node);
- return $node;
- }
- return finalPatch;
-}
-exports.diff = diff;
\ No newline at end of file
diff --git a/src/vdom/mount.js b/src/vdom/mount.js
deleted file mode 100644
index 1f4a671..0000000
--- a/src/vdom/mount.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/** Actually places a DOM onto the page by replacing an old version with a new version.
-* @param {Element} $newNode The node that you want to show on the page.
-* @param {Element} $realNode The real node that will be replaced.
-*/
-const mount = ($newNode, $realNode) => {
- // This "works" by making the new node sit inside of root instead of replacing root.
- // however, there is a problem where you update different component states and every now and
- // then, if the updates comes at different times, we end up with child-child elements.
- if($realNode.firstChild) {
- $realNode.firstChild.replaceWith($newNode);
- } else {
- $realNode.appendChild($newNode);
- }
-
- // This works.
- // $realNode.replaceWith($newNode);
- return $newNode;
-}
-exports.mount = mount;
\ No newline at end of file
diff --git a/src/vdom/patch.js b/src/vdom/patch.js
new file mode 100644
index 0000000..88a49ee
--- /dev/null
+++ b/src/vdom/patch.js
@@ -0,0 +1,62 @@
+import { Mosaic } from '../index';
+import render from './render';
+import { setAttributes } from '../util';
+
+const patch = function($dom, vnode, $parent = $dom.parentNode, instance = null) {
+ const replace = $parent ? ($el => { $parent.replaceChild($el, $dom); return $el }) : ($el => $el);
+
+ // 1.) Patch the differences of a Mosaic type.
+ if(typeof vnode === 'object' && typeof vnode.type === 'object' && vnode.type.__isMosaic === true) {
+ return Mosaic.patch($dom, vnode, $parent);
+ }
+ // 2.) Compare plain text nodes.
+ else if(typeof vnode !== 'object' && $dom instanceof Text) {
+ return ($dom.textContent !== vnode) ? replace(render(vnode, $parent, instance)) : $dom;
+ }
+ // 3.) If one is an object and one is text, just replace completely.
+ else if(typeof vnode === 'object' && $dom instanceof Text) {
+ return replace(render(vnode, $parent, instance));
+ }
+ // 4.) One is an object and the tags are different, so replace completely.
+ else if(typeof vnode === 'object' && (vnode.type && !vnode.type.__isMosaic) && $dom.nodeName !== vnode.type.toUpperCase()) {
+ let n = replace(render(vnode, $parent, instance));
+ return n;
+ }
+ // 5.) If they are objects and their tags are equal, patch their children recursively.
+ else if(typeof vnode === 'object' && $dom.nodeName === vnode.type.toUpperCase()) {
+ const pool = {};
+ const active = document.activeElement;
+
+ [].concat(...$dom.childNodes).map((child, index) => {
+ const key = child.__mosaicKey || `__index_${index}`;
+ pool[key] = child;
+ });
+ [].concat(...vnode.children).map((child, index) => {
+ const key = child.props && child.props.key || `__index_${index}`;
+ var $node;
+
+ if(pool[key]) $node = patch(pool[key], child)
+ else $node = render(child, $dom, instance);
+
+ $dom.appendChild($node);
+ delete pool[key];
+ });
+
+ // Unmount the component and call the lifecycle function.
+ for(const key in pool) {
+ const instance = pool[key].__mosaicInstance;
+ if(instance && instance.willDestroy) instance.willDestroy();
+ pool[key].remove();
+ }
+
+ // Remove and reset the necessary attributes.
+ for(var attr in $dom.attributes) $dom.removeAttribute(attr.name);
+ for(var prop in vnode.props) setAttributes($dom, prop, vnode.props[prop], vnode);
+ active.focus();
+
+ // Return the real dom node.
+ return $dom;
+ }
+}
+
+export default patch;
\ No newline at end of file
diff --git a/src/vdom/render.js b/src/vdom/render.js
index 987e923..4e9013f 100644
--- a/src/vdom/render.js
+++ b/src/vdom/render.js
@@ -1,113 +1,29 @@
-const { createElement } = require('./createElement');
+import { Mosaic } from '../index';
+import { setAttributes } from '../util';
-/** Checks whether or not the property is an event handler. */
-const isEventProperty = (name) => {
- return /^on/.test(name);
-}
-
-/** Handles setting attributse on a real DOM element when it might be an object, etc.
-* @param {Element} $element The dom element to set attributes on.
-* @param {String} propName The name of the attribute.
-* @param {String | Number | Object} propVal The value of the attribute. */
-const setDomAttributes = ($element, propName, propVal) => {
- if(propName === 'style') {
- let styleString = "";
-
- if(typeof propVal === 'object') {
- Object.keys(propVal).forEach(styleName => {
- const hypenated = styleName.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase();
- styleString += `${hypenated}:${propVal[styleName]};`;
- });
- } else if(typeof propVal === 'string') {
- styleString = propVal;
- }
-
- $element.setAttribute(propName, styleString);
- } else {
- $element.setAttribute(propName, propVal);
- }
-}
-
-
-/** Handles adding event handlers to a real DOM element. */
-const setEventHandlers = ($element, eventName, eventValue) => {
- const name = eventName.slice(2).toLowerCase();
- $element.addEventListener(name, eventValue);
-}
-
-/** Renders a component as a VNode. */
-const renderMosaicComponent = (component) => {
- // Get the view.
- const val = createElement(component);
-
- // Create the actual dom element.
- const $element = document.createElement(val.nodeName);
-
- // Add all of the properties to this element.
- Object.keys(val.properties).forEach(propName => {
- if(isEventProperty(propName)) {
- setEventHandlers($element, propName, vNode.properties[propName]);
- } else {
- setDomAttributes($element, propName, val.properties[propName]);
- }
- });
-
- // Append all of the children to this element.
- Object.keys(val.children).forEach(childIndex => {
- const x = render(val.children[childIndex]);
- $element.appendChild(x);
- });
-
- return $element;
-}
-
-/** Renders any regular dom element that is not a text node. */
-const renderRegularNode = (vNode) => {
- // Create the actual dom element.
- const $element = document.createElement(vNode.nodeName);
-
- // Add all of the properties to this element.
- Object.keys(vNode.properties).forEach(propName => {
- if(isEventProperty(propName)) {
- setEventHandlers($element, propName, vNode.properties[propName]);
- } else {
- setDomAttributes($element, propName, vNode.properties[propName]);
- }
- });
-
- // Append all of the children to this element.
- Object.keys(vNode.children).forEach(childIndex => {
- const x = render(vNode.children[childIndex]);
- if(typeof x !== 'undefined') $element.appendChild(x);
- });
-
- return $element;
-}
+const render = function(vnode, $parent = null, instance = null) {
+ const mount = $parent ? ($el => $parent.appendChild($el)) : ($el => $el);
-/** Takes a virtual dom node and returns a real dom node.
-* @param {Object} vNode A virtual dom node.
-* @returns {Element} A real dom node. */
-const render = (vNode) => {
- if(typeof vNode === 'string' || typeof vNode === 'number') {
- return document.createTextNode(vNode);
+ // 1.) Primitive types.
+ if(typeof vnode === 'string' || typeof vnode === 'number') {
+ let $e = document.createTextNode(typeof vnode === 'boolean' ? '' : vnode);
+ return mount($e);
}
- else if(typeof vNode === 'object' && vNode.view) {
- return renderMosaicComponent(vNode);
+ // 2.) A Mosaic component.
+ else if(typeof vnode === 'object' && typeof vnode.type === 'object' && vnode.type.__isMosaic === true) {
+ return Mosaic.view(vnode, $parent);
}
- else if(typeof vNode === 'object' && vNode.length) {
- // Basically create a holder element and create elements for each child.
- let $holder = document.createElement('div');
- for(var i = 0; i < vNode.length; i++) {
- let $node = render(vNode[i]);
- $holder.appendChild($node);
- }
- return $holder;
+ // 3.) Handle child components and attributes.
+ else if(typeof vnode === 'object' && typeof vnode.type === 'string') {
+ const $e = document.createElement(vnode.type);
+ const $dom = mount($e);
+ for(var child of [].concat(...vnode.children)) render(child, $dom, instance);
+ for(var prop in vnode.props) setAttributes($dom, prop, vnode.props[prop], instance);
+ return $dom;
}
+ // 4.) Otherwise, throw an error.
else {
- return renderRegularNode(vNode);
+ throw new Error(`Invalid Virtual DOM Node: ${vnode}.`);
}
}
-exports.render = render;
-exports.setDomAttributes = setDomAttributes;
-exports.setEventHandlers = setEventHandlers;
-exports.isEventProperty = isEventProperty;
\ No newline at end of file
+export default render;
\ No newline at end of file