From 1cbe762838e2583e9c5f49b5ac63230098548d80 Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Tue, 18 Aug 2015 07:32:11 -0700 Subject: [PATCH 01/97] Remove event delegation --- lib/render.js | 95 ++++++++--------------------------------------- test/dom/index.js | 18 +++++---- 2 files changed, 27 insertions(+), 86 deletions(-) diff --git a/lib/render.js b/lib/render.js index d928495..5ac7c3e 100644 --- a/lib/render.js +++ b/lib/render.js @@ -62,7 +62,6 @@ function render (app, container, opts) { * Listen to DOM events */ var rootElement = getRootElement(container) - addNativeEventListeners() /** * Watch for changes to the app so that we can update @@ -86,7 +85,6 @@ function render (app, container, opts) { */ function teardown () { - removeNativeEventListeners() removeNativeElement() app.off('unmount', onunmount) app.off('mount', onmount) @@ -174,7 +172,6 @@ function render (app, container, opts) { if (!entity) return trigger('beforeUnmount', entity, [entity.context, entity.nativeElement]) unmountChildren(entityId) - removeAllEvents(entityId) var componentEntities = components[entityId].entities; delete componentEntities[entityId] delete components[entityId] @@ -440,7 +437,7 @@ function render (app, container, opts) { // set attributes. forEach(attributes, function (value, name) { - setAttribute(entityId, path, el, name, value) + setAttribute(el, name, value) }) // add children. @@ -668,15 +665,16 @@ function render (app, container, opts) { // add new attrs forEach(nextAttrs, function (value, name) { - if (events[name] || !(name in prevAttrs) || prevAttrs[name] !== value) { - setAttribute(entityId, path, el, name, value) + var previousValue = prevAttrs[name] + if (events[name] || !(name in prevAttrs) || previousValue !== value) { + setAttribute(el, name, value, previousValue) } }) // remove old attrs forEach(prevAttrs, function (value, name) { if (!(name in nextAttrs)) { - removeAttribute(entityId, path, el, name) + removeAttribute(el, name, value) } }) } @@ -838,13 +836,14 @@ function render (app, container, opts) { * @param {String} value */ - function setAttribute (entityId, path, el, name, value) { + function setAttribute (el, name, value, previousValue) { if (!value) { - removeAttribute(entityId, path, el, name) + removeAttribute(el, name, previousValue) return } if (events[name]) { - addEvent(entityId, path, events[name], value) + if (previousValue) removeEvent(el, events[name], previousValue) + addEvent(el, events[name], value) return } switch (name) { @@ -876,9 +875,9 @@ function render (app, container, opts) { * @param {String} name */ - function removeAttribute (entityId, path, el, name) { - if (events[name]) { - removeEvent(entityId, path, events[name]) + function removeAttribute (el, name, value) { + if (events[name] && value) { + removeEvent(el, events[name], value) return } switch (name) { @@ -1109,48 +1108,6 @@ function render (app, container, opts) { }) } - /** - * Add all of the DOM event listeners - */ - - function addNativeEventListeners () { - forEach(events, function (eventType) { - rootElement.addEventListener(eventType, handleEvent, true) - }) - } - - /** - * Add all of the DOM event listeners - */ - - function removeNativeEventListeners () { - forEach(events, function (eventType) { - rootElement.removeEventListener(eventType, handleEvent, true) - }) - } - - /** - * Handle an event that has occured within the container - * - * @param {Event} event - */ - - function handleEvent (event) { - var target = event.target - var eventType = event.type - - // Walk up the DOM tree and see if there is a handler - // for this event type higher up. - while (target) { - var fn = keypath.get(handlers, [target.__entity__, target.__path__, eventType]) - if (fn) { - event.delegateTarget = target - if (fn(event) === false) break - } - target = target.parentNode - } - } - /** * Bind events for an element, and all it's rendered child elements. * @@ -1159,15 +1116,8 @@ function render (app, container, opts) { * @param {Function} fn */ - function addEvent (entityId, path, eventType, fn) { - keypath.set(handlers, [entityId, path, eventType], function (e) { - var entity = entities[entityId] - if (entity) { - fn.call(null, e, entity.context, setState(entity)) - } else { - fn.call(null, e) - } - }) + function addEvent (el, eventType, fn) { + el.addEventListener(eventType, fn) } /** @@ -1176,21 +1126,8 @@ function render (app, container, opts) { * @param {String} entityId */ - function removeEvent (entityId, path, eventType) { - var args = [entityId] - if (path) args.push(path) - if (eventType) args.push(eventType) - keypath.del(handlers, args) - } - - /** - * Unbind all events from an entity - * - * @param {Entity} entity - */ - - function removeAllEvents (entityId) { - keypath.del(handlers, [entityId]) + function removeEvent (el, eventType, fn) { + el.removeEventListener(eventType, fn) } /** diff --git a/test/dom/index.js b/test/dom/index.js index d8fccbe..646355f 100644 --- a/test/dom/index.js +++ b/test/dom/index.js @@ -879,9 +879,11 @@ test('adding, removing and updating events', ({equal,end}) => { test('should bubble events', ({equal,end,fail,ok}) => { var {mount,renderer,el,$} = setup(equal) + var state = {} var Test = { - render: function ({props,state}) { + render: function ({props}) { + let state = props.state return (
@@ -892,18 +894,20 @@ test('should bubble events', ({equal,end,fail,ok}) => { } } - var onClickTest = function (event, component, setState) { - setState({ active: true }) - equal(el.firstChild.firstChild, event.delegateTarget, 'event.delegateTarget is set') - return false + var onClickTest = function (event) { + state.active = true + equal(el.firstChild.firstChild.firstChild, event.target, 'event.target is set') + event.stopImmediatePropagation() } var onParentClick = function () { - // fail('event bubbling was not stopped') + fail('event bubbling was not stopped') } - mount() + mount() trigger($('a'), 'click') + equal(state.active, true, 'state was changed') + mount() ok($('.active'), 'event fired on parent element') teardown({renderer,el}) end() From fa0467af83198b0c2d96909599d061ffe2d18357 Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Tue, 18 Aug 2015 07:40:12 -0700 Subject: [PATCH 02/97] Removed sources/propTypes --- lib/application.js | 10 --- lib/render.js | 134 +------------------------------ lib/stringify.js | 9 --- test/dom/index.js | 183 +------------------------------------------ test/string/index.js | 15 +--- 5 files changed, 5 insertions(+), 346 deletions(-) diff --git a/lib/application.js b/lib/application.js index 72acb50..505eba4 100644 --- a/lib/application.js +++ b/lib/application.js @@ -51,16 +51,6 @@ Application.prototype.option = function (name, val) { return this } -/** - * Set value used somewhere in the IO network. - */ - -Application.prototype.set = function (name, data) { - this.sources[name] = data - this.emit('source', name, data) - return this -} - /** * Mount a virtual element. * diff --git a/lib/render.js b/lib/render.js index 5ac7c3e..ddbdf12 100644 --- a/lib/render.js +++ b/lib/render.js @@ -70,7 +70,6 @@ function render (app, container, opts) { app.on('unmount', onunmount) app.on('mount', onmount) - app.on('source', onupdate) /** * If the app has already mounted an element, we can just @@ -88,7 +87,6 @@ function render (app, container, opts) { removeNativeElement() app.off('unmount', onunmount) app.off('mount', onmount) - app.off('source', onupdate) } /** @@ -112,20 +110,6 @@ function render (app, container, opts) { currentElement = null } - /** - * Update all components that are bound to the source - * - * @param {String} name - * @param {*} data - */ - - function onupdate (name, data) { - if (!connections[name]) return; - connections[name].forEach(function(update) { - update(data) - }) - } - /** * Render and mount a component to the native dom. * @@ -134,8 +118,6 @@ function render (app, container, opts) { */ function mountEntity (entity) { - register(entity) - setSources(entity) children[entity.id] = {} entities[entity.id] = entity @@ -172,9 +154,6 @@ function render (app, container, opts) { if (!entity) return trigger('beforeUnmount', entity, [entity.context, entity.nativeElement]) unmountChildren(entityId) - var componentEntities = components[entityId].entities; - delete componentEntities[entityId] - delete components[entityId] delete entities[entityId] delete children[entityId] } @@ -314,7 +293,6 @@ function render (app, container, opts) { function updateEntity (entityId) { var entity = entities[entityId] - setSources(entity) if (!shouldUpdate(entity)) { commit(entity) @@ -402,7 +380,7 @@ function render (app, container, opts) { function toNative (entityId, path, vnode) { switch (nodeType(vnode)) { case 'text': return toNativeText(vnode) - case 'empty': return toNativeEmptyElement(entityId, path) + case 'empty': return toNativeEmptyElement() case 'element': return toNativeElement(entityId, path, vnode) case 'component': return toNativeComponent(entityId, path, vnode) } @@ -446,10 +424,6 @@ function render (app, container, opts) { if (!childEl.parentNode) el.appendChild(childEl) }) - // store keys on the native element for fast event handling. - el.__entity__ = entityId - el.__path__ = path - return el } @@ -457,11 +431,8 @@ function render (app, container, opts) { * Create a native element from a virtual element. */ - function toNativeEmptyElement (entityId, path) { - var el = document.createElement('noscript') - el.__entity__ = entityId - el.__path__ = path - return el + function toNativeEmptyElement () { + return document.createElement('noscript') } /** @@ -1011,103 +982,6 @@ function render (app, container, opts) { return bool } - /** - * Register an entity. - * - * This is mostly to pre-preprocess component properties and values chains. - * - * The end result is for every component that gets mounted, - * you create a set of IO nodes in the network from the `value` definitions. - * - * @param {Component} component - */ - - function register (entity) { - registerEntity(entity) - var component = entity.component - if (component.registered) return - - // initialize sources once for a component type. - registerSources(entity) - component.registered = true - } - - /** - * Add entity to data-structures related to components/entities. - * - * @param {Entity} entity - */ - - function registerEntity(entity) { - var component = entity.component - // all entities for this component type. - var entities = component.entities = component.entities || {} - // add entity to component list - entities[entity.id] = entity - // map to component so you can remove later. - components[entity.id] = component - } - - /** - * Initialize sources for a component by type. - * - * @param {Entity} entity - */ - - function registerSources(entity) { - var component = components[entity.id] - // get 'class-level' sources. - // if we've already hooked it up, then we're good. - var sources = component.sources - if (sources) return - var entities = component.entities - - // hook up sources. - var map = component.sourceToPropertyName = {} - component.sources = sources = [] - var propTypes = component.propTypes - for (var name in propTypes) { - var data = propTypes[name] - if (!data) continue - if (!data.source) continue - sources.push(data.source) - map[data.source] = name - } - - // send value updates to all component instances. - sources.forEach(function (source) { - connections[source] = connections[source] || [] - connections[source].push(update) - - function update (data) { - var prop = map[source] - for (var entityId in entities) { - var entity = entities[entityId] - var changes = {} - changes[prop] = data - updateEntityProps(entityId, assign(entity.pendingProps, changes)) - } - } - }) - } - - /** - * Set the initial source value on the entity - * - * @param {Entity} entity - */ - - function setSources (entity) { - var component = entity.component - var map = component.sourceToPropertyName - var sources = component.sources - sources.forEach(function (source) { - var name = map[source] - if (entity.pendingProps[name] != null) return - entity.pendingProps[name] = app.sources[source] // get latest value plugged into global store - }) - } - /** * Bind events for an element, and all it's rendered child elements. * @@ -1141,7 +1015,6 @@ function render (app, container, opts) { return { entities: entities, handlers: handlers, - connections: connections, currentElement: currentElement, options: options, app: app, @@ -1175,7 +1048,6 @@ function Entity (component, props, ownerId) { this.id = uid() this.ownerId = ownerId this.component = component - this.propTypes = component.propTypes || {} this.context = {} this.context.id = this.id this.context.props = defaults(props || {}, component.defaultProps || {}) diff --git a/lib/stringify.js b/lib/stringify.js index 954fe69..84e30db 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -20,18 +20,9 @@ module.exports = function (app) { */ function stringify (component, optProps, children) { - var propTypes = component.propTypes || {} var props = defaults(optProps || {}, component.defaultProps || {}) var state = component.initialState ? component.initialState(props) : {} props.children = children; - - for (var name in propTypes) { - var options = propTypes[name] - if (options.source) { - props[name] = app.sources[options.source] - } - } - if (component.beforeMount) component.beforeMount({ props: props, state: state }) if (component.beforeRender) component.beforeRender({ props: props, state: state }) var node = component.render({ props: props, state: state }) diff --git a/test/dom/index.js b/test/dom/index.js index 646355f..233e8d7 100644 --- a/test/dom/index.js +++ b/test/dom/index.js @@ -1160,185 +1160,4 @@ if (document.body.createShadowRoot) { renderer.remove() document.body.removeChild(host) }) -} - -/** - * Sources. - * This feature will be removed in a future version. It's kept in now - * for backwards-compatibility - */ - -test('should set source without property type', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - const App = { - propTypes: { - foo: { - source: 'foo' - } - }, - render({props}) { - return ( -
- {props.foo || 'App'} -
- ); - } - } - - app.set('foo', 'bar') - mount() - equal(el.innerHTML, '
bar
') - teardown({renderer,el}) - end() -}) - -test('should handle removing entities', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - const App = { - propTypes: { - foo: { source: 'foo' } - }, - render({props}) { - let {foo} = props - let page = foo ? : - return
{page}
- } - } - - const Page1 = { - propTypes: { - foo: { source: 'foo' } - }, - render() { - return
Page1
- } - } - - const Page2 = { - propTypes: { - foo: { source: 'foo' } - }, - render(component) { - return
Page2
- } - } - - app.set('foo', 'bar'); - mount() - equal(el.innerHTML, '
Page2
') - app.set('foo', false); - equal(el.innerHTML, '
Page1
') - teardown({renderer,el}) - end() -}) - -test('should get default value from data value', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - var Test = { - propTypes: { - 'data': { source: 'meta' } - }, - render: function({props,state}) { - return
{props.data.title}
- } - } - - app.set('meta', { title: 'Hello World' }) - mount() - equal(el.innerHTML, '
Hello World
') - teardown({renderer,el}) - end() -}); - -test('should update with new value from data source', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - var Test = { - propTypes: { - text: { source: 'title' } - }, - render: function({props,state}) { - return
{props.text}
- } - } - - app.set('title', 'Hello World') - mount() - equal(el.innerHTML, '
Hello World
') - app.set('title', 'Hello Pluto') - equal(el.innerHTML, '
Hello Pluto
') - teardown({renderer,el}) - end() -}) - -test('should handle two-way updating', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - var Test = { - propTypes: { - 'text': { source: 'title' }, - 'updateTitle': { source: 'setTitle' } - }, - render: function({props,state}) { - return dom('div', { onClick: onClick }, props.text); - function onClick() { - props.updateTitle('Hello Pluto'); - } - } - } - - function setTitle(string) { - app.set('title', string); - } - - app.set('title', 'Hello World') - app.set('setTitle', setTitle) - app.mount() - equal(el.innerHTML, '
Hello World
') - trigger(el.querySelector('div'), 'click') - equal(el.innerHTML, '
Hello Pluto
') - teardown({renderer,el}) - end() -}) - -test('should handle two-way updating with multiple components depending on the same source', ({equal,end}) => { - var {mount,renderer,el,app} = setup(equal) - - var TestA = { - propTypes: { - 'text': { source: 'title' }, - 'updateTitle': { source: 'setTitle' } - }, - render: function({props,state}) { - return dom('span', { onClick: onClick }, props.text); - function onClick() { - props.updateTitle('Hello Pluto'); - } - } - } - - var TestB = { - propTypes: { - 'text': { source: 'title' }, - }, - render: function({props,state}) { - return dom('span', null, props.text); - } - } - - function setTitle(string) { - app.set('title', string); - } - - app.set('title', 'Hello World') - app.set('setTitle', setTitle) - app.mount(
) - equal(el.innerHTML, '
Hello WorldHello World
') - trigger(el.querySelector('span'), 'click') - equal(el.innerHTML, '
Hello PlutoHello Pluto
') - teardown({renderer,el}) - end() -}) +} \ No newline at end of file diff --git a/test/string/index.js b/test/string/index.js index 24f2233..1e95ec4 100644 --- a/test/string/index.js +++ b/test/string/index.js @@ -121,17 +121,4 @@ test('renderString: empty attributes', assert => { assert.equal(renderString(app), '', 'undefined attribute unexpectedly rendered') assert.end() -}) - -test('rendering data sources to a string', ({equal,end}) => { - var Component = { - propTypes: { - 'text': { source: 'text' } - }, - render: ({props,state}) =>
{props.text}
- } - var app = deku() - app.set('text', 'Hello World') - equal(renderString(app), '
Hello World
') - end() -}) +}) \ No newline at end of file From 8819cc5e691300a55ab8cf859963ec33282ae40b Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Tue, 18 Aug 2015 07:42:40 -0700 Subject: [PATCH 03/97] Removed shouldUpdate hook Just use memoization --- lib/render.js | 21 --------------------- test/dom/index.js | 29 ----------------------------- 2 files changed, 50 deletions(-) diff --git a/lib/render.js b/lib/render.js index ddbdf12..e44f0fa 100644 --- a/lib/render.js +++ b/lib/render.js @@ -293,12 +293,6 @@ function render (app, container, opts) { function updateEntity (entityId) { var entity = entities[entityId] - - if (!shouldUpdate(entity)) { - commit(entity) - return updateChildren(entityId) - } - var currentTree = entity.virtualElement var nextProps = entity.pendingProps var nextState = entity.pendingState @@ -967,21 +961,6 @@ function render (app, container, opts) { } } - /** - * Try to avoid creating new virtual dom if possible. - * - * Later we may expose this so you can override, but not there yet. - */ - - function shouldUpdate (entity) { - if (!entity.dirty) return false - if (!entity.component.shouldUpdate) return true - var nextProps = entity.pendingProps - var nextState = entity.pendingState - var bool = entity.component.shouldUpdate(entity.context, nextProps, nextState) - return bool - } - /** * Bind events for an element, and all it's rendered child elements. * diff --git a/test/dom/index.js b/test/dom/index.js index 233e8d7..eb146a8 100644 --- a/test/dom/index.js +++ b/test/dom/index.js @@ -355,10 +355,6 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m beforeMount ({props}) { log.push(props.name + ' beforeMount') }, - shouldUpdate ({props}) { - log.push(props.name + ' shouldUpdate') - return true - }, beforeUpdate ({props}) { log.push(props.name + ' beforeUpdate') }, @@ -432,17 +428,14 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m ) deepEqual(log, [ - 'GrandParent shouldUpdate', 'GrandParent beforeUpdate', 'GrandParent beforeRender', 'GrandParent validate', 'GrandParent render', - 'Parent shouldUpdate', 'Parent beforeUpdate', 'Parent beforeRender', 'Parent validate', 'Parent render', - 'Child shouldUpdate', 'Child beforeUpdate', 'Child beforeRender', 'Child validate', @@ -503,13 +496,6 @@ test('component lifecycle hook signatures', ({ok,end,equal}) => { ok(state.open === true, 'beforeMount has initial state') ok(id, 'beforeMount has id') }, - shouldUpdate ({props, state, id}, nextProps, nextState) { - ok(props.count === 0, 'shouldUpdate has current props') - ok(state.open === true, 'shouldUpdate has current state') - ok(nextProps.count === 0, 'shouldUpdate has next props') - ok(nextState.open === false, 'shouldUpdate has next state') - return true - }, beforeUpdate ({props, state, id}, nextProps, nextState) { ok(props.count === 0, 'beforeUpdate has props') ok(state.open === true, 'beforeUpdate has state') @@ -716,21 +702,6 @@ test('rendering new elements should be batched with state changes', ({equal,end} }) }) -test('skipping updates with shouldUpdate', ({equal,end,fail}) => { - var {mount,renderer,el} = setup(equal) - - var Test = { - afterUpdate: () => fail('component was updated'), - shouldUpdate: () => false, - render: () =>
- } - - mount() - mount() - teardown({renderer,el}) - end() -}) - test('skipping updates when the same virtual element is returned', ({equal,end,fail,pass}) => { var {mount,renderer,el} = setup(equal) var el =
From f5db3fc955abe4dec5edfba9fad62a57e818b038 Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Tue, 18 Aug 2015 23:57:03 -0700 Subject: [PATCH 04/97] Extracted DOM helpers --- lib/dom.js | 191 ++++++++++++++++++++++++++++++++++++++++++++ lib/render.js | 216 ++++---------------------------------------------- 2 files changed, 205 insertions(+), 202 deletions(-) create mode 100644 lib/dom.js diff --git a/lib/dom.js b/lib/dom.js new file mode 100644 index 0000000..dfb3916 --- /dev/null +++ b/lib/dom.js @@ -0,0 +1,191 @@ +var events = require('./events') +var svg = require('./svg') + +/** + * Retrieve the nearest 'body' ancestor of the given element or else the root + * element of the document in which stands the given element. + * + * This is necessary if you want to attach the events handler to the correct + * element and be able to dispatch events in document fragments such as + * Shadow DOM. + * + * @param {HTMLElement} el The element on which we will render an app. + * @return {HTMLElement} The root element on which we will attach the events + * handler. + */ + +exports.getRootElement = function (el) { + while (el.parentElement) { + if (el.tagName === 'BODY' || !el.parentElement) { + return el + } + el = el.parentElement + } + return el +} + +/** + * Set the value property of an element and keep the text selection + * for input fields. + * + * @param {HTMLElement} el + * @param {String} value + */ + +exports.setElementValue = function (el, value) { + if (el === document.activeElement && exports.canSelectText(el)) { + var start = el.selectionStart + var end = el.selectionEnd + el.value = value + el.setSelectionRange(start, end) + } else { + el.value = value + } +} + +/** + * For some reason only certain types of inputs can set the selection range. + * + * @param {HTMLElement} el + * + * @return {Boolean} + */ + +exports.canSelectText = function (el) { + return el.tagName === 'INPUT' && ['text','search','password','tel','url'].indexOf(el.type) > -1 +} + +/** + * Bind events for an element, and all it's rendered child elements. + * + * @param {String} path + * @param {String} event + * @param {Function} fn + */ + +exports.addEvent = function (el, eventType, fn) { + el.addEventListener(eventType, fn) +} + +/** + * Unbind events for a entityId + * + * @param {String} entityId + */ + +exports.removeEvent = function (el, eventType, fn) { + el.removeEventListener(eventType, fn) +} + +/** + * Is the DOM node an element node + * + * @param {HTMLElement} el + * + * @return {Boolean} + */ + +exports.isElement = function (el) { + return !!(el && el.tagName) +} + +/** + * Remove all the child nodes from an element + * + * @param {HTMLElement} el + */ + +exports.removeAllChildren = function (el) { + while (el.firstChild) el.removeChild(el.firstChild) +} + +/** + * Checks to see if one tree path is within + * another tree path. Example: + * + * 0.1 vs 0.1.1 = true + * 0.2 vs 0.3.5 = false + * + * @param {String} target + * @param {String} path + * + * @return {Boolean} + */ + +exports.isWithinPath = function (target, path) { + return path.indexOf(target + '.') === 0 +} + + +/** + * Set the attribute of an element, performing additional transformations + * dependning on the attribute name + * + * @param {HTMLElement} el + * @param {String} name + * @param {String} value + */ + +exports.setAttribute = function (el, name, value, previousValue) { + if (previousValue === value) { + return + } + if (!value) { + exports.removeAttribute(el, name, previousValue) + return + } + if (events[name]) { + if (previousValue) exports.removeEvent(el, events[name], previousValue) + exports.addEvent(el, events[name], value) + return + } + switch (name) { + case 'checked': + case 'disabled': + case 'selected': + el[name] = true + break + case 'innerHTML': + el.innerHTML = value + break + case 'value': + exports.setElementValue(el, value) + break + case svg.isAttribute(name): + el.setAttributeNS(svg.namespace, name, value) + break + default: + el.setAttribute(name, value) + break + } +} + +/** + * Remove an attribute, performing additional transformations + * dependning on the attribute name + * + * @param {HTMLElement} el + * @param {String} name + */ + +exports.removeAttribute = function (el, name, value) { + if (events[name] && value) { + exports.removeEvent(el, events[name], value) + return + } + switch (name) { + case 'checked': + case 'disabled': + case 'selected': + el[name] = false + break + case 'innerHTML': + el.innerHTML = '' + case 'value': + exports.setElementValue(el, null) + break + default: + el.removeAttribute(name) + break + } +} \ No newline at end of file diff --git a/lib/render.js b/lib/render.js index e44f0fa..c6f49e9 100644 --- a/lib/render.js +++ b/lib/render.js @@ -6,13 +6,23 @@ var raf = require('component-raf') var isDom = require('is-dom') var uid = require('get-uid') var keypath = require('object-path') -var events = require('./events') var svg = require('./svg') var defaults = require('object-defaults') var forEach = require('fast.js/forEach') var assign = require('fast.js/object/assign') var reduce = require('fast.js/reduce') var nodeType = require('./node-type') +var dom = require('./dom') +var setAttribute = dom.setAttribute +var removeAttribute = dom.removeAttribute +var isWithinPath = dom.isWithinPath +var getRootElement = dom.getRootElement +var removeAllChildren = dom.removeAllChildren +var isElement = dom.isElement +var addEvent = dom.addEvent +var removeEvent = dom.removeEvent +var canSelectText = dom.canSelectText +var setElementValue = dom.setElementValue /** * Expose `dom`. @@ -39,7 +49,6 @@ function render (app, container, opts) { var connections = {} var components = {} var entities = {} - var handlers = {} var mountQueue = [] var children = {} children[rootId] = {} @@ -630,16 +639,13 @@ function render (app, container, opts) { // add new attrs forEach(nextAttrs, function (value, name) { - var previousValue = prevAttrs[name] - if (events[name] || !(name in prevAttrs) || previousValue !== value) { - setAttribute(el, name, value, previousValue) - } + setAttribute(el, name, value, prevAttrs[name]) }) // remove old attrs forEach(prevAttrs, function (value, name) { if (!(name in nextAttrs)) { - removeAttribute(el, name, value) + removeAttribute(el, name, nextAttrs[name], value) } }) } @@ -692,7 +698,6 @@ function render (app, container, opts) { function removeElement (entityId, path, el) { var childrenByPath = children[entityId] var childId = childrenByPath[path] - var entityHandlers = handlers[entityId] || {} var removals = [] // If the path points to a component we should use that @@ -715,13 +720,6 @@ function render (app, container, opts) { removals.push(childPath) } }) - - // Remove all events at this path or below it - forEach(entityHandlers, function (fn, handlerPath) { - if (handlerPath === path || isWithinPath(path, handlerPath)) { - removeEvent(entityId, handlerPath) - } - }) } // Remove the paths from the object without touching the @@ -792,115 +790,6 @@ function render (app, container, opts) { } } - /** - * Set the attribute of an element, performing additional transformations - * dependning on the attribute name - * - * @param {HTMLElement} el - * @param {String} name - * @param {String} value - */ - - function setAttribute (el, name, value, previousValue) { - if (!value) { - removeAttribute(el, name, previousValue) - return - } - if (events[name]) { - if (previousValue) removeEvent(el, events[name], previousValue) - addEvent(el, events[name], value) - return - } - switch (name) { - case 'checked': - case 'disabled': - case 'selected': - el[name] = true - break - case 'innerHTML': - el.innerHTML = value - break - case 'value': - setElementValue(el, value) - break - case svg.isAttribute(name): - el.setAttributeNS(svg.namespace, name, value) - break - default: - el.setAttribute(name, value) - break - } - } - - /** - * Remove an attribute, performing additional transformations - * dependning on the attribute name - * - * @param {HTMLElement} el - * @param {String} name - */ - - function removeAttribute (el, name, value) { - if (events[name] && value) { - removeEvent(el, events[name], value) - return - } - switch (name) { - case 'checked': - case 'disabled': - case 'selected': - el[name] = false - break - case 'innerHTML': - el.innerHTML = '' - case 'value': - setElementValue(el, null) - break - default: - el.removeAttribute(name) - break - } - } - - /** - * Checks to see if one tree path is within - * another tree path. Example: - * - * 0.1 vs 0.1.1 = true - * 0.2 vs 0.3.5 = false - * - * @param {String} target - * @param {String} path - * - * @return {Boolean} - */ - - function isWithinPath (target, path) { - return path.indexOf(target + '.') === 0 - } - - /** - * Is the DOM node an element node - * - * @param {HTMLElement} el - * - * @return {Boolean} - */ - - function isElement (el) { - return !!(el && el.tagName) - } - - /** - * Remove all the child nodes from an element - * - * @param {HTMLElement} el - */ - - function removeAllChildren (el) { - while (el.firstChild) el.removeChild(el.firstChild) - } - /** * Trigger a hook on a component. * @@ -961,28 +850,6 @@ function render (app, container, opts) { } } - /** - * Bind events for an element, and all it's rendered child elements. - * - * @param {String} path - * @param {String} event - * @param {Function} fn - */ - - function addEvent (el, eventType, fn) { - el.addEventListener(eventType, fn) - } - - /** - * Unbind events for a entityId - * - * @param {String} entityId - */ - - function removeEvent (el, eventType, fn) { - el.removeEventListener(eventType, fn) - } - /** * Used for debugging to inspect the current state without * us needing to explicitly manage storing/updating references. @@ -993,7 +860,6 @@ function render (app, container, opts) { function inspect () { return { entities: entities, - handlers: handlers, currentElement: currentElement, options: options, app: app, @@ -1037,58 +903,4 @@ function Entity (component, props, ownerId) { this.virtualElement = null this.nativeElement = null this.displayName = component.name || 'Component' -} - -/** - * Retrieve the nearest 'body' ancestor of the given element or else the root - * element of the document in which stands the given element. - * - * This is necessary if you want to attach the events handler to the correct - * element and be able to dispatch events in document fragments such as - * Shadow DOM. - * - * @param {HTMLElement} el The element on which we will render an app. - * @return {HTMLElement} The root element on which we will attach the events - * handler. - */ - -function getRootElement (el) { - while (el.parentElement) { - if (el.tagName === 'BODY' || !el.parentElement) { - return el - } - el = el.parentElement - } - return el -} - -/** - * Set the value property of an element and keep the text selection - * for input fields. - * - * @param {HTMLElement} el - * @param {String} value - */ - -function setElementValue (el, value) { - if (el === document.activeElement && canSelectText(el)) { - var start = el.selectionStart - var end = el.selectionEnd - el.value = value - el.setSelectionRange(start, end) - } else { - el.value = value - } -} - -/** - * For some reason only certain types of inputs can set the selection range. - * - * @param {HTMLElement} el - * - * @return {Boolean} - */ - -function canSelectText (el) { - return el.tagName === 'INPUT' && ['text','search','password','tel','url'].indexOf(el.type) > -1 -} +} \ No newline at end of file From ddde87840afaae08c4644036adcb560caca11334 Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Wed, 19 Aug 2015 21:39:08 -0700 Subject: [PATCH 05/97] Removed setState --- lib/render.js | 103 +++--------------------------------- test/dom/index.js | 129 ++++++---------------------------------------- 2 files changed, 21 insertions(+), 211 deletions(-) diff --git a/lib/render.js b/lib/render.js index c6f49e9..a22350d 100644 --- a/lib/render.js +++ b/lib/render.js @@ -40,14 +40,11 @@ module.exports = render * @return {Object} */ -function render (app, container, opts) { - var frameId +function render (app, container) { var isRendering var rootId = 'root' var currentElement var currentNativeElement - var connections = {} - var components = {} var entities = {} var mountQueue = [] var children = {} @@ -57,16 +54,6 @@ function render (app, container, opts) { throw new Error('Container element must be a DOM element') } - /** - * Rendering options. Batching is only ever really disabled - * when running tests, and pooling can be disabled if the user - * is doing something stupid with the DOM in their components. - */ - - var options = defaults(assign({}, app.options || {}, opts || {}), { - batching: true - }) - /** * Listen to DOM events */ @@ -106,7 +93,7 @@ function render (app, container, opts) { */ function onmount () { - invalidate() + render() } /** @@ -179,41 +166,11 @@ function render (app, container, opts) { var component = entity.component var fn = typeof component === 'function' ? component : component.render if (!fn) throw new Error('Component needs a render function') - var result = fn(entity.context, setState(entity)) + var result = fn(entity.context) if (!result) throw new Error('Render function must return an element.') return result } - /** - * Whenever setState or setProps is called, we mark the entity - * as dirty in the renderer. This lets us optimize the re-rendering - * and skip components that definitely haven't changed. - * - * @param {Entity} entity - * - * @return {Function} A curried function for updating the state of an entity - */ - - function setState (entity) { - return function (nextState) { - updateEntityState(entity, nextState) - } - } - - /** - * Tell the app it's dirty and needs to re-render. If batching is disabled - * we can just trigger a render immediately, otherwise we'll wait until - * the next available frame. - */ - - function invalidate () { - if (!options.batching) { - if (!isRendering) render() - } else { - if (!frameId) frameId = raf(render) - } - } - /** * Update the DOM. If the update fails we stop the loop * so we don't get errors on every frame. @@ -222,21 +179,6 @@ function render (app, container, opts) { */ function render () { - // If this is called synchronously we need to - // cancel any pending future updates - clearFrame() - - // If the rendering from the previous frame is still going, - // we'll just wait until the next frame. Ideally renders should - // not take over 16ms to stay within a single frame, but this should - // catch it if it does. - if (isRendering) { - frameId = raf(render) - return - } else { - isRendering = true - } - // 1. If there isn't a native element rendered for the current mounted element // then we need to create it from scratch. // 2. If a new element has been mounted, we should diff them. @@ -262,10 +204,6 @@ function render (app, container, opts) { // Call mount events on all new entities flushMountQueue() - - // Allow rendering again. - isRendering = false - } /** @@ -278,20 +216,10 @@ function render (app, container, opts) { var entityId = mountQueue.shift() var entity = entities[entityId] trigger('afterRender', entity, [entity.context, entity.nativeElement]) - trigger('afterMount', entity, [entity.context, entity.nativeElement, setState(entity)]) + trigger('afterMount', entity, [entity.context, entity.nativeElement]) } } - /** - * Clear the current scheduled frame - */ - - function clearFrame () { - if (!frameId) return - raf.cancel(frameId) - frameId = 0 - } - /** * Update a component. * @@ -304,12 +232,10 @@ function render (app, container, opts) { var entity = entities[entityId] var currentTree = entity.virtualElement var nextProps = entity.pendingProps - var nextState = entity.pendingState - var previousState = entity.context.state var previousProps = entity.context.props // hook before rendering. could modify state just before the render occurs. - trigger('beforeUpdate', entity, [entity.context, nextProps, nextState]) + trigger('beforeUpdate', entity, [entity.context, nextProps]) trigger('beforeRender', entity, [entity.context]) // commit state and props. @@ -332,7 +258,7 @@ function render (app, container, opts) { trigger('afterRender', entity, [entity.context, entity.nativeElement]) // trigger afterUpdate after all children have updated. - trigger('afterUpdate', entity, [entity.context, previousProps, previousState, setState(entity)]) + trigger('afterUpdate', entity, [entity.context, previousProps]) } /** @@ -365,7 +291,6 @@ function render (app, container, opts) { */ function removeNativeElement () { - clearFrame() removeElement(rootId, '0', currentNativeElement) currentNativeElement = null } @@ -819,17 +744,6 @@ function render (app, container, opts) { var entity = entities[entityId] entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {}) entity.dirty = true - invalidate() - } - - /** - * Update component instance state. - */ - - function updateEntityState (entity, nextState) { - entity.pendingState = assign(entity.pendingState, nextState) - entity.dirty = true - invalidate() } /** @@ -838,11 +752,9 @@ function render (app, container, opts) { function commit (entity) { entity.context = { - state: entity.pendingState, props: entity.pendingProps, id: entity.id } - entity.pendingState = assign({}, entity.context.state) entity.pendingProps = assign({}, entity.context.props) entity.dirty = false if (typeof entity.component.validate === 'function') { @@ -861,7 +773,6 @@ function render (app, container, opts) { return { entities: entities, currentElement: currentElement, - options: options, app: app, container: container, children: children @@ -896,9 +807,7 @@ function Entity (component, props, ownerId) { this.context = {} this.context.id = this.id this.context.props = defaults(props || {}, component.defaultProps || {}) - this.context.state = this.component.initialState ? this.component.initialState(this.context.props) : {} this.pendingProps = assign({}, this.context.props) - this.pendingState = assign({}, this.context.state) this.dirty = false this.virtualElement = null this.nativeElement = null diff --git a/test/dom/index.js b/test/dom/index.js index eb146a8..9b78928 100644 --- a/test/dom/index.js +++ b/test/dom/index.js @@ -14,22 +14,6 @@ var ListItem = ({props}) =>
  • {props.children}
  • var Wrapper = ({props}) =>
    {props.children}
    var TwoWords = ({props}) => {props.one} {props.two} -var StateChangeOnMount = { - initialState: p => ({text: 'foo'}), - afterMount: (c,el,setState) => setState({ text: 'bar' }), - render: ({state}) => {state.text} -} - -var Delegate = function ({props,state}) { - var active = state.active || 0 - var items = [1,2,3].map(i => { -
  • setState({ active: i })}> - link -
  • - }) - return
      {items}
    -} - // Test helpers var div = function(){ @@ -314,20 +298,17 @@ test('components', ({equal,end}) => { var Test = { defaultProps: { name: 'Amanda' }, initialState: (props) => ({ text: 'Hello World' }), - render: ({props,state}) => {state.text}, - afterMount: (c, el, updateState) => updateState({ text: 'Hello Pluto' }) + render: ({props}) => Hello World, } mount() var root = el.firstElementChild equal(root.getAttribute('count'), '2', 'rendered with props') equal(root.getAttribute('name'), 'Amanda', 'has default props') - equal(root.innerHTML, 'Hello World', 'rendered with initial state') mount() equal(root.getAttribute('count'), '3', 'props updated') equal(root.getAttribute('name'), 'Amanda', 'default props still exist') - equal(root.innerHTML, 'Hello Pluto', 'rendered updated state') teardown({renderer,el}) equal(el.innerHTML, '', 'the element is removed') @@ -393,17 +374,14 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m ) deepEqual(log, [ - 'GrandParent initialState', 'GrandParent validate', 'GrandParent beforeMount', 'GrandParent beforeRender', 'GrandParent render', - 'Parent initialState', 'Parent validate', 'Parent beforeMount', 'Parent beforeRender', 'Parent render', - 'Child initialState', 'Child validate', 'Child beforeMount', 'Child beforeRender', @@ -486,64 +464,45 @@ test('component lifecycle hook signatures', ({ok,end,equal}) => { defaultProps: { count: 0 }, - initialState () { - return { - open: true - } - }, - beforeMount ({props, state, id}) { + beforeMount ({props, id}) { ok(props.count === 0, 'beforeMount has default props') - ok(state.open === true, 'beforeMount has initial state') ok(id, 'beforeMount has id') }, - beforeUpdate ({props, state, id}, nextProps, nextState) { + beforeUpdate ({props, id}, nextProps) { ok(props.count === 0, 'beforeUpdate has props') - ok(state.open === true, 'beforeUpdate has state') ok(id, 'beforeUpdate has id') }, - beforeRender ({props, state, id}) { + beforeRender ({props, id}) { ok(props, 'beforeRender has props') - ok(state, 'beforeRender has state') ok(id, 'beforeRender has id') }, - validate ({props, state, id}) { + validate ({props, id}) { ok(props, 'validate has props') - ok(state, 'validate has state') ok(id, 'validate has id') }, - render ({props, state, id}, setState) { + render ({props, id}) { ok(props, 'render has props') - ok(state, 'render has state') ok(id, 'render has id') - ok(typeof setState === 'function', 'render has state mutator') return
    }, - afterRender ({props, state, id}, el) { + afterRender ({props, id}, el) { ok(props, 'afterRender has props') - ok(state, 'afterRender has state') ok(id, 'afterRender has id') ok(el, 'afterRender has DOM element') }, - afterUpdate ({props, state, id}, prevProps, prevState, setState) { + afterUpdate ({props, id}, prevProps) { ok(props.count === 0, 'afterUpdate has current props') - ok(state.open === false, 'afterUpdate has current state') ok(prevProps.count === 0, 'afterUpdate has previous props') - ok(prevState.open === true, 'afterUpdate has previous state') - ok(typeof setState === 'function', 'afterUpdate can update state') ok(id, 'afterUpdate has id') }, - afterMount ({props, state, id}, el, setState) { + afterMount ({props, id}, el) { ok(props, 'afterMount has props') - ok(state, 'afterMount has state') ok(id, 'afterMount has id') ok(el, 'afterMount has DOM element') - ok(typeof setState === 'function', 'afterMount can update state') ok(document.getElementById('foo'), 'element is in the DOM') - setState({ open: false }) }, - beforeUnmount ({props, state, id}, el) { + beforeUnmount ({props, id}, el) { ok(props, 'beforeUnmount has props') - ok(state, 'beforeUnmount has state') ok(id, 'beforeUnmount has id') ok(el, 'beforeUnmount has el') end() @@ -569,14 +528,14 @@ test(`should update all children when a parent component changes`, ({equal,end}) var childCalls = 0 var Child = { - render: function({props, state}){ + render: function({props}){ childCalls++ return {props.text} } } var Parent = { - render: function({props, state}){ + render: function({props}){ parentCalls++ return (
    @@ -594,20 +553,7 @@ test(`should update all children when a parent component changes`, ({equal,end}) end() }) -test('update nested components when state changes', assert => { - var app = deku(); - app.mount() - var container = div() - var rendered = render(app, container) - assert.equal(container.innerHTML, '
    foo
    ', 'initial render') - raf(function(){ - assert.equal(container.innerHTML, '
    bar
    ', 'updated on the next frame') - rendered.remove() - assert.end() - }) -}) - -test('batched rendering', assert => { +test.skip('batched rendering', assert => { var i = 0 var IncrementAfterUpdate = { render: function(){ @@ -657,51 +603,6 @@ test('rendering nested components', ({equal,end}) => { end() }) -test('rendering new elements should be batched with state changes', ({equal,end}) => { - var app = deku() - var el = div() - var renderer = render(app, el) - var mount = app.mount.bind(app) - var unmount = app.unmount.bind(app) - var emitter = new Emitter() - var i = 0 - - var ComponentA = { - initialState: function(){ - return { - text: 'Deku Shield' - } - }, - afterMount: function(component, el, updateState) { - emitter.on('data', text => updateState({ text: text })) - }, - render: function({props,state}){ - i++ - return
    {props.text} {state.text}
    - } - } - - var ComponentB = { - render: function({props,state}){ - i++ - return
    - } - } - - mount() - - raf(function(){ - emitter.emit('data', 'Mirror Shield') - mount() - raf(function(){ - equal(i, 4, 'rendering was batched') - equal(el.innerHTML, `
    3x Mirror Shield
    `, 'rendered correctly') - teardown({renderer,el}) - end() - }) - }) -}) - test('skipping updates when the same virtual element is returned', ({equal,end,fail,pass}) => { var {mount,renderer,el} = setup(equal) var el =
    @@ -749,7 +650,7 @@ test('unmount sub-components that move themselves in the DOM', ({equal,end}) => } var Parent = { - render: ({props, state}) => { + render: ({props}) => { if (props.show) { return (
    @@ -809,7 +710,7 @@ test('replacing components with other components', ({equal,end}) => { var ComponentA = () =>
    A
    var ComponentB = () =>
    B
    - var ComponentC = ({props,state}) => { + var ComponentC = ({props}) => { if (props.type === 'A') { return } else { From 615b169569ded3eb812b285aead1c63f18cda14b Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Thu, 20 Aug 2015 08:02:38 -0700 Subject: [PATCH 06/97] Removed from more cruft --- lib/render.js | 125 +++++++++++++++----------------------------------- 1 file changed, 36 insertions(+), 89 deletions(-) diff --git a/lib/render.js b/lib/render.js index a22350d..21eac9d 100644 --- a/lib/render.js +++ b/lib/render.js @@ -16,7 +16,6 @@ var dom = require('./dom') var setAttribute = dom.setAttribute var removeAttribute = dom.removeAttribute var isWithinPath = dom.isWithinPath -var getRootElement = dom.getRootElement var removeAllChildren = dom.removeAllChildren var isElement = dom.isElement var addEvent = dom.addEvent @@ -41,7 +40,6 @@ module.exports = render */ function render (app, container) { - var isRendering var rootId = 'root' var currentElement var currentNativeElement @@ -54,18 +52,13 @@ function render (app, container) { throw new Error('Container element must be a DOM element') } - /** - * Listen to DOM events - */ - var rootElement = getRootElement(container) - /** * Watch for changes to the app so that we can update * the DOM as needed. */ app.on('unmount', onunmount) - app.on('mount', onmount) + app.on('mount', render) /** * If the app has already mounted an element, we can just @@ -82,18 +75,7 @@ function render (app, container) { function teardown () { removeNativeElement() app.off('unmount', onunmount) - app.off('mount', onmount) - } - - /** - * Swap the current rendered node with a new one that is rendered - * from the new virtual element mounted on the app. - * - * @param {VirtualElement} element - */ - - function onmount () { - render() + app.off('mount', render) } /** @@ -106,39 +88,6 @@ function render (app, container) { currentElement = null } - /** - * Render and mount a component to the native dom. - * - * @param {Entity} entity - * @return {HTMLElement} - */ - - function mountEntity (entity) { - children[entity.id] = {} - entities[entity.id] = entity - - // commit initial state and props. - commit(entity) - - // callback before mounting. - trigger('beforeMount', entity, [entity.context]) - trigger('beforeRender', entity, [entity.context]) - - // render virtual element. - var virtualElement = renderEntity(entity) - // create native element. - var nativeElement = toNative(entity.id, '0', virtualElement) - - entity.virtualElement = virtualElement - entity.nativeElement = nativeElement - - // Fire afterRender and afterMount hooks at the end - // of the render cycle - mountQueue.push(entity.id) - - return nativeElement - } - /** * Remove a component from the native dom. * @@ -194,12 +143,9 @@ function render (app, container) { } removeAllChildren(container) container.appendChild(currentNativeElement) - } else if (currentElement !== app.element) { + } else { currentNativeElement = patch(rootId, currentElement, app.element, currentNativeElement) currentElement = app.element - updateChildren(rootId) - } else { - updateChildren(rootId) } // Call mount events on all new entities @@ -252,7 +198,6 @@ function render (app, container) { // apply new virtual tree to native dom. entity.nativeElement = patch(entityId, currentTree, nextTree, entity.nativeElement) entity.virtualElement = nextTree - updateChildren(entityId) // trigger render hook trigger('afterRender', entity, [entity.context, entity.nativeElement]) @@ -367,10 +312,34 @@ function render (app, container) { * Create a native element from a component. */ - function toNativeComponent (entityId, path, vnode) { - var child = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), entityId) - children[entityId][path] = child.id - return mountEntity(child) + function toNativeComponent (ownerId, path, vnode) { + var entity = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), ownerId) + + children[ownerId][path] = entity.id + children[entity.id] = {} + entities[entity.id] = entity + vnode.entityId = entity.id + + // commit initial state and props. + commit(entity) + + // callback before mounting. + trigger('beforeMount', entity, [entity.context]) + trigger('beforeRender', entity, [entity.context]) + + // render virtual element. + var virtualElement = renderEntity(entity) + // create native element. + var nativeElement = toNative(entity.id, '0', virtualElement) + + entity.virtualElement = virtualElement + entity.nativeElement = nativeElement + + // Fire afterRender and afterMount hooks at the end + // of the render cycle + mountQueue.push(entity.id) + + return nativeElement } /** @@ -586,13 +555,13 @@ function render (app, container) { return replaceElement(entityId, path, el, next) } else { var targetId = children[entityId][path] - - // This is a hack for now if (targetId) { updateEntityProps(targetId, assign({ children: next.children }, next.attributes)) + updateEntity(targetId) + return entities[targetId].nativeElement + } else { + return el } - - return el } } @@ -743,7 +712,6 @@ function render (app, container) { function updateEntityProps (entityId, nextProps) { var entity = entities[entityId] entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {}) - entity.dirty = true } /** @@ -756,37 +724,18 @@ function render (app, container) { id: entity.id } entity.pendingProps = assign({}, entity.context.props) - entity.dirty = false if (typeof entity.component.validate === 'function') { entity.component.validate(entity.context) } } - /** - * Used for debugging to inspect the current state without - * us needing to explicitly manage storing/updating references. - * - * @return {Object} - */ - - function inspect () { - return { - entities: entities, - currentElement: currentElement, - app: app, - container: container, - children: children - } - } - /** * Return an object that lets us completely remove the automatic * DOM rendering and export debugging tools. */ return { - remove: teardown, - inspect: inspect + remove: teardown } } @@ -808,8 +757,6 @@ function Entity (component, props, ownerId) { this.context.id = this.id this.context.props = defaults(props || {}, component.defaultProps || {}) this.pendingProps = assign({}, this.context.props) - this.dirty = false this.virtualElement = null this.nativeElement = null - this.displayName = component.name || 'Component' } \ No newline at end of file From 4bc7f79b73622500452fec86fe227966832411d7 Mon Sep 17 00:00:00 2001 From: Anthony Short Date: Thu, 20 Aug 2015 08:34:32 -0700 Subject: [PATCH 07/97] Breaking changes * props are now passed to the component, instead of { props, id }. Keeping the uid in there for components would encourage people to have state that's mapped to the component tree, which is bad practice * Removed beforeMount. This is mainly used to trigger loading data, but this should live outside of the components anyway. * Removed beforeRender. Not really used, side-effects should come at the end. * afterUpdate is removed. Any state changes you could have performed in here should live outside of the components. Things like Redux can manage this state instead. --- lib/render.js | 76 ++++++--------------------- test/dom/index.js | 130 ++++++++++++++-------------------------------- 2 files changed, 56 insertions(+), 150 deletions(-) diff --git a/lib/render.js b/lib/render.js index 21eac9d..9d510c0 100644 --- a/lib/render.js +++ b/lib/render.js @@ -97,7 +97,7 @@ function render (app, container) { function unmountEntity (entityId) { var entity = entities[entityId] if (!entity) return - trigger('beforeUnmount', entity, [entity.context, entity.nativeElement]) + trigger('beforeUnmount', entity, [entity.props, entity.nativeElement]) unmountChildren(entityId) delete entities[entityId] delete children[entityId] @@ -115,7 +115,7 @@ function render (app, container) { var component = entity.component var fn = typeof component === 'function' ? component : component.render if (!fn) throw new Error('Component needs a render function') - var result = fn(entity.context) + var result = fn(entity.props) if (!result) throw new Error('Render function must return an element.') return result } @@ -161,8 +161,8 @@ function render (app, container) { while (mountQueue.length > 0) { var entityId = mountQueue.shift() var entity = entities[entityId] - trigger('afterRender', entity, [entity.context, entity.nativeElement]) - trigger('afterMount', entity, [entity.context, entity.nativeElement]) + trigger('afterRender', entity, [entity.props, entity.nativeElement]) + trigger('afterMount', entity, [entity.props, entity.nativeElement]) } } @@ -177,15 +177,6 @@ function render (app, container) { function updateEntity (entityId) { var entity = entities[entityId] var currentTree = entity.virtualElement - var nextProps = entity.pendingProps - var previousProps = entity.context.props - - // hook before rendering. could modify state just before the render occurs. - trigger('beforeUpdate', entity, [entity.context, nextProps]) - trigger('beforeRender', entity, [entity.context]) - - // commit state and props. - commit(entity) // re-render. var nextTree = renderEntity(entity) @@ -200,10 +191,7 @@ function render (app, container) { entity.virtualElement = nextTree // trigger render hook - trigger('afterRender', entity, [entity.context, entity.nativeElement]) - - // trigger afterUpdate after all children have updated. - trigger('afterUpdate', entity, [entity.context, previousProps]) + trigger('afterRender', entity, [entity.props, entity.nativeElement]) } /** @@ -313,30 +301,21 @@ function render (app, container) { */ function toNativeComponent (ownerId, path, vnode) { - var entity = new Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), ownerId) + var entity = Entity(vnode.type, assign({ children: vnode.children }, vnode.attributes), ownerId) children[ownerId][path] = entity.id children[entity.id] = {} entities[entity.id] = entity vnode.entityId = entity.id - // commit initial state and props. - commit(entity) - - // callback before mounting. - trigger('beforeMount', entity, [entity.context]) - trigger('beforeRender', entity, [entity.context]) + trigger('validate', entity, [entity.props]) - // render virtual element. var virtualElement = renderEntity(entity) - // create native element. var nativeElement = toNative(entity.id, '0', virtualElement) entity.virtualElement = virtualElement entity.nativeElement = nativeElement - // Fire afterRender and afterMount hooks at the end - // of the render cycle mountQueue.push(entity.id) return nativeElement @@ -711,22 +690,8 @@ function render (app, container) { function updateEntityProps (entityId, nextProps) { var entity = entities[entityId] - entity.pendingProps = defaults({}, nextProps, entity.component.defaultProps || {}) - } - - /** - * Commit props and state changes to an entity. - */ - - function commit (entity) { - entity.context = { - props: entity.pendingProps, - id: entity.id - } - entity.pendingProps = assign({}, entity.context.props) - if (typeof entity.component.validate === 'function') { - entity.component.validate(entity.context) - } + entity.props = defaults({}, nextProps, entity.component.defaultProps || {}) + trigger('validate', entity, [entity.props]) } /** @@ -741,22 +706,15 @@ function render (app, container) { /** * A rendered component instance. - * - * This manages the lifecycle, props and state of the component. - * It's basically just a data object for more straightfoward lookup. - * - * @param {Component} component - * @param {Object} props */ function Entity (component, props, ownerId) { - this.id = uid() - this.ownerId = ownerId - this.component = component - this.context = {} - this.context.id = this.id - this.context.props = defaults(props || {}, component.defaultProps || {}) - this.pendingProps = assign({}, this.context.props) - this.virtualElement = null - this.nativeElement = null + return { + id: uid(), + ownerId: ownerId, + component: component, + props: defaults(props || {}, component.defaultProps || {}), + virtualElement: null, + nativeElement: null + } } \ No newline at end of file diff --git a/test/dom/index.js b/test/dom/index.js index 9b78928..1626c4d 100644 --- a/test/dom/index.js +++ b/test/dom/index.js @@ -9,10 +9,10 @@ import test from 'tape' // Test Components -var RenderChildren = ({props}) => props.children[0] -var ListItem = ({props}) =>
  • {props.children}
  • -var Wrapper = ({props}) =>
    {props.children}
    -var TwoWords = ({props}) => {props.one} {props.two} +var RenderChildren = props => props.children[0] +var ListItem = props =>
  • {props.children}
  • +var Wrapper = props =>
    {props.children}
    +var TwoWords = props => {props.one} {props.two} // Test helpers @@ -298,7 +298,7 @@ test('components', ({equal,end}) => { var Test = { defaultProps: { name: 'Amanda' }, initialState: (props) => ({ text: 'Hello World' }), - render: ({props}) => Hello World, + render: (props) => Hello World, } mount() @@ -317,7 +317,7 @@ test('components', ({equal,end}) => { test('simple components', ({equal,end}) => { var {el,renderer,mount,html} = setup(equal) - var Box = ({props}) =>
    {props.text}
    + var Box = (props) =>
    {props.text}
    mount() html('
    Hello World
    ', 'function component rendered') teardown({renderer,el}) @@ -329,36 +329,20 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m var log = [] var LifecycleLogger = { - initialState (props) { - log.push(props.name + ' initialState') - return {} - }, - beforeMount ({props}) { - log.push(props.name + ' beforeMount') - }, - beforeUpdate ({props}) { - log.push(props.name + ' beforeUpdate') - }, - beforeRender ({props}) { - log.push(props.name + ' beforeRender') - }, - validate ({props}) { + validate (props) { log.push(props.name + ' validate') }, - render ({props}) { + render (props) { log.push(props.name + ' render') return
    {props.children}
    }, - afterRender ({props}) { + afterRender (props) { log.push(props.name + ' afterRender') }, - afterUpdate ({props}) { - log.push(props.name + ' afterUpdate') - }, - afterMount ({props}) { + afterMount (props) { log.push(props.name + ' afterMount') }, - beforeUnmount ({props}, el) { + beforeUnmount (props, el) { log.push(props.name + ' beforeUnmount') } } @@ -375,16 +359,10 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m deepEqual(log, [ 'GrandParent validate', - 'GrandParent beforeMount', - 'GrandParent beforeRender', 'GrandParent render', 'Parent validate', - 'Parent beforeMount', - 'Parent beforeRender', 'Parent render', 'Child validate', - 'Child beforeMount', - 'Child beforeRender', 'Child render', 'Child afterRender', 'Child afterMount', @@ -406,24 +384,15 @@ test('nested component lifecycle hooks fire in the correct order', ({deepEqual,m ) deepEqual(log, [ - 'GrandParent beforeUpdate', - 'GrandParent beforeRender', 'GrandParent validate', 'GrandParent render', - 'Parent beforeUpdate', - 'Parent beforeRender', 'Parent validate', 'Parent render', - 'Child beforeUpdate', - 'Child beforeRender', 'Child validate', 'Child render', 'Child afterRender', - 'Child afterUpdate', 'Parent afterRender', - 'Parent afterUpdate', - 'GrandParent afterRender', - 'GrandParent afterUpdate' + 'GrandParent afterRender' ], 'updated') log = [] @@ -464,46 +433,24 @@ test('component lifecycle hook signatures', ({ok,end,equal}) => { defaultProps: { count: 0 }, - beforeMount ({props, id}) { - ok(props.count === 0, 'beforeMount has default props') - ok(id, 'beforeMount has id') - }, - beforeUpdate ({props, id}, nextProps) { - ok(props.count === 0, 'beforeUpdate has props') - ok(id, 'beforeUpdate has id') - }, - beforeRender ({props, id}) { - ok(props, 'beforeRender has props') - ok(id, 'beforeRender has id') - }, - validate ({props, id}) { + validate (props) { ok(props, 'validate has props') - ok(id, 'validate has id') }, - render ({props, id}) { + render (props) { ok(props, 'render has props') - ok(id, 'render has id') return
    }, - afterRender ({props, id}, el) { + afterRender (props, el) { ok(props, 'afterRender has props') - ok(id, 'afterRender has id') ok(el, 'afterRender has DOM element') }, - afterUpdate ({props, id}, prevProps) { - ok(props.count === 0, 'afterUpdate has current props') - ok(prevProps.count === 0, 'afterUpdate has previous props') - ok(id, 'afterUpdate has id') - }, - afterMount ({props, id}, el) { + afterMount (props, el) { ok(props, 'afterMount has props') - ok(id, 'afterMount has id') ok(el, 'afterMount has DOM element') ok(document.getElementById('foo'), 'element is in the DOM') }, - beforeUnmount ({props, id}, el) { + beforeUnmount (props, el) { ok(props, 'beforeUnmount has props') - ok(id, 'beforeUnmount has id') ok(el, 'beforeUnmount has el') end() } @@ -528,14 +475,14 @@ test(`should update all children when a parent component changes`, ({equal,end}) var childCalls = 0 var Child = { - render: function({props}){ + render: function(props){ childCalls++ return {props.text} } } var Parent = { - render: function({props}){ + render: function(props){ parentCalls++ return (
    @@ -579,10 +526,10 @@ test.skip('batched rendering', assert => { test('rendering nested components', ({equal,end}) => { var {mount,renderer,el,html} = setup(equal) - var ComponentA = ({props}) =>
    {props.children}
    - var ComponentB = ({props}) =>
    {props.children}
    + var ComponentA = (props) =>
    {props.children}
    + var ComponentB = (props) =>
    {props.children}
    - var ComponentC = ({props}) => { + var ComponentC = (props) => { return (
    @@ -606,13 +553,15 @@ test('rendering nested components', ({equal,end}) => { test('skipping updates when the same virtual element is returned', ({equal,end,fail,pass}) => { var {mount,renderer,el} = setup(equal) var el =
    + var i = 0 var Component = { render (component) { + i++ return el }, - afterUpdate () { - fail('component was updated') + afterRender () { + if (i === 2) fail('component was updated') } } @@ -650,7 +599,7 @@ test('unmount sub-components that move themselves in the DOM', ({equal,end}) => } var Parent = { - render: ({props}) => { + render: (props) => { if (props.show) { return (
    @@ -678,11 +627,10 @@ test('firing mount events on sub-components created later', ({equal,pass,end,pla var ComponentA = { render: () =>
    , beforeUnmount: () => pass('beforeUnmount called'), - beforeMount: () => pass('beforeMount called'), afterMount: () => pass('afterMount called') } - plan(3) + plan(2) mount() mount(
    ) teardown({renderer,el}) @@ -692,8 +640,8 @@ test('firing mount events on sub-components created later', ({equal,pass,end,pla test('should change root node and still update correctly', ({equal,end}) => { var {mount,html,renderer,el} = setup(equal) - var ComponentA = ({props}) => dom(props.type, null, props.text) - var Test = ({props}) => + var ComponentA = (props) => dom(props.type, null, props.text) + var Test = (props) => mount() html('test') @@ -710,7 +658,7 @@ test('replacing components with other components', ({equal,end}) => { var ComponentA = () =>
    A
    var ComponentB = () =>
    B
    - var ComponentC = ({props}) => { + var ComponentC = (props) => { if (props.type === 'A') { return } else { @@ -733,7 +681,7 @@ test('adding, removing and updating events', ({equal,end}) => { var onclickb = () => count -= 1 var Page = { - render: ({props}) => + render: (props) => } mount() @@ -754,7 +702,7 @@ test('should bubble events', ({equal,end,fail,ok}) => { var state = {} var Test = { - render: function ({props}) { + render: function (props) { let state = props.state return (
    @@ -804,7 +752,7 @@ test('update sub-components with the same element', ({equal,end}) => { var {mount,renderer,el} = setup(equal) let Page1 = { - render({ props }) { + render(props) { return ( @@ -827,7 +775,7 @@ test('update sub-components with the same element', ({equal,end}) => { } } - let Page2 = ({props}) => { + let Page2 = (props) => { return (
    {props.title} @@ -835,7 +783,7 @@ test('update sub-components with the same element', ({equal,end}) => { ) } - let App = ({props}) => props.page === 1 ? : + let App = (props) => props.page === 1 ? : mount() mount() @@ -979,7 +927,7 @@ test('updating event handlers when children are removed', ({equal,end}) => { var {mount,renderer,el} = setup(equal) var items = ['foo','bar','baz'] - var ListItem = ({props}) => { + var ListItem = (props) => { return (
  • items.splice(props.index, 1)} /> @@ -987,7 +935,7 @@ test('updating event handlers when children are removed', ({equal,end}) => { ) } - var List = ({props}) => { + var List = (props) => { return (