From 46c7e0d292798645c6cc2335fbeaddd0be61c7ba Mon Sep 17 00:00:00 2001 From: Adeola Uthman Date: Mon, 28 Jan 2019 10:36:26 -0500 Subject: [PATCH 1/8] Using new virtual dom system, so components can act independently --- example/.babelrc | 2 +- example/app.js | 124 +++++++++---- example/footer.js | 32 ---- example/home.js | 58 ------ package.json | 3 +- src/index.js | 362 ++++++++++++++++++++++++-------------- src/observable.js | 0 src/util.js | 118 +++++++++++-- src/vdom/createElement.js | 7 - src/vdom/diff.js | 190 -------------------- src/vdom/mount.js | 19 -- src/vdom/render.js | 113 ------------ 12 files changed, 431 insertions(+), 597 deletions(-) delete mode 100644 example/footer.js delete mode 100644 example/home.js create mode 100644 src/observable.js delete mode 100644 src/vdom/createElement.js delete mode 100644 src/vdom/diff.js delete mode 100644 src/vdom/mount.js delete mode 100644 src/vdom/render.js diff --git a/example/.babelrc b/example/.babelrc index 43c8870..b0477b5 100644 --- a/example/.babelrc +++ b/example/.babelrc @@ -1,6 +1,6 @@ { "presets": ["env"], "plugins": [["babel-plugin-transform-react-jsx", { - "pragma": "h" + "pragma": "createElement" }]] } \ No newline at end of file diff --git a/example/app.js b/example/app.js index 1d1b08c..ed7b4be 100644 --- a/example/app.js +++ b/example/app.js @@ -1,42 +1,104 @@ -import { h, Mosaic } from '../src/index'; -import Home from './home'; +import { createElement, render, Component, Mosaic } from '../src/index'; const root = document.getElementById('root'); root.innerHTML = ''; -const appStyles = { - width: '100%', - color: 'white', - paddingTop: '10px', - textAlign: 'center', - fontFamily: 'Avenir', - paddingBottom: '100px', - backgroundColor: '#4341B5' -} -const app = new Mosaic({ - element: root, - data: { - title: "Mosaic", - subtitle: "A front-end JavaScript library for building user interfaces" +// class App extends Component { +// render() { +// return
+//

Working

+//
+// } +// } +// render(, document.getElementById('root')); + +const Counter = new Mosaic({ + state: { count: 0 }, + view: function() { + return {this.state.count} }, - components: { - home1: Mosaic.Child(Home, { instance: 0, componentInstance: "First Home Instance: " }), - home2: Mosaic.Child(Home, { instance: 1, componentInstance: "Second Home Instance: " }), + created: function() { + setInterval(() => { + this.setState({ count: Math.floor(Math.random() * 100) }); + }, 1000); + } +}) +const Label = new Mosaic({ + state: {}, + view: function() { + return

Count:

}, + created: function() { + + } +}) +const App = new Mosaic({ + element: document.getElementById('root'), + state: { title: "Mosaic" }, view: function() { - return ( -
-

Welcome to {this.data.title}!

-

{this.data.subtitle}

-

Use the buttons below to try out the counter!

- - { this.home1.view() } - { this.home2.view() } -
- ) + return
+

Welcome to {this.state.title}!

+
}, created: function() { - this.home1.setData({ count: 10 }); + } }); -app.paint(); \ No newline at end of file +App.paint(); + + +// import Gooact, { render, Component } from '../src/index'; + +// class Title extends Component { +// componentDidMount() { +// console.log('title'); +// console.log(document.getElementById('title')); +// } + +// render() { +// return ( +//

{this.props.children}

+// ); +// } +// } + +// class App extends Component { +// constructor(props) { +// super(props); +// this.state = {counter: 0}; +// this.onIncrease = this.onIncrease.bind(this); +// this.onDecrease = this.onDecrease.bind(this); +// } + +// componentDidMount() { +// console.log('app'); +// } + +// onIncrease() { +// this.setState({counter: this.state.counter + 1}); +// } + +// onDecrease() { +// this.setState({counter: this.state.counter - 1}); +// } + +// render() { +// const {counter} = this.state; +// return ( +//
+// Hello Gooact!!!! +//

+// +// {' '}Counter: {counter}{' '} +// +//

+//
+// ); +// } +// } + +// let app = ; +// render(app, document.getElementById('root')); \ No newline at end of file diff --git a/example/footer.js b/example/footer.js deleted file mode 100644 index 722d984..0000000 --- a/example/footer.js +++ /dev/null @@ -1,32 +0,0 @@ -import { h, Mosaic } from '../src/index'; - -export default new Mosaic({ - data: { - name: "Example Text" - }, - actions: function(self) { - return { - change: function() { - self.setData({ name: "NEW TEXT" }); - } - } - }, - view: function() { - return ( -
- -
- ) - }, - created: function() { - console.log("Created Footer: ", this); - }, - willUpdate: function(old) { - console.log("About to update footer: ", old); - }, - updated: function() { - console.log("Just updated footer: ", this); - } -}); \ No newline at end of file diff --git a/example/home.js b/example/home.js deleted file mode 100644 index b40179f..0000000 --- a/example/home.js +++ /dev/null @@ -1,58 +0,0 @@ -import { h, Mosaic } from '../src/index'; -import Footer from './footer'; - - -const homeStyles = { - paddingTop: '10px' -} -const buttonStyles = { - width: '50px', - height: '50px', - margin: '20px', - border: 'none', - outline: 'none', - fontSize: '25px', - color: '#4341B5', - cursor: 'pointer', - borderRadius: '100%', - fontFamily: 'Avenir', -} -export default new Mosaic({ - data: { - count: 0 - }, - components: { - footer: Mosaic.Child(Footer) - }, - actions: function(self) { - return { - countUp: function() { - self.setData({ count: self.data.count + 1 }); - }, - countDown: function() { - self.setData({ count: self.data.count - 1 }); - } - } - }, - view: function() { - return ( -
-

{this.data.componentInstance || ""}

-

Count: {this.data.count}

- - - { this.footer.view() } -
- ); - }, - - 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..e1f922c 100644 --- a/package.json +++ b/package.json @@ -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..2a07629 100644 --- a/src/index.js +++ b/src/index.js @@ -1,146 +1,246 @@ -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'); - -/** The attributes that go along with a Mosaic component. */ -const MosaicOptions = { - /** The DOM element to use to wrap around a Mosaic component. */ - element: Element || String, - - /** The state of this component. */ - data: Object.create(null), - - /** The actions that modify this component's data. */ - actions: Function, - - /** The Mosaic children that are nested components of this one. */ - components: Object, +import { setAttributes } from './util'; + +const createElement = function(type, props = {}, ...children) { + return { + type: type, + props: props || {}, + children, + }; +} - /** The view that this component will take on. */ - view: Function, +const render = function(vnode, $parent = null) { + const mount = $parent ? ($el => $parent.appendChild($el)) : ($el => $el); + + // 1.) Primitive types. + if(typeof vnode === 'string' || typeof vnode === 'number') { + let $e = document.createTextNode(typeof vnode === 'boolean' ? '' : vnode); + return mount($e); + } + // 2.) A Mosaic component. + else if(typeof vnode === 'object' && typeof vnode.type === 'object' && vnode.type.__isMosaic === true) { + return Mosaic.view(vnode, $parent); + } + // 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); + for(var prop in vnode.props) setAttributes($dom, prop, vnode.props[prop]); + return $dom; + } + // 4.) Otherwise, throw an error. + else { + throw new Error(`Invalid Virtual DOM Node: ${vnode}.`); + } +} - /** The function to run when this component gets created. */ - created: Function, +const patch = function($dom, vnode, $parent = $dom.parentNode) { + 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)) : $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)); + } + // 4.) One is an object and the tags are different, so replace completely. + // else if(typeof vnode === 'object' && $dom.nodeName !== (typeof vnode.type).toUpperCase()) { + // console.log('GOT HERE: ', vnode, $dom); + // let n = replace(render(vnode, $parent)); + // console.log(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}`; + $dom.appendChild(pool[key] ? patch(pool[key], child) : render(child, $dom)); + 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]); + active.focus(); + + // Return the real dom node. + return $dom; + } +} - /** The function to run when this component is about to update. */ - willUpdate: Function, - /** The function to run when this component gets updated. */ - updated: Function +const MosaicOptions = { + element: HTMLElement, + state: Object, + view: Function, + created: Function, + willUpdate: Function, + updated: Function, + willDestroy: Function, + destroyed: Function, } - -/** Creates a new Mosaic component. -* @param {MosaicOptions} options The configuration options for a Mosaic component. */ 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; - } + this.base = options.element + this.state = options.state; + this.view = options.view; + this.created = options.created; + this.willUpdate = options.willUpdate; + this.updated = options.updated; + this.willDestroy = options.willDestroy; + this.destroyed = options.destroyed; + this.__isMosaic = true; + + this.setState = function(next) { + const compat = (a) => typeof this.state == 'object' && typeof a == 'object'; + if(this.base) { + if(this.willUpdate) this.willUpdate(); + + this.state = compat(next) ? Object.assign({}, this.state, next) : next; + patch(this.base, this.view()); + + if(this.updated) this.updated(); + } + } + this.paint = function() { + render(createElement(this), this.base); + } + 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 }; +Mosaic.view = function(vnode, $parent = null) { + const props = Object.assign({}, vnode.props, { children: vnode.children }); + + // Render a new instance of this component. + if(typeof vnode.type === 'object' && vnode.type.__isMosaic) { + const options = { + element: vnode.type.base, + state: Object.assign({}, vnode.type.state), + view: vnode.type.view, + created: vnode.type.created, + } + const instance = new Mosaic(options); + instance.base = render(instance.view(), $parent); + instance.base.__mosaicInstance = instance; + instance.base.__mosaicKey = vnode.props.key; + + if(instance.created) instance.created(); + return instance.base; + } else { + return render(vnode.type.view(props), $parent); + } +} +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); + } + 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); + } } -/** "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(); +const Component = class { + + constructor() { + this.state = null; + } + + static render(vnode, $parent = null) { + const props = Object.assign({}, vnode.props, { children: vnode.children }); + // Render a new instance of this component. + if(Component.isPrototypeOf(vnode.type)) { + const instance = new (vnode.type)(props); + instance.componentWillMount(); + instance.base = render(instance.render(), $parent); + instance.base.__mosaicInstance = instance; + instance.base.__mosaicKey = vnode.props.key; + instance.componentDidMount(); + return instance.base; + } else { + return render(vnode.type(props), $parent); + } + } + static patch($dom, vnode, $parent = $dom.parentNode) { + const props = Object.assign({}, vnode.props, { children: vnode.children }); + + if($dom.__mosaicInstance && $dom.__mosaicInstance.constructor === vnode.type) { + $dom.__gooactInstance.props = props; + return patch($dom, $dom.__gooactInstance.render(), $parent); + } + else if(Component.isPrototypeOf(vnode.type)) { + const $ndom = Component.render(vnode, $parent); + return $parent ? ($parent.replaceChild($ndom, $dom) && $ndom) : $ndom; + } + else if(!Component.isPrototypeOf(vnode.type)) { + return patch($dom, vnode.type(props), $parent); + } + } + + setState(next) { + const compat = (a) => typeof this.state == 'object' && typeof a == 'object'; + if(this.base) { + const prevState = this.state; + this.componentWillUpdate(this.props, next); + this.state = compat(next) ? Object.assign({}, this.state, next) : next; + patch(this.base, this.render()); + this.componentDidUpdate(this.props, prevState); + } + } + + componentWillReceiveProps(nextProps) { + return undefined; + } + + componentWillUpdate(nextProps, nextState) { + return undefined; + } + + componentDidUpdate(prevProps, prevState) { + return undefined; + } + + componentWillMount() { + return undefined; + } + + componentDidMount() { + return undefined; + } + + componentWillUnmount() { + return undefined; + } } -/** 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(); -} -exports.Mosaic = Mosaic; -exports.h = createElement; \ No newline at end of file +exports.createElement = createElement; +exports.render = render; +exports.Component = Component; +exports.Mosaic = Mosaic; \ No newline at end of file diff --git a/src/observable.js b/src/observable.js new file mode 100644 index 0000000..e69de29 diff --git a/src/util.js b/src/util.js index 198df95..f587302 100644 --- a/src/util.js +++ b/src/util.js @@ -3,18 +3,43 @@ const randomID = () => { return '_' + Math.random().toString(36).substr(2, 9); } -// /** 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(); -// } - -// children.forEach(child => incrementallyCreateMosaics(child)); -// } +/** Checks whether or not the property is an event handler. */ +const isEventProperty = (name) => { + return /^on/.test(name); +} + +const setAttributes = function($element, key, value) { + // 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' || key === 'class') { + $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); + } +} /** Validates the child Mosaic components of a parent Mosaic component to make sure they * all follow the same schema. @@ -38,6 +63,71 @@ const validateMosaicChildren = (components) => { return foundOneWrong === undefined || foundOneWrong === null; } +/** 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); + } + + for(var i in start.children) { + traverseVDomTree(head, start.children[i], array, comparator, action); + } + return array; +} + +/** Clones a function. */ +const clone = 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; +}; + +/** 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 clone.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 out; +} + exports.randomID = randomID; -// exports.incrementallyCreateMosaics = incrementallyCreateMosaics; -exports.validateMosaicChildren = validateMosaicChildren; \ No newline at end of file +exports.isEventProperty = isEventProperty; +exports.setAttributes = setAttributes; +exports.validateMosaicChildren = validateMosaicChildren; +exports.traverseVDomTree = traverseVDomTree; +exports.deepClone = deepClone; diff --git a/src/vdom/createElement.js b/src/vdom/createElement.js deleted file mode 100644 index bb3161f..0000000 --- a/src/vdom/createElement.js +++ /dev/null @@ -1,7 +0,0 @@ -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 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/render.js b/src/vdom/render.js deleted file mode 100644 index 987e923..0000000 --- a/src/vdom/render.js +++ /dev/null @@ -1,113 +0,0 @@ -const { createElement } = require('./createElement'); - -/** 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; -} - -/** 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); - } - else if(typeof vNode === 'object' && vNode.view) { - return renderMosaicComponent(vNode); - } - 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; - } - else { - return renderRegularNode(vNode); - } -} -exports.render = render; -exports.setDomAttributes = setDomAttributes; -exports.setEventHandlers = setEventHandlers; -exports.isEventProperty = isEventProperty; \ No newline at end of file From 6d19d7c00d6f61e8706a916eb590438b8d258d51 Mon Sep 17 00:00:00 2001 From: Adeola Uthman Date: Mon, 28 Jan 2019 10:53:48 -0500 Subject: [PATCH 2/8] Clean up the code --- example/.babelrc | 2 +- example/app.js | 14 +-- src/index.js | 237 +++++++++----------------------------- src/observable.js | 38 ++++++ src/vdom/createElement.js | 8 ++ src/vdom/patch.js | 56 +++++++++ src/vdom/render.js | 29 +++++ 7 files changed, 191 insertions(+), 193 deletions(-) create mode 100644 src/vdom/createElement.js create mode 100644 src/vdom/patch.js create mode 100644 src/vdom/render.js diff --git a/example/.babelrc b/example/.babelrc index b0477b5..43c8870 100644 --- a/example/.babelrc +++ b/example/.babelrc @@ -1,6 +1,6 @@ { "presets": ["env"], "plugins": [["babel-plugin-transform-react-jsx", { - "pragma": "createElement" + "pragma": "h" }]] } \ No newline at end of file diff --git a/example/app.js b/example/app.js index ed7b4be..20b63c9 100644 --- a/example/app.js +++ b/example/app.js @@ -1,4 +1,4 @@ -import { createElement, render, Component, Mosaic } from '../src/index'; +import { h, Mosaic } from '../src/index'; const root = document.getElementById('root'); root.innerHTML = ''; @@ -13,18 +13,18 @@ root.innerHTML = ''; // render(, document.getElementById('root')); const Counter = new Mosaic({ - state: { count: 0 }, + data: { count: 0 }, view: function() { - return {this.state.count} + return {this.data.count} }, created: function() { setInterval(() => { - this.setState({ count: Math.floor(Math.random() * 100) }); + this.setData({ count: Math.floor(Math.random() * 100) }); }, 1000); } }) const Label = new Mosaic({ - state: {}, + data: {}, view: function() { return

Count:

}, @@ -34,10 +34,10 @@ const Label = new Mosaic({ }) const App = new Mosaic({ element: document.getElementById('root'), - state: { title: "Mosaic" }, + data: { title: "Mosaic" }, view: function() { return
-

Welcome to {this.state.title}!

+

Welcome to {this.data.title}!

) } diff --git a/example/app.css b/example/app.css new file mode 100644 index 0000000..fbdbaaa --- /dev/null +++ b/example/app.css @@ -0,0 +1,51 @@ +.app { + position: absolute; + margin: 0px; + width: 100%; + height: 100%; + padding: 0px; + text-align: center; + background-color: #4341b5; +} + +.app-title { + color: white; + font-weight: 300; + font-family: 'Avenir'; +} + +input { + width: 70%; + height: 50px; + border: none; + padding: 15px; + font-size: 16px; + border-radius: 10px; + font-family: 'Avenir'; +} + +button { + position: relative; + width: 70%; + height: 50px; + border: none; + outline: none; + display: block; + color: white; + cursor: pointer; + font-size: 14px; + margin-top: 10px; + margin-left: auto; + margin-right: auto; + border-radius: 10px; + transition-duration: 0.1s; + background-color: #6866d8; +} +button:hover { + color: lightgray; + background-color: #5856b9; +} +button:active { + color: gray; + background-color: #333194; +} \ No newline at end of file diff --git a/example/app.html b/example/app.html index 56a90eb..cafaf45 100644 --- a/example/app.html +++ b/example/app.html @@ -1,6 +1,7 @@ Mosaic + diff --git a/example/app.js b/example/app.js index 53b9006..5fac0a7 100644 --- a/example/app.js +++ b/example/app.js @@ -1,45 +1,51 @@ import { h, Mosaic } from '../src/index'; +import Observable from '../src/observable'; -const root = document.getElementById('root'); -root.innerHTML = ''; +/* Example of a Todo application using Mosaic. */ -const Counter = new Mosaic({ - data: { - count: 0 - }, +const TodoItem = new Mosaic({ + data: { title: "" }, view: function() { - return - }, - created: function() { - // setInterval(() => { - // this.data.count = Math.floor(Math.random() * 100); - // }, 1000); + return
  • {this.data.title}
  • + } +}); + +const TodoApp = new Mosaic({ + element: document.getElementById('root'), + data: { + todos: ['a', 'b', 'c'] }, actions: { - countUp: function() { - this.data.count += 1; + addTodo: function() { + let value = document.getElementById('inp').value; + document.getElementById('inp').value = ''; + + console.log(this.data.todos); + this.data.todos.push(1); } - } -}) -const Label = new Mosaic({ - data: {}, - view: function() { - return

    Count:
    Something: {this.data.name || "nothing"}

    - } -}) -const App = new Mosaic({ - element: root, - data: { title: "Mosaic" }, + }, view: function() { - return
    -

    Welcome to {this.data.title}!

    -

    Added property: {this.data.author || 'none'}

    -