UNPKG

preact

Version:

Fast 3kb React-compatible Virtual DOM library.

481 lines (434 loc) 14.3 kB
import { hydrate, render as preactRender, cloneElement as preactCloneElement, createRef, h, Component, options, toChildArray, createContext, Fragment, _unmount } from 'preact'; import * as hooks from 'preact/hooks'; import { Suspense, lazy } from './suspense'; import { assign, removeNode } from '../../src/util'; const version = '16.8.0'; // trick libraries to think we are react /* istanbul ignore next */ const REACT_ELEMENT_TYPE = (typeof Symbol!=='undefined' && Symbol.for && Symbol.for('react.element')) || 0xeac7; const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip|color|fill|flood|font|glyph|horiz|marker|overline|paint|stop|strikethrough|stroke|text|underline|unicode|units|v|vector|vert|word|writing|x)[A-Z]/; let oldEventHook = options.event; options.event = e => { /* istanbul ignore next */ if (oldEventHook) e = oldEventHook(e); e.persist = () => {}; return e.nativeEvent = e; }; /** * Legacy version of createElement. * @param {import('./internal').VNode["type"]} type The node name or Component constructor */ function createFactory(type) { return createElement.bind(null, type); } /** * Normalize DOM vnode properties. * @param {import('./internal').VNode} vnode The vnode to normalize props of * @param {object | null | undefined} props props to normalize */ function handleElementVNode(vnode, props) { let shouldSanitize, attrs, i; for (i in props) if ((shouldSanitize = CAMEL_PROPS.test(i))) break; if (shouldSanitize) { attrs = vnode.props = {}; for (i in props) { attrs[CAMEL_PROPS.test(i) ? i.replace(/([A-Z0-9])/, '-$1').toLowerCase() : i] = props[i]; } } } /** * Proxy render() since React returns a Component reference. * @param {import('./internal').VNode} vnode VNode tree to render * @param {import('./internal').PreactElement} parent DOM node to render vnode tree into * @param {() => void} [callback] Optional callback that will be called after rendering * @returns {import('./internal').Component | null} The root component reference or null */ function render(vnode, parent, callback) { // React destroys any existing DOM nodes, see #1727 // ...but only on the first render, see #1828 if (parent._children==null) { while (parent.firstChild) { removeNode(parent.firstChild); } } preactRender(vnode, parent); if (typeof callback==='function') callback(); return vnode ? vnode._component : null; } class ContextProvider { getChildContext() { return this.props.context; } render(props) { return props.children; } } /** * Portal component * @param {object | null | undefined} props */ function Portal(props) { let _this = this; let container = props.container; let wrap = h(ContextProvider, { context: _this.context }, props.vnode); // When we change container we should clear our old container and // indicate a new mount. if (_this._container && _this._container !== container) { if (_this._temp.parentNode) _this._container.removeChild(_this._temp); _unmount(_this._wrap); _this._hasMounted = false; } // When props.vnode is undefined/false/null we are dealing with some kind of // conditional vnode. This should not trigger a render. if (props.vnode) { if (!_this._hasMounted) { // Create a placeholder that we can use to insert into. _this._temp = document.createTextNode(''); // Hydrate existing nodes to keep the dom intact, when rendering // wrap into the container. hydrate('', container); // Insert before first child (will just append if firstChild is null). container.insertBefore(_this._temp, container.firstChild); // At this point we have mounted and should set our container. _this._hasMounted = true; _this._container = container; // Render our wrapping element into temp. preactRender(wrap, container, _this._temp); _this._children = this._temp._children; } else { // When we have mounted and the vnode is present it means the // props have changed or a parent is triggering a rerender. // This implies we only need to call render. But we need to keep // the old tree around, otherwise will treat the vnodes as new and // will wrongly call `componentDidMount` on them container._children = _this._children; preactRender(wrap, container); _this._children = container._children; } } // When we come from a conditional render, on a mounted // portal we should clear the DOM. else if (_this._hasMounted) { if (_this._temp.parentNode) _this._container.removeChild(_this._temp); _unmount(_this._wrap); } // Set the wrapping element for future unmounting. _this._wrap = wrap; _this.componentWillUnmount = () => { if (_this._temp.parentNode) _this._container.removeChild(_this._temp); _unmount(_this._wrap); }; return null; } /** * Create a `Portal` to continue rendering the vnode tree at a different DOM node * @param {import('./internal').VNode} vnode The vnode to render * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to. */ function createPortal(vnode, container) { return h(Portal, { vnode, container }); } const mapFn = (children, fn) => { if (!children) return null; return toChildArray(children).map(fn); }; // This API is completely unnecessary for Preact, so it's basically passthrough. let Children = { map: mapFn, forEach: mapFn, count(children) { return children ? toChildArray(children).length : 0; }, only(children) { children = toChildArray(children); if (children.length!==1) throw new Error('Children.only() expects only one child.'); return children[0]; }, toArray: toChildArray }; /** * Wrap `createElement` to apply various vnode normalizations. * @param {import('./internal').VNode["type"]} type The node name or Component constructor * @param {object | null | undefined} [props] The vnode's properties * @param {Array<import('./internal').ComponentChildren>} [children] The vnode's children * @returns {import('./internal').VNode} */ function createElement(...args) { let vnode = h(...args); let type = vnode.type, props = vnode.props; if (typeof type!='function') { if (props.defaultValue) { if (!props.value && props.value!==0) { props.value = props.defaultValue; } delete props.defaultValue; } if (Array.isArray(props.value) && props.multiple && type==='select') { toChildArray(props.children).forEach((child) => { if (props.value.indexOf(child.props.value)!=-1) { child.props.selected = true; } }); delete props.value; } handleElementVNode(vnode, props); } vnode.preactCompatNormalized = false; return normalizeVNode(vnode); } /** * Normalize a vnode * @param {import('./internal').VNode} vnode */ function normalizeVNode(vnode) { vnode.preactCompatNormalized = true; applyClassName(vnode); return vnode; } /** * Wrap `cloneElement` to abort if the passed element is not a valid element and apply * all vnode normalizations. * @param {import('./internal').VNode} element The vnode to clone * @param {object} props Props to add when cloning * @param {Array<import('./internal').ComponentChildren>} rest Optional component children */ function cloneElement(element) { if (!isValidElement(element)) return element; let vnode = normalizeVNode(preactCloneElement.apply(null, arguments)); return vnode; } /** * Check if the passed element is a valid (p)react node. * @param {*} element The element to check * @returns {boolean} */ function isValidElement(element) { return !!element && element.$$typeof===REACT_ELEMENT_TYPE; } /** * Normalize event handlers like react does. Most famously it uses `onChange` for any input element. * @param {import('./internal').VNode} vnode The vnode to normalize events on */ function applyEventNormalization({ type, props }) { if (!props || typeof type!='string') return; let newProps = {}; for (let i in props) { if (/^on(Ani|Tra)/.test(i)) { props[i.toLowerCase()] = props[i]; delete props[i]; } newProps[i.toLowerCase()] = i; } if (newProps.ondoubleclick) { props.ondblclick = props[newProps.ondoubleclick]; delete props[newProps.ondoubleclick]; } if (newProps.onbeforeinput) { props.onbeforeinput = props[newProps.onbeforeinput]; delete props[newProps.onbeforeinput]; } // for *textual inputs* (incl textarea), normalize `onChange` -> `onInput`: if (newProps.onchange && (type==='textarea' || (type.toLowerCase()==='input' && !/^fil|che|ra/i.test(props.type)))) { let normalized = newProps.oninput || 'oninput'; if (!props[normalized]) { props[normalized] = props[newProps.onchange]; delete props[newProps.onchange]; } } } /** * Remove a component tree from the DOM, including state and event handlers. * @param {import('./internal').PreactElement} container * @returns {boolean} */ function unmountComponentAtNode(container) { if (container._children) { preactRender(null, container); return true; } return false; } /** * Alias `class` prop to `className` if available * @param {import('./internal').VNode} vnode */ function applyClassName(vnode) { let a = vnode.props; if (a.class || a.className) { classNameDescriptor.enumerable = 'className' in a; if (a.className) a.class = a.className; Object.defineProperty(a, 'className', classNameDescriptor); } } let classNameDescriptor = { configurable: true, get() { return this.class; } }; /** * Check if two objects have a different shape * @param {object} a * @param {object} b * @returns {boolean} */ function shallowDiffers(a, b) { for (let i in a) if (i !== '__source' && !(i in b)) return true; for (let i in b) if (i !== '__source' && a[i]!==b[i]) return true; return false; } /** * Get the matching DOM node for a component * @param {import('./internal').Component} component * @returns {import('./internal').PreactElement | null} */ function findDOMNode(component) { return component && (component.base || component.nodeType === 1 && component) || null; } /** * Component class with a predefined `shouldComponentUpdate` implementation */ class PureComponent extends Component { constructor(props) { super(props); // Some third-party libraries check if this property is present this.isPureReactComponent = true; } shouldComponentUpdate(props, state) { return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); } } // Some libraries like `react-virtualized` explicitly check for this. Component.prototype.isReactComponent = {}; /** * Memoize a component, so that it only updates when the props actually have * changed. This was previously known as `React.pure`. * @param {import('./internal').FunctionalComponent} c functional component * @param {(prev: object, next: object) => boolean} [comparer] Custom equality function * @returns {import('./internal').FunctionalComponent} */ function memo(c, comparer) { function shouldUpdate(nextProps) { let ref = this.props.ref; let updateRef = ref==nextProps.ref; if (!updateRef && ref) { ref.call ? ref(null) : (ref.current = null); } return (!comparer ? shallowDiffers(this.props, nextProps) : !comparer(this.props, nextProps)) || !updateRef; } function Memoed(props) { this.shouldComponentUpdate = shouldUpdate; return h(c, assign({}, props)); } Memoed.prototype.isReactComponent = true; Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; Memoed._forwarded = true; return Memoed; } /** * Pass ref down to a child. This is mainly used in libraries with HOCs that * wrap components. Using `forwardRef` there is an easy way to get a reference * of the wrapped component instead of one of the wrapper itself. * @param {import('./internal').ForwardFn} fn * @returns {import('./internal').FunctionalComponent} */ function forwardRef(fn) { function Forwarded(props) { let ref = props.ref; delete props.ref; return fn(props, ref); } Forwarded.prototype.isReactComponent = true; Forwarded._forwarded = true; Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; return Forwarded; } // Patch in `UNSAFE_*` lifecycle hooks function setSafeDescriptor(proto, key) { if (proto['UNSAFE_'+key] && !proto[key]) { Object.defineProperty(proto, key, { configurable: false, get() { return this['UNSAFE_' + key]; }, set(v) { this['UNSAFE_' + key] = v; } }); } } let oldVNodeHook = options.vnode; options.vnode = vnode => { vnode.$$typeof = REACT_ELEMENT_TYPE; applyEventNormalization(vnode); let type = vnode.type; if (type && type._forwarded && vnode.ref) { vnode.props.ref = vnode.ref; vnode.ref = null; } // We can't just patch the base component class, because components that use // inheritance and are transpiled down to ES5 will overwrite our patched // getters and setters. See #1941 if (typeof type === 'function' && !type._patchedLifecycles && type.prototype) { setSafeDescriptor(type.prototype, 'componentWillMount'); setSafeDescriptor(type.prototype, 'componentWillReceiveProps'); setSafeDescriptor(type.prototype, 'componentWillUpdate'); type._patchedLifecycles = true; } /* istanbul ignore next */ if (oldVNodeHook) oldVNodeHook(vnode); }; /** * Deprecated way to control batched rendering inside the reconciler, but we * already schedule in batches inside our rendering code * @template Arg * @param {(arg: Arg) => void} callback function that triggers the updated * @param {Arg} [arg] Optional argument that can be passed to the callback */ // eslint-disable-next-line camelcase const unstable_batchedUpdates = (callback, arg) => callback(arg); export * from 'preact/hooks'; export { version, Children, render, render as hydrate, unmountComponentAtNode, createPortal, createElement, createContext, createFactory, cloneElement, createRef, Fragment, isValidElement, findDOMNode, Component, PureComponent, memo, forwardRef, // eslint-disable-next-line camelcase unstable_batchedUpdates, Suspense, lazy }; // React copies the named exports to the default one. export default assign({ version, Children, render, hydrate: render, unmountComponentAtNode, createPortal, createElement, createContext, createFactory, cloneElement, createRef, Fragment, isValidElement, findDOMNode, Component, PureComponent, memo, forwardRef, unstable_batchedUpdates, Suspense, lazy }, hooks);