UNPKG

lupine.web

Version:

lupine.web is a extremely fast, small size and lightweight frontend framework, using React TSX syntax.

234 lines (223 loc) 8.99 kB
import { Logger } from '../lib/logger'; import { uniqueIdGenerator } from '../lib/unique-id'; import { processStyle } from './bind-styles'; // import { bindPageResetEvent } from './page-reset-events'; import { camelToHyphens } from './camel-to-hyphens'; const logger = new Logger('render-components'); export const domUniqueId = uniqueIdGenerator('l'); // l means label // bindPageResetEvent(() => { // // reset unique id // domUniqueId(true); // }); function renderChildren(html: string[], children: any, uniqueClassName?: string, globalCssId?: string) { if (typeof children === 'string') { html.push(children); } else if (children === false || children === null || typeof children === 'undefined') { // add nothing } else if (typeof children === 'number' || typeof children === 'boolean') { // true will be added html.push(children.toString()); } else if (Array.isArray(children)) { for (let i = 0; i < children.length; i++) { const item = children[i]; renderChildren(html, item, uniqueClassName, globalCssId); } } else if (children.type && children.props) { renderComponent(children.type, children.props, uniqueClassName, globalCssId); html.push(...children.props._html); children.props._html.length = 0; } else { logger.warn('Unexpected', children); } } const selfClosingTags = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', ]; const genUniqueId = (props: any) => { if (!props._id) { props._id = domUniqueId(); } return props._id; }; // data-refid will be assigned with a ref.id function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?: string, globalCssId?: string) { const html = []; // data-refid is used for nested components like this: // <div class='class-name' ref={ref} ...>... // <div data-refid={ref}> // then data-refid can be located: // ref.$(`.class-name[data-refid=${ref.id}]`) // if (props['data-refid'] && props['data-refid'].id) { // props['data-refid'] = props['data-refid'].id; // } for (let i in props) { if (i === 'ref') { if (props[i]) { props[i].id = genUniqueId(props); html.push('data-ref'); } } else if (!['children', 'key', '_result', '_html', '_id'].includes(i)) { //, "_lb" // , "value", "checked" // style is a string, in-line style if (i === 'style') { if (typeof props[i] === 'object') { let attrs = `${i}="`; for (let j in props[i]) { attrs += `${camelToHyphens(j)}:${props[i][j]};`; } attrs += `"`; html.push(attrs); } else { html.push(`${i}="${props[i]}"`); } } else if (i === 'css') { // css is a <style> tag, and is the first element in html genUniqueId(props); // props._lb = props._id; } else if (i[0] === 'o' && i[1] === 'n') { genUniqueId(props); } else if (i === 'defaultChecked') { if (props[i] === true || props[i] === 'checked') { html.push(`checked="true"`); } } else if (i === 'readonly' || i === 'disabled' || i === 'selected' || i === 'checked') { if (props[i] !== undefined && props[i] !== false && props[i] !== 'false') { html.push(`${i}="${props[i]}"`); } } else if (i === 'class' || i === 'className') { let classNameList = props[i].split(' ').filter((item: string) => item && item !== ''); if ((props['css'] || props['ref']) && !classNameList.includes(props._id)) { // add as the first classNameList.unshift(props._id); } if (props['ref'] && props['ref'].globalCssId && !classNameList.includes(props['ref'].globalCssId)) { // add as the first classNameList.unshift(props['ref'].globalCssId); } if (globalCssId && uniqueClassName) { // &xx -> globalCssId + xx and uniqueClassName+xx classNameList = classNameList.flatMap((item: string) => { if (item.includes('&')) { return [item.replace(/&/g, globalCssId), item.replace(/&/g, uniqueClassName)]; } return [item]; }); } else if (globalCssId) { classNameList = classNameList.map((item: string) => item.replace(/&/g, globalCssId)); } else if (uniqueClassName) { classNameList = classNameList.map((item: string) => item.replace(/&/g, uniqueClassName)); } html.push(`class="${classNameList.join(' ')}"`); } else if (i !== 'dangerouslySetInnerHTML') { html.push(`${i}="${props[i]}"`); } } } if (props._id) { // tag id will be after all attributes html.push(props._id); } return html.join(' '); } // The result has only one element export const renderComponent = (type: any, props: any, uniqueClassName?: string, globalCssId?: string) => { // logger.log("==================renderComponent", type); if (Array.isArray(props)) { const jsxNodes = { type: 'Fragment', props: { children: props } } as any; renderComponent(jsxNodes.type, jsxNodes.props, uniqueClassName, globalCssId); return; } props._html = []; if (typeof type === 'function') { props._result = type.call(null, props); if (props._result === null || props._result === undefined || props._result === false) { // placeholder for sub components props._result = { type: 'Fragment', props }; } // if (props._fragment_ref && props._result && props._result.props) { // // pass the ref to the sub Fragment tag // props._result.props.ref = props._fragment_ref; // props._result.props._id = genUniqueId(props._result.props); // } // logger.log('==========props._result', props._result); if (typeof props._result.type === 'function') { renderComponent(props._result.type, props._result.props, uniqueClassName, globalCssId); if (props._result.props._html) { props._html.push(...props._result.props._html); props._result.props._html.length = 0; } // function component doesn't have any attributes return; } } const newType = (props._result && props._result.type) || type; const newProps = (props._result && props._result.props) || props; if (newType === 'div' && newProps.class === 'answer-box') { console.log('renderComponent', newType, newProps); } if (typeof newType === 'string') { if (newProps._id) { console.warn('This component reference is used more than once and will have binding issues: ', newProps); } let newUniqueClassName = uniqueClassName; if (newProps['css'] || newProps['ref']) { newUniqueClassName = genUniqueId(newProps); if (!newProps['class'] && !newProps['className']) { newProps['class'] = newUniqueClassName; } } let newGlobalCssId = globalCssId; if (newProps['ref'] && newProps['ref'].globalCssId) { newGlobalCssId = newProps['ref'].globalCssId; } const attrs = renderAttribute(newType, newProps, { type, props }, newUniqueClassName, newGlobalCssId); if (selfClosingTags.includes(newType.toLowerCase())) { // for Fragment, only needs this tag when Ref is assigned if (newType !== 'Fragment' || newProps.ref) { props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs} />`); } if (newProps['css']) { console.warn(`ClosingTag [${newType}] doesn't support 'css', please use 'style' instead.`); } } else { if (newType !== 'Fragment' || newProps.ref) { props._html.push(`<${newType}${attrs ? ' ' : ''}${attrs}>`); } if (newProps['css']) { const cssText = processStyle(newUniqueClassName!, newProps['css']).join(''); props._html.push(`<style id="sty-${newUniqueClassName}">${cssText}</style>`); // sty means style, and updateStyles has the same name } if (newProps.children || newProps.children === 0) { // if (newProps._lb) { // assignLabels(newProps._lb, newProps.children); // } renderChildren(props._html, newProps.children, newUniqueClassName, newGlobalCssId); } else if (newProps['dangerouslySetInnerHTML']) { props._html.push(newProps['dangerouslySetInnerHTML']); } else { // single element } if (newType !== 'Fragment' || newProps.ref) { props._html.push(`</${newType}>`); } } } else if (newType.name === 'Fragment') { renderChildren(props._html, newProps.children, uniqueClassName, globalCssId); } else { logger.warn('Unknown type: ', type, props, newType, newProps); } };