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
text/typescript
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);
}
};