mach-react
Version:
Fast, and tiny react implementation.
482 lines (443 loc) • 15.9 kB
JavaScript
'use strict';
import assignObject from './assignObject';
import setZeroTimeout from './setZeroTimeout';
import { EventEmitter } from 'events';
import { h, diff, patch, create as createVirtualElement } from 'virtual-dom';
import svg from 'virtual-dom/virtual-hyperscript/svg';
export setZeroTimeout from './setZeroTimeout';
export { EventEmitter } from 'events';
export function createElement(type, props, ...children) {
return create(type, props, children);
}
export class BaseComponent extends EventEmitter {
static appendDOM = attach;
static assignObject = assignObject;
static createElement = createElement;
static mixin(constructor) {
let prototype = constructor.prototype;
if (prototype && constructor.mixins && !constructor.mixins.done) {
constructor.assignObject(prototype, ...constructor.mixins);
constructor.mixins.done = true;
}
}
static removeDOM = detach;
static resolveDOM = resolve;
assignObject = this.constructor.assignObject;
resolveDOM = this.constructor.resolveDOM;
constructor() {
super();
this.props = this.props || {};
this.state = this.state || {};
this.context = this.context || {};
this.next = {};
this.assignObject(this.props, this.constructor.defaultProps, this.props);
this.constructor.mixin && this.constructor.mixin(this.constructor);
}
cancelUpdate() {
if (this.boundUpdate) {
unsetZeroTimeout(this.boundUpdate);
this.boundUpdate = null;
}
return this;
}
isUpdating = false;
mergeObjectProperty(property, value) {
let changes = this.next[property] = this.next[property] || [];
changes.push(value);
}
mount(parent) {
this.refs = {};
this.componentWillMount && this.componentWillMount();
this.update(true);
let finishMount = () => {
// TODO: might need a timeout here
this.componentDidMount && this.componentDidMount();
this.emit('mount');
}
if (parent) {
this.constructor.appendDOM(this.domNode, parent);
return finishMount();
}
return finishMount;
}
replaceObjectProperty(property, value) {
let changes = this.next[property] = this.next[property] || [];
changes.push(null);
changes.push(value);
}
safeRender() { return this.render(this.constructor); }
safeUpdate(force) { (force || this.boundUpdate) && this.cancelUpdate().update(force); }
setupContext(rootComponent, parentComponent) {
this.rootComponent = rootComponent;
this.parentComponent = parentComponent;
this.context = this.assignObject({},
rootComponent.context,
rootComponent && rootComponent.getChildContext());
if (parentComponent && parentComponent !== rootComponent) {
this.assignObject(this.context, parentComponent.getChildContext());
}
}
unmount() {
this.componentWillUnmount && this.componentWillUnmount();
this.lastVirtualElement = this.virtualElement;
this.virtualElement = null;
this.domNode = this.resolveDOM(this);
this.lastVirtualElement = null;
let parent = this.domNode && this.domNode.parentNode;
if (parent) this.constructor.removeDOM(this.domNode);
this.componentDidUnmount && this.componentDidUnmount();
this.emit('unmount');
if (this.domNode) {
this.domNode.component = null;
this.domNode = null;
}
}
queueUpdate(callback) {
if (callback) this.once('update', callback);
if (this.boundUpdate) return;
this.boundUpdate = this.update.bind(this);
setZeroTimeout(this.boundUpdate);
}
update(force) {
this.boundUpdate = null;
this.isUpdating = true;
let next = this.next,
temp = {props: this.props, state: this.state};
this.next = {};
['props', 'state'].forEach(property => {
let changes = next[property] || [],
lastNull = changes.lastIndexOf(null);
if (lastNull !== -1) {
changes = changes.slice(lastNull + 1);
temp[property] = {};
}
if (temp[property] === this[property] && changes.length) {
temp[property] = this.assignObject({}, temp[property]);
}
changes.forEach(change => {
if (property === 'state' && typeof change === 'function') {
change = change.call(this, temp.state, temp.props);
}
this.assignObject(temp[property], change);
});
});
if (!force && this.shouldComponentUpdate &&
!this.shouldComponentUpdate(temp.props, temp.state)) return;
this.assignObject(this, temp);
!force && this.componentWillUpdate && this.componentWillUpdate(this.props, this.state);
this.refs = {};
this.lastVirtualElement = this.virtualElement;
let afterRender = () => {
// this.virtualElement.properties.key = this.virtualElement.properties.key || this.props.key;
// this.virtualElement.key = this.virtualElement.key || this.props.key;
if (this.domNode) delete this.domNode.component;
this.domNode = this.resolveDOM(this);
let finishUpdate = () => {
!force && this.componentDidUpdate && this.componentDidUpdate();
this.emit('update');
this.isUpdating = false;
this.lastComponent = null;
};
setZeroTimeout(finishUpdate);
};
if (this.renderAsync) {
this.renderAsync(this.constructor, (newVirtualElement) => {
this.virtualElement = newVirtualElement;
afterRender();
});
}
else {
this.virtualElement = this.safeRender();
afterRender();
}
}
}
export default class ReactComponent extends BaseComponent {
static Component = ReactComponent;
// componentDidMount() {}
// componentDidUnmount() {}
// componentDidUpdate(prevProps, prevState) {}
// componentWillMount() {}
// componentWillReceiveProps(nextProps) {}
// componentWillReceiveState(nextState) {}
// componentWillUnmount() {}
// componentWillUpdate(nextProps, nextState) {}
// render(React) { return null; }
// shouldComponentUpdate(nextProps, nextState) { return true; }
autoUpdateWhenPropsChange = true;
constructor(props, context) {
super();
props && this.assignObject(this.props, props);
context && this.assignObject(this.context, context);
}
forceUpdate() { this.update(true); }
get displayName() { return this.constructor.name; }
getChildContext() { return this.childContext || {}; }
getDOMNode() { return this.domNode; }
isMounted() { return this.domNode && this.domNode.parentNode; }
replaceProps(newProps, callback) {
this.replaceObjectProperty('props', newProps);
this.autoUpdateWhenPropsChange && this.queueUpdate(callback);
}
replaceState(newState, callback) {
this.replaceObjectProperty('state', newState);
this.queueUpdate(callback);
}
setProps(nextProps, callback) {
this.componentWillReceiveProps && this.componentWillReceiveProps(nextProps);
this.mergeObjectProperty('props', nextProps);
this.autoUpdateWhenPropsChange && this.queueUpdate(callback);
}
setState(nextState, callback) {
this.componentWillReceiveState && this.componentWillReceiveState(nextState);
this.mergeObjectProperty('state', nextState);
this.queueUpdate(callback);
}
}
export let Component = ReactComponent;
export class ComponentWidget {
type = 'Widget';
constructor(Component, props, children, context) {
props = props || {};
props.children = props.children ? props.children.concat(children) : children;
this.component = new Component(props, context);
this.name = this.component.displayName;
this.id = this.name;
}
init() {
let componentDidMount = this.component.mount();
if (!this.component.domNode) return;
setZeroTimeout(componentDidMount);
this.component.domNode.component = this.component;
this.refHook(this.component.domNode);
return this.component.domNode;
}
update(previous, domNode) {
this.component.lastComponent = domNode.component || previous.component;
this.component.virtualElement = this.component.lastComponent.virtualElement;
this.component.assignObject(this.component.state, previous.component.state);
this.component.update();
if (this.component.domNode) {
this.component.domNode.component = this.component;
}
this.refHook(domNode);
return this.component.domNode;
}
destroy(domNode) {
this.component.unmount();
delete domNode.component;
}
refHook(domNode) {
if (this.component.props.refHook) {
this.component.props.refHook.hook(this.component.domNode || domNode);
}
}
}
export class RefHook {
constructor(name, rootComponent) {
this.name = name;
this.rootComponent = rootComponent;
}
hook(domNode) {
let refs = this.rootComponent.refs;
if (this.name.charAt(0) === '$') {
refs[this.name] = refs[this.name] || [];
if (domNode) refs[this.name].push(domNode.component || domNode);
}
else refs[this.name] = domNode.component || domNode || refs[this.name];
}
}
export class HtmlHook {
constructor(value) { this.value = value; }
hook(domNode) {
let html = this.value && this.value.__html || this.value;
if (typeof html === 'string') domNode.innerHTML = html;
}
}
export class OnChangeHook {
constructor(handler) { this.handler = handler; }
onFocus(event) {
this.changeInterval = setInterval(this.detectChange.bind(this, event), 100);
}
onBlur(event) {
this.detectChange(event);
this.cancelInterval();
}
detectChange(event) {
if (event.target.value !== this.lastValue) {
this.lastValue = event.target.value;
this.handler(event);
}
}
cancelInterval() { clearInterval(this.changeInterval); }
hook(domNode, previous) {
this.lastValue = domNode.value;
// TODO: this is causing a memory leak
let onFocusHandler = event => { this.onFocus(event); },
onBlurHandler = event => { this.onBlur(event); };
addEvent(domNode, 'focus', onFocusHandler);
addEvent(domNode, 'blur', onBlurHandler)
if (this.remove) this.remove();
this.remove = () => {
this.cancelInterval();
removeEvent(domNode, 'focus', onFocusHandler);
removeEvent(domNode, 'blur', onBlurHandler);
};
}
unhook(domNode) {
this.remove();
}
}
export function addEvent(elem, event, fn) {
if (elem.addEventListener) elem.addEventListener(event, fn, false);
else elem.attachEvent('on' + event, () => fn.call(elem, window.event));
}
export function removeEvent(elem, event, fn) {
if (elem.addEventListener) elem.removeEventListener(event, fn, false);
else elem.detachEvent('on' + event, () => fn.call(elem, window.event));
}
export function attach(element, parent) {
if (element && parent && parent.appendChild) return parent.appendChild(element);
if (element) throw new Error('Failed to attach element.');
}
export function detach(element) {
if (element && element.parentNode) return element.parentNode.removeChild(element);
if (element) throw new Error('Failed to detach element.');
}
export function create(element, props, children, context) {
let definition = element,
type = typeof element;
props = fixProps(props || {});
if (type === 'string') {
if (props.cssSelector) element += cssSelector;
// TODO: you have to make sure to add svg={true} to every svg element or else it wont work
definition = (props.svg ? svg : h)(element, props, children);
}
else if (type === 'function') {
definition = (element.prototype && element.prototype.displayName) ?
new ComponentWidget(element, props, children, context) : element(props);
}
return definition;
}
export function fixProps(props) {
let newProps = {};
Object.keys(props).forEach(prop => {
if (prop === 'dangerouslySetInnerHTML') {
newProps.htmlHook = new HtmlHook(props[prop]);
}
if (prop === 'defaultValue') {
newProps.value = props.value || props.defaultValue;
}
if (prop === 'defaultChecked') {
newProps.checked = typeof props.checked === 'boolean' ?
props.checked : props.defaultChecked;
}
if (prop === 'style') {
let styles = props[prop];
if (Array.isArray(styles)) {
if (styles.__cssMergeCache__) styles = styles.__cssMergeCache__;
else styles = styles.__cssMergeCache__ = Object.assign({}, ...styles);
}
newProps[prop] = typeof styles === 'string' ? styles : fixProps.fixStyles(styles);
return;
}
if (prop === 'onChange') {
newProps.onChangeHook = new OnChangeHook(props.onChange);
return;
}
if (prop.indexOf('on') === 0) {
let handler = props[prop];
prop = prop.toLowerCase();
newProps[prop] = handler;
return;
}
newProps[prop] = props[prop];
});
return newProps;
}
// TODO: guard against css properties that do not use a unit.
const autoMap = {_off:0, _on:1, width:0, height:0}; // on by default
fixProps.fixStyles = function fixStyles(styles) {
if (styles) Object.keys(styles).forEach(key => {
if (autoMap[key]) return;
if (typeof styles[key] === 'number') styles[key] += 'px';
});
return styles;
};
export function render(virtualElement, parentDomNode, callback, delay) {
let detacher;
if (virtualElement.component && virtualElement.component.mount) {
virtualElement.component.mount(parentDomNode);
detacher = virtualElement.component.unmount.bind(virtualElement.component);
}
else {
let domNode = createVirtualElement(virtualElement);
attach(domNode, parentDomNode);
detacher = function () {
let changes = diff(virtualElement, null);
domNode = patch(domNode, changes);
detach(domNode, parentDomNode);
}
}
if (callback) setTimeout(callback, delay || 0);
return detacher;
}
export function resolve(component, rootComponent) {
walkVirtual(component.virtualElement, (def, rootComponent, parentComponent) => {
if (def) {
if (def.component) {
if (def.component.setupContext) {
def.component.setupContext(rootComponent, parentComponent);
}
if (def.component.props && def.component.props.ref) {
def.component.props.refHook =
def.component.props.refHook || new RefHook(def.component.props.ref, rootComponent);
}
}
else if (def.properties && def.properties.ref) {
def.properties.refHook =
def.properties.refHook || new RefHook(def.properties.ref, rootComponent);
}
}
}, component);
let domNode = component.domNode ||
component.lastComponent && component.lastComponent.domNode;
let lastDomNode = domNode;
if (!domNode) {
domNode = createVirtualElement(component.virtualElement);
if (domNode) domNode.component = component;
}
else {
let changes = diff(component.lastVirtualElement, component.virtualElement);
domNode.component = component;
domNode = patch(domNode, changes);
if (domNode) domNode.component = component;
}
if (lastDomNode && lastDomNode !== domNode) {
if (lastDomNode.component && lastDomNode.component.domNode === lastDomNode) {
lastDomNode.component.domNode = null;
}
lastDomNode.component = null;
}
return domNode;
}
export function walkVirtual(definition, iterator, rootComponent, parentComponent) {
let children = [];
if (!definition || typeof definition !== 'object') return;
if (definition.constructor.name === 'VirtualText') return;
iterator(definition, rootComponent, parentComponent);
if (Array.isArray(definition)) {
children = definition.slice(0);
}
if (definition.component) {
if (definition.component.props) {
children = children.concat(definition.component.props.children || []);
}
}
if (Array.isArray(definition.children)) {
children = children.concat(definition.children);
}
if (Array.isArray(children)) {
children.forEach(child => walkVirtual(child, iterator, rootComponent, definition.component || parentComponent));
}
}