UNPKG

nano-jsx

Version:

SSR first, lightweight 1kB JSX library.

275 lines 10.2 kB
import './types.js'; export const isSSR = () => typeof _nano !== 'undefined' && _nano.isSSR === true; /** Creates a new Microtask using Promise() */ export const tick = Promise.prototype.then.bind(Promise.resolve()); export const removeAllChildNodes = (parent) => { while (parent.firstChild) { parent.removeChild(parent.firstChild); } }; // https://stackoverflow.com/a/7616484/12656855 export const strToHash = (s) => { let hash = 0; for (let i = 0; i < s.length; i++) { const chr = s.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return Math.abs(hash).toString(32); }; export const appendChildren = (element, children, escape = true) => { // if the child is an html element if (!Array.isArray(children)) { appendChildren(element, [children], escape); return; } // htmlCollection to array if (typeof children === 'object') children = Array.prototype.slice.call(children); children.forEach(child => { // if child is an array of children, append them instead if (Array.isArray(child)) appendChildren(element, child, escape); else { // render the component const c = _render(child); if (typeof c !== 'undefined') { // if c is an array of children, append them instead if (Array.isArray(c)) appendChildren(element, c, escape); // apply the component to parent element else { if (isSSR() && !escape) element.appendChild(c.nodeType == null ? c.toString() : c); else element.appendChild(c.nodeType == null ? document.createTextNode(c.toString()) : c); } } } }); }; /** * A simple component for rendering SVGs */ const SVG = (props) => { const child = props.children[0]; const attrs = child.attributes; if (isSSR()) return child; const svg = hNS('svg'); for (let i = attrs.length - 1; i >= 0; i--) { svg.setAttribute(attrs[i].name, attrs[i].value); } svg.innerHTML = child.innerHTML; return svg; }; /** Returns the populated parent if available else one child or an array of children */ export const render = (component, parent = null, removeChildNodes = true) => { let el = _render(component); if (Array.isArray(el)) { el = el.map(e => _render(e)); if (el.length === 1) el = el[0]; } if (parent) { if (removeChildNodes) removeAllChildNodes(parent); // if parent and child are the same, we replace the parent instead of appending to it if (el && parent.id && parent.id === el.id && parent.parentElement) { parent.parentElement.replaceChild(el, parent); } else { // append element(s) to the parent if (Array.isArray(el)) el.forEach((e) => { appendChildren(parent, _render(e)); //parent.appendChild(_render(e)) }); else appendChildren(parent, _render(el)); } return parent; } // returning one child or an array of children else { if (isSSR() && !Array.isArray(el)) return [el]; return el; } }; export const hydrate = render; export const _render = (comp) => { // null, false, undefined if (comp === null || comp === false || typeof comp === 'undefined') return []; // string, number if (typeof comp === 'string' || typeof comp === 'number') return comp.toString(); // SVGElement if (comp.tagName && comp.tagName.toLowerCase() === 'svg') return SVG({ children: [comp] }); // HTMLElement if (comp.tagName) return comp; // TEXTNode (Node.TEXT_NODE === 3) if (comp && comp.nodeType === 3) return comp; // Class Component if (comp && comp.component && comp.component.isClass) return renderClassComponent(comp); // Class Component (Uninitialized) if (comp.isClass) return renderClassComponent({ component: comp, props: {} }); // Functional Component if (comp.component && typeof comp.component === 'function') return renderFunctionalComponent(comp); // Array (render each child and return the array) (is probably a fragment) if (Array.isArray(comp)) return comp.map(c => _render(c)).flat(); // function if (typeof comp === 'function' && !comp.isClass) return _render(comp()); // if component is a HTMLElement (rare case) if (comp.component && comp.component.tagName && typeof comp.component.tagName === 'string') return _render(comp.component); // (rare case) if (Array.isArray(comp.component)) return _render(comp.component); // (rare case) if (comp.component) return _render(comp.component); // object if (typeof comp === 'object') return []; console.warn('Something unexpected happened with:', comp); }; const renderFunctionalComponent = (fncComp) => { const { component, props } = fncComp; return _render(component(props)); }; const renderClassComponent = (classComp) => { const { component, props } = classComp; // calc hash const hash = strToHash(component.toString()); // make hash accessible in constructor, without passing it to it component.prototype._getHash = () => hash; const Component = new component(props); if (!isSSR()) Component.willMount(); let el = Component.render(); el = _render(el); Component.elements = el; // pass the component instance as ref if (props && props.ref) props.ref(Component); if (!isSSR()) tick(() => { Component._didMount(); }); return el; }; const hNS = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag); // https://stackoverflow.com/a/42405694/12656855 export const h = (tagNameOrComponent, props = {}, ...children) => { // if children is passed as props, merge with ...children if (props && props.children) { if (Array.isArray(children)) { if (Array.isArray(props.children)) children = [...props.children, ...children]; else children.push(props.children); } else { if (Array.isArray(props.children)) children = props.children; else children = [props.children]; } } // render WebComponent in SSR if (isSSR() && _nano.ssrTricks.isWebComponent(tagNameOrComponent)) { const element = _nano.ssrTricks.renderWebComponent(tagNameOrComponent, props, children, _render); if (element === null) return `ERROR: "<${tagNameOrComponent} />"`; else return element; } // if tagNameOrComponent is a component if (typeof tagNameOrComponent !== 'string') return { component: tagNameOrComponent, props: { ...props, children: children } }; // custom message if document is not defined in SSR try { if (isSSR() && typeof tagNameOrComponent === 'string' && !document) throw new Error('document is not defined'); } catch (err) { console.log('ERROR:', err.message, '\n > Please read: https://github.com/nanojsx/nano/issues/106'); } let ref; const element = tagNameOrComponent === 'svg' ? hNS('svg') : document.createElement(tagNameOrComponent); // check if the element includes the event (for example 'oninput') const isEvent = (el, p) => { // check if the event begins with 'on' if (0 !== p.indexOf('on')) return false; // we return true if SSR, since otherwise it will get rendered if (el._ssr) return true; // check if the event is present in the element as object (null) or as function return typeof el[p] === 'object' || typeof el[p] === 'function'; }; for (const p in props) { // https://stackoverflow.com/a/45205645/12656855 // style object to style string if (p === 'style' && typeof props[p] === 'object') { const styles = Object.keys(props[p]) .map(k => `${k}:${props[p][k]}`) .join(';') .replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); props[p] = `${styles};`; } // handel ref if (p === 'ref') ref = props[p]; // handle events else if (isEvent(element, p.toLowerCase())) element.addEventListener(p.toLowerCase().substring(2), (e) => props[p](e)); // dangerouslySetInnerHTML else if (p === 'dangerouslySetInnerHTML' && props[p].__html) { if (!isSSR()) { const fragment = document.createElement('fragment'); fragment.innerHTML = props[p].__html; element.appendChild(fragment); } else { element.innerHTML = props[p].__html; } } // modern dangerouslySetInnerHTML else if (p === 'innerHTML' && props[p].__dangerousHtml) { if (!isSSR()) { const fragment = document.createElement('fragment'); fragment.innerHTML = props[p].__dangerousHtml; element.appendChild(fragment); } else { element.innerHTML = props[p].__dangerousHtml; } } // className else if (/^className$/i.test(p)) element.setAttribute('class', props[p]); // setAttribute else if (typeof props[p] !== 'undefined') element.setAttribute(p, props[p]); } // these tags should not be escaped by default (in ssr) const escape = !['noscript', 'script', 'style'].includes(tagNameOrComponent); appendChildren(element, children, escape); if (ref) ref(element); return element; }; //# sourceMappingURL=core.js.map