-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Factor codebase into modules (addresses #3)
- Loading branch information
Showing
18 changed files
with
1,148 additions
and
1,108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { hook } from './hooks'; | ||
import { extend } from './util'; | ||
import { createLinkedState } from './linked-state'; | ||
import { triggerComponentRender, setComponentProps } from './vdom/component'; | ||
|
||
/** Base Component class, for he ES6 Class method of creating Components | ||
* @public | ||
* | ||
* @example | ||
* class MyFoo extends Component { | ||
* render(props, state) { | ||
* return <div />; | ||
* } | ||
* } | ||
*/ | ||
export default function Component(props, context) { | ||
/** @private */ | ||
this._dirty = this._disableRendering = false; | ||
/** @private */ | ||
this._linkedStates = {}; | ||
/** @private */ | ||
this._renderCallbacks = []; | ||
/** @public */ | ||
this.prevState = this.prevProps = this.prevContext = this.base = null; | ||
/** @public */ | ||
this.context = context || null; | ||
/** @type {object} */ | ||
this.props = props || hook(this, 'getDefaultProps') || {}; | ||
/** @type {object} */ | ||
this.state = hook(this, 'getInitialState') || {}; | ||
} | ||
|
||
|
||
extend(Component.prototype, { | ||
|
||
/** Returns a `boolean` value indicating if the component should re-render when receiving the given `props` and `state`. | ||
* @param {object} nextProps | ||
* @param {object} nextState | ||
* @param {object} nextContext | ||
* @returns {Boolean} should the component re-render | ||
* @name shouldComponentUpdate | ||
* @function | ||
*/ | ||
// shouldComponentUpdate() { | ||
// return true; | ||
// }, | ||
|
||
|
||
/** Returns a function that sets a state property when called. | ||
* Calling linkState() repeatedly with the same arguments returns a cached link function. | ||
* | ||
* Provides some built-in special cases: | ||
* - Checkboxes and radio buttons link their boolean `checked` value | ||
* - Inputs automatically link their `value` property | ||
* - Event paths fall back to any associated Component if not found on an element | ||
* - If linked value is a function, will invoke it and use the result | ||
* | ||
* @param {string} key The path to set - can be a dot-notated deep key | ||
* @param {string} [eventPath] If set, attempts to find the new state value at a given dot-notated path within the object passed to the linkedState setter. | ||
* @returns {function} linkStateSetter(e) | ||
* | ||
* @example Update a "text" state value when an input changes: | ||
* <input onChange={ this.linkState('text') } /> | ||
* | ||
* @example Set a deep state value on click | ||
* <button onClick={ this.linkState('touch.coords', 'touches.0') }>Tap</button | ||
*/ | ||
linkState(key, eventPath) { | ||
let c = this._linkedStates, | ||
cacheKey = key + '|' + (eventPath || ''); | ||
return c[cacheKey] || (c[cacheKey] = createLinkedState(this, key, eventPath)); | ||
}, | ||
|
||
|
||
/** Update component state by copying properties from `state` to `this.state`. | ||
* @param {object} state A hash of state properties to update with new values | ||
*/ | ||
setState(state, callback) { | ||
let s = this.state; | ||
if (!this.prevState) this.prevState = extend({}, s); | ||
extend(s, typeof state==='function' ? state(s, this.props) : state); | ||
if (callback) this._renderCallbacks.push(callback); | ||
triggerComponentRender(this); | ||
}, | ||
|
||
|
||
/** @private */ | ||
setProps(props, opts) { | ||
return setComponentProps(this, props, opts); | ||
}, | ||
|
||
|
||
/** Accepts `props` and `state`, and returns a new Virtual DOM tree to build. | ||
* Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx). | ||
* @param {object} props Props (eg: JSX attributes) received from parent element/component | ||
* @param {object} state The component's current state | ||
* @returns VNode | ||
*/ | ||
render() { | ||
// return h('div', null, props.children); | ||
return null; | ||
} | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// render modes | ||
export const NO_RENDER = { render: false }; | ||
export const SYNC_RENDER = { renderSync: true }; | ||
export const DOM_RENDER = { build: true }; | ||
|
||
export const EMPTY = {}; | ||
export const EMPTY_BASE = ''; | ||
|
||
// is this a DOM environment | ||
export const HAS_DOM = typeof document!=='undefined'; | ||
export const TEXT_CONTENT = !HAS_DOM || 'textContent' in document ? 'textContent' : 'nodeValue'; | ||
|
||
export const ATTR_KEY = '__preactattr_'; | ||
|
||
// DOM properties that should NOT have "px" added when numeric | ||
export const NON_DIMENSION_PROPS = { | ||
boxFlex:1,boxFlexGroup:1,columnCount:1,fillOpacity:1,flex:1,flexGrow:1, | ||
flexPositive:1,flexShrink:1,flexNegative:1,fontWeight:1,lineClamp:1,lineHeight:1, | ||
opacity:1,order:1,orphans:1,strokeOpacity:1,widows:1,zIndex:1,zoom:1 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { ATTR_KEY, EMPTY } from '../constants'; | ||
import { hasOwnProperty, memoize } from '../util'; | ||
import options from '../options'; | ||
import { hook } from '../hooks'; | ||
|
||
/** Append multiple children to a Node. | ||
* Uses a Document Fragment to batch when appending 2 or more children | ||
* @private | ||
*/ | ||
export function appendChildren(parent, children) { | ||
let len = children.length; | ||
if (len<=2) { | ||
parent.appendChild(children[0]); | ||
if (len===2) parent.appendChild(children[1]); | ||
return; | ||
} | ||
|
||
let frag = document.createDocumentFragment(); | ||
for (let i=0; i<len; i++) frag.appendChild(children[i]); | ||
parent.appendChild(frag); | ||
} | ||
|
||
|
||
|
||
/** Retrieve the value of a rendered attribute | ||
* @private | ||
*/ | ||
export function getAccessor(node, name, value) { | ||
if (name!=='type' && name in node) return node[name]; | ||
if (name==='class') return node.className; | ||
if (name==='style') return node.style.cssText; | ||
let attrs = node[ATTR_KEY]; | ||
if (hasOwnProperty.call(attrs, name)) return attrs[name]; | ||
return value; | ||
} | ||
|
||
|
||
|
||
/** Set a named attribute on the given Node, with special behavior for some names and event handlers. | ||
* If `value` is `null`, the attribute/handler will be removed. | ||
* @param {Element} node An element to mutate | ||
* @param {string} name The name/key to set, such as an event or attribute name | ||
* @param {any} value An attribute value, such as a function to be used as an event handler | ||
* @param {any} previousValue The last value that was set for this name/node pair | ||
* @private | ||
*/ | ||
export function setAccessor(node, name, value) { | ||
if (name==='class') { | ||
node.className = value; | ||
} | ||
else if (name==='style') { | ||
node.style.cssText = value; | ||
} | ||
else if (name==='dangerouslySetInnerHTML') { | ||
node.innerHTML = value.__html; | ||
} | ||
else if (name in node && name!=='type') { | ||
node[name] = value; | ||
} | ||
else { | ||
setComplexAccessor(node, name, value); | ||
} | ||
|
||
node[ATTR_KEY][name] = getAccessor(node, name, value); | ||
} | ||
|
||
|
||
/** For props without explicit behavior, apply to a Node as event handlers or attributes. | ||
* @private | ||
*/ | ||
function setComplexAccessor(node, name, value) { | ||
if (name.substring(0,2)==='on') { | ||
let type = normalizeEventName(name), | ||
l = node._listeners || (node._listeners = {}), | ||
fn = !l[type] ? 'add' : !value ? 'remove' : null; | ||
if (fn) node[fn+'EventListener'](type, eventProxy); | ||
l[type] = value; | ||
return; | ||
} | ||
|
||
let type = typeof value; | ||
if (value===null) { | ||
node.removeAttribute(name); | ||
} | ||
else if (type!=='function' && type!=='object') { | ||
node.setAttribute(name, value); | ||
} | ||
} | ||
|
||
|
||
|
||
/** Proxy an event to hooked event handlers | ||
* @private | ||
*/ | ||
function eventProxy(e) { | ||
let fn = this._listeners[normalizeEventName(e.type)]; | ||
if (fn) return fn.call(this, hook(options, 'event', e) || e); | ||
} | ||
|
||
|
||
|
||
/** Convert an Event name/type to lowercase and strip any "on*" prefix. | ||
* @function | ||
* @private | ||
*/ | ||
let normalizeEventName = memoize(t => t.replace(/^on/i,'').toLowerCase()); | ||
|
||
|
||
|
||
/** Get a hashmap of node properties, preferring preact's cached property values over the DOM's | ||
* @private | ||
*/ | ||
export function getNodeAttributes(node) { | ||
return node[ATTR_KEY] || getRawNodeAttributes(node) || EMPTY; | ||
// let list = getRawNodeAttributes(node), | ||
// l = node[ATTR_KEY]; | ||
// return l && list ? extend(list, l) : (l || list || EMPTY); | ||
} | ||
|
||
|
||
/** Get a node's attributes as a hashmap, regardless of type. | ||
* @private | ||
*/ | ||
function getRawNodeAttributes(node) { | ||
let list = node.attributes; | ||
if (!list || !list.getNamedItem) return list; | ||
if (list.length) return getAttributesAsObject(list); | ||
} | ||
|
||
|
||
/** Convert a DOM `.attributes` NamedNodeMap to a hashmap. | ||
* @private | ||
*/ | ||
function getAttributesAsObject(list) { | ||
let attrs; | ||
for (let i=list.length; i--; ) { | ||
let item = list[i]; | ||
if (!attrs) attrs = {}; | ||
attrs[item.name] = item.value; | ||
} | ||
return attrs; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { ATTR_KEY } from '../constants'; | ||
import { hasOwnProperty, memoize } from '../util'; | ||
import { setAccessor } from './index'; | ||
|
||
/** DOM node pool, keyed on nodeName. */ | ||
|
||
let nodes = {}; | ||
|
||
let normalizeName = memoize(name => name.toUpperCase()); | ||
|
||
|
||
export function collectNode(node) { | ||
cleanNode(node); | ||
let name = normalizeName(node.nodeName), | ||
list = nodes[name]; | ||
if (list) list.push(node); | ||
else nodes[name] = [node]; | ||
} | ||
|
||
|
||
export function createNode(nodeName) { | ||
let name = normalizeName(nodeName), | ||
list = nodes[name], | ||
node = list && list.pop() || document.createElement(nodeName); | ||
node[ATTR_KEY] = {}; | ||
return node; | ||
} | ||
|
||
|
||
function cleanNode(node) { | ||
if (node.parentNode) node.parentNode.removeChild(node); | ||
|
||
if (node.nodeType===3) return; | ||
|
||
delete node._component; | ||
delete node._componentConstructor; | ||
|
||
let attrs = node[ATTR_KEY]; | ||
for (let i in attrs) { | ||
if (hasOwnProperty.call(attrs, i)) { | ||
setAccessor(node, i, null, attrs[i]); | ||
} | ||
} | ||
node[ATTR_KEY] = null; | ||
|
||
// if (node.childNodes.length>0) { | ||
// console.warn(`Warning: Recycler collecting <${node.nodeName}> with ${node.childNodes.length} children.`); | ||
// toArray(node.childNodes).forEach(recycler.collect); | ||
// } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import options from './options'; | ||
import VNode from './vnode'; | ||
import { hook } from './hooks'; | ||
import { empty } from './util'; | ||
|
||
|
||
/** JSX/hyperscript reviver | ||
* @see http://jasonformat.com/wtf-is-jsx | ||
* @public | ||
* @example | ||
* /** @jsx h *\/ | ||
* import { render, h } from 'preact'; | ||
* render(<span>foo</span>, document.body); | ||
*/ | ||
export default function h(nodeName, attributes, ...args) { | ||
let children, | ||
sharedArr = [], | ||
len = args.length, | ||
arr, lastSimple; | ||
if (len) { | ||
children = []; | ||
for (let i=0; i<len; i++) { | ||
let p = args[i]; | ||
if (empty(p)) continue; | ||
if (p.join) { | ||
arr = p; | ||
} | ||
else { | ||
arr = sharedArr; | ||
arr[0] = p; | ||
} | ||
for (let j=0; j<arr.length; j++) { | ||
let child = arr[j], | ||
simple = !empty(child) && !(child instanceof VNode); | ||
if (simple) child = String(child); | ||
if (simple && lastSimple) { | ||
children[children.length-1] += child; | ||
} | ||
else if (!empty(child)) { | ||
children.push(child); | ||
} | ||
lastSimple = simple; | ||
} | ||
} | ||
} | ||
|
||
if (attributes && attributes.children) { | ||
delete attributes.children; | ||
} | ||
|
||
let p = new VNode(nodeName, attributes || undefined, children || undefined); | ||
hook(options, 'vnode', p); | ||
return p; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/** Invoke a "hook" method with arguments if it exists. | ||
* @private | ||
*/ | ||
export function hook(obj, name, ...args) { | ||
let fn = obj[name]; | ||
if (fn && typeof fn==='function') return fn.apply(obj, args); | ||
} | ||
|
||
|
||
|
||
/** Invoke hook() on a component and child components (recursively) | ||
* @private | ||
*/ | ||
export function deepHook(obj, ...args) { | ||
do { | ||
hook(obj, ...args); | ||
} while ((obj=obj._component)); | ||
} |
Oops, something went wrong.