Skip to content

Commit

Permalink
Factor codebase into modules (addresses #3)
Browse files Browse the repository at this point in the history
  • Loading branch information
developit committed Feb 3, 2016
1 parent 95f18e0 commit 4f59bdb
Show file tree
Hide file tree
Showing 18 changed files with 1,148 additions and 1,108 deletions.
104 changes: 104 additions & 0 deletions src/component.js
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;
}

});
20 changes: 20 additions & 0 deletions src/constants.js
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
};
142 changes: 142 additions & 0 deletions src/dom/index.js
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;
}
50 changes: 50 additions & 0 deletions src/dom/recycler.js
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);
// }
}
54 changes: 54 additions & 0 deletions src/h.js
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;
}
18 changes: 18 additions & 0 deletions src/hooks.js
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));
}

0 comments on commit 4f59bdb

Please sign in to comment.