UNPKG

@builder.io/mitosis

Version:

Write components once, run everywhere. Compiles to Vue, React, Solid, and Liquid. Import code from Figma and Builder.io

410 lines (409 loc) 20 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.blockToAngular = void 0; const html_tags_1 = require("../../../constants/html_tags"); const helpers_1 = require("../../../generators/angular/helpers"); const hooks_1 = require("../../../generators/angular/helpers/hooks"); const parse_selector_1 = require("../../../generators/angular/helpers/parse-selector"); const bindings_1 = require("../../../helpers/bindings"); const event_handlers_1 = require("../../../helpers/event-handlers"); const get_tag_name_1 = require("../../../helpers/get-tag-name"); const is_children_1 = __importDefault(require("../../../helpers/is-children")); const is_mitosis_node_1 = require("../../../helpers/is-mitosis-node"); const remove_surrounding_block_1 = require("../../../helpers/remove-surrounding-block"); const slots_1 = require("../../../helpers/slots"); const symbol_processor_1 = require("../../../symbols/symbol-processor"); const mitosis_node_1 = require("../../../types/mitosis-node"); const function_1 = require("fp-ts/function"); const lodash_1 = require("lodash"); const mappers = { Fragment: (root, json, options, blockOptions) => { return `<ng-container>${json.children .map((item) => (0, exports.blockToAngular)({ root, json: item, options, blockOptions })) .join('\n')}</ng-container>`; }, Slot: (root, json, options, blockOptions) => { const renderChildren = () => { var _a; return (_a = json.children) === null || _a === void 0 ? void 0 : _a.map((item) => (0, exports.blockToAngular)({ root, json: item, options, blockOptions })).join('\n'); }; return `<ng-content ${Object.entries({ ...json.bindings, ...json.properties }) .map(([binding, value]) => { if (value && binding === 'name') { const selector = (0, function_1.pipe)((0, lodash_1.isString)(value) ? value : value.code, slots_1.stripSlotPrefix, lodash_1.kebabCase); return `select="[${selector}]"`; } }) .join('\n')}>${Object.entries(json.bindings) .map(([binding, value]) => { if (value && binding !== 'name') { return value.code; } }) .join('\n')}${renderChildren()}</ng-content>`; }, }; // TODO: Maybe in the future allow defining `string | function` as values const BINDINGS_MAPPER = { innerHTML: 'innerHTML', style: 'ngStyle', }; const handleNgOutletBindings = (node, options) => { var _a; let allProps = ''; for (const key in node.properties) { if (key.startsWith('$')) { continue; } if (key === 'key') { continue; } const value = node.properties[key]; allProps += `${key}: '${value}', `; } for (const key in node.bindings) { if (key.startsWith('"')) { continue; } if (key.startsWith('$')) { continue; } let { code, arguments: cusArgs = ['event'] } = node.bindings[key]; if (options.state === 'class-properties') { code = `this.${code}`; if (((_a = node.bindings[key]) === null || _a === void 0 ? void 0 : _a.type) === 'spread') { allProps += `...${code}, `; continue; } } let keyToUse = key.includes('-') ? `'${key}'` : key; keyToUse = keyToUse.replace('state.', '').replace('props.', ''); if ((0, event_handlers_1.checkIsEvent)(key)) { const { event, value } = processEventBinding(key, code, node.name, cusArgs[0]); allProps += `on${event.charAt(0).toUpperCase() + event.slice(1)}: ${value.replace(/\(.*?\)/g, '')}.bind(this), `; } else { const codeToUse = options.state === 'inline-with-wrappers' ? processCodeBlockInTemplate(code) : code; allProps += `${keyToUse}: ${codeToUse}, `; } } if (allProps.endsWith(', ')) { allProps = allProps.slice(0, -2); } if (allProps.startsWith(', ')) { allProps = allProps.slice(2); } return allProps; }; const handleObjectBindings = (code) => { let objectCode = code.replace(/^{/, '').replace(/}$/, ''); objectCode = objectCode.replace(/\/\/.*\n/g, ''); let temp = objectCode; //STEP 1. remove spread operator for expressions like '{ ...objectName }' and replace them with object name, example {...obj} => obj temp = temp.replace(/\{\s*\.\.\.(\w+)\s*}/g, '$1'); //STEP 2. remove all remaining spread operators that could be nested somewhere deeper, example { ...obj, field1: value1 } => { obj, field1: value1 } temp = temp.replace(/\.\.\./g, ''); //STEP 3. deal with consequences of STEP 2 - for all left field assignments we create new objects provided to useObjectWrapper, //and we get rid of surrounding brackets of the initial input value, example {...obj1,test:true,...obj2} => obj1, {test: true}, obj2 temp = temp.replace(/(\s*\w+\s*:\s*((["'].+["'])|(\[.+])|([\w.]+)))(,|[\n\s]*)/g, `{ $1 },`); // handle template strings if (temp.includes('`')) { // template str let str = temp.match(/`[^`]*`/g); let values = str && str[0].match(/\${[^}]*}/g); let forValues = values === null || values === void 0 ? void 0 : values.map((val) => val.slice(2, -1)).join(' + '); if (str && forValues) { temp = temp.replace(str[0], forValues); } } return temp; }; const processCodeBlockInTemplate = (code) => { // contains helper calls as Angular doesn't support JS expressions in templates if (code.startsWith('{') && code.includes('...')) { // Objects cannot be spread out directly in Angular so we need to use `useObjectWrapper` return `useObjectWrapper(${handleObjectBindings(code)})`; } else if (code.startsWith('Object.values')) { let stripped = code.replace('Object.values', ''); return `useObjectDotValues${stripped}`; } else if (code.includes('JSON.stringify')) { let obj = code.match(/JSON.stringify\((.*)\)/); return `useJsonStringify(${obj})`; } else if (code.includes(' as ')) { const asIndex = code.indexOf('as'); const asCode = code.slice(0, asIndex - 1); return `$any${asCode})`; } else { return `${code}`; } }; const processEventBinding = (key, code, nodeName, customArg) => { let event = key.replace('on', ''); event = event.charAt(0).toLowerCase() + event.slice(1); // TODO: proper babel transform to replace. Util for this const eventName = customArg; const regexp = new RegExp('(^|\\n|\\r| |;|\\(|\\[|!)' + eventName + '(\\?\\.|\\.|\\(| |;|\\)|$)', 'g'); const replacer = '$1$event$2'; const finalValue = (0, remove_surrounding_block_1.removeSurroundingBlock)(code.replace(regexp, replacer)); return { event, value: finalValue, }; }; const stringifyBinding = (node, options, blockOptions) => ([key, binding]) => { var _a, _b; if (key.startsWith('$') || key.startsWith('"') || key === 'key') { return; } if ((binding === null || binding === void 0 ? void 0 : binding.type) === 'spread') { return; } const keyToUse = BINDINGS_MAPPER[key] || key; const { code, arguments: cusArgs = ['event'] } = binding; // TODO: proper babel transform to replace. Util for this if ((0, event_handlers_1.checkIsEvent)(keyToUse)) { const { event, value } = processEventBinding(keyToUse, code, node.name, cusArgs[0]); // native events are all lowerCased const lowerCaseEvent = event.toLowerCase(); const eventKey = (0, event_handlers_1.checkIsBindingNativeEvent)(event) || ((_a = blockOptions.nativeEvents) === null || _a === void 0 ? void 0 : _a.find((nativeEvent) => nativeEvent === keyToUse || nativeEvent === event || nativeEvent === lowerCaseEvent)) ? lowerCaseEvent : event; return ` (${eventKey})="${value}"`; } else if (keyToUse === 'class') { return ` [class]="${code}" `; } else if (keyToUse === 'ref' || keyToUse === 'spreadRef') { return ` #${code} `; } else if ((html_tags_1.VALID_HTML_TAGS.includes(node.name.trim()) || keyToUse.includes('-')) && !((_b = blockOptions.nativeAttributes) === null || _b === void 0 ? void 0 : _b.includes(keyToUse)) && !Object.values(BINDINGS_MAPPER).includes(keyToUse)) { // standard html elements need the attr to satisfy the compiler in many cases: eg: svg elements and [fill] return ` [attr.${keyToUse}]="${code}" `; } else if (keyToUse === 'innerHTML') { return blockOptions.sanitizeInnerHTML ? ` [innerHTML]="${code}" ` : ` [innerHTML]="sanitizer.bypassSecurityTrustHtml(${code})" `; } else { const codeToUse = options.state === 'inline-with-wrappers' ? processCodeBlockInTemplate(code) : code; return `[${keyToUse}]="${codeToUse}"`; } }; const blockToAngular = ({ root, json, options = {}, blockOptions = { nativeAttributes: [], nativeEvents: [], }, rootRef, }) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; const childComponents = (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.childComponents) || []; if (mappers[json.name]) { return mappers[json.name](root, json, options, blockOptions); } if ((0, is_children_1.default)({ node: json })) { return `<ng-content></ng-content>`; } if (json.properties._text) { return json.properties._text; } const textCode = (_a = json.bindings._text) === null || _a === void 0 ? void 0 : _a.code; if (textCode) { if ((0, slots_1.isSlotProperty)(textCode)) { return `<ng-content select="[${(0, slots_1.toKebabSlot)(textCode)}]"></ng-content>`; } if (textCode.includes('JSON.stringify')) { const obj = textCode.replace(/JSON.stringify\(\s*(\w+)\s*,?.*\)/, '$1'); return `{{${obj} | json}}`; } return `{{${textCode}}}`; } let str = ''; if ((0, mitosis_node_1.checkIsForNode)(json)) { const indexName = json.scope.indexName; const forName = json.scope.forName; // Check if "key" is present for the first child of the for loop if ((0, helpers_1.hasFirstChildKeyAttribute)(json)) { const fnIndex = ((_b = root.meta) === null || _b === void 0 ? void 0 : _b._trackByForIndex) || 0; const trackByFnName = `trackBy${forName ? forName.charAt(0).toUpperCase() + forName.slice(1) : ''}${fnIndex}`; root.meta._trackByForIndex = fnIndex + 1; let code = (_c = json.children[0].bindings.key) === null || _c === void 0 ? void 0 : _c.code; root.state[trackByFnName] = { code: `${trackByFnName}(${indexName !== null && indexName !== void 0 ? indexName : '_'}, ${forName}) { return ${code}; }`, type: 'method', }; str += `<ng-container *ngFor="let ${forName !== null && forName !== void 0 ? forName : '_'} of ${(_d = json.bindings.each) === null || _d === void 0 ? void 0 : _d.code}${indexName ? `; index as ${indexName}` : ''}; trackBy: ${trackByFnName}">`; } else { str += `<ng-container *ngFor="let ${forName !== null && forName !== void 0 ? forName : '_'} of ${(_e = json.bindings.each) === null || _e === void 0 ? void 0 : _e.code}${indexName ? `; index as ${indexName}` : ''}">`; } str += json.children .map((item) => (0, exports.blockToAngular)({ root, json: item, options, blockOptions })) .join('\n'); str += `</ng-container>`; } else if (json.name === 'Show') { let condition = (_f = json.bindings.when) === null || _f === void 0 ? void 0 : _f.code; if (options.state === 'inline-with-wrappers' && (condition === null || condition === void 0 ? void 0 : condition.includes('typeof'))) { let wordAfterTypeof = condition.split('typeof')[1].trim().split(' ')[0]; condition = condition.replace(`typeof ${wordAfterTypeof}`, `useTypeOf(${wordAfterTypeof})`); } str += `<ng-container *ngIf="${condition}">`; str += json.children .map((item) => (0, exports.blockToAngular)({ root, json: item, options, blockOptions })) .join('\n'); str += `</ng-container>`; // else condition if ((0, is_mitosis_node_1.isMitosisNode)((_g = json.meta) === null || _g === void 0 ? void 0 : _g.else)) { str += `<ng-container *ngIf="!(${condition})">`; str += (0, exports.blockToAngular)({ root, json: json.meta.else, options, blockOptions }); str += `</ng-container>`; } } else if (json.name.includes('.')) { const elSelector = childComponents.find((impName) => impName === json.name) ? (0, lodash_1.kebabCase)(json.name) : json.name; let allProps = handleNgOutletBindings(json, options); if (options.state === 'class-properties') { const inputsPropsStateName = `mergedInputs_${(0, symbol_processor_1.hashCodeAsString)(allProps)}`; root.state[inputsPropsStateName] = { code: '{}' + (options.typescript ? ' as any' : ''), type: 'property', }; if (!((_h = root.hooks.onInit) === null || _h === void 0 ? void 0 : _h.code.includes(inputsPropsStateName))) { (0, hooks_1.addCodeToOnInit)(root, `this.${inputsPropsStateName} = {${allProps}};`); } if (!((_j = root.hooks.onUpdate) === null || _j === void 0 ? void 0 : _j.map((hook) => hook.code).join('').includes(inputsPropsStateName))) { (0, hooks_1.addCodeToOnUpdate)(root, `this.${inputsPropsStateName} = {${allProps}};`); } allProps = `${inputsPropsStateName}`; } else { allProps = `{ ${allProps} }`; } str += `<ng-container *ngComponentOutlet=" ${elSelector.replace('state.', '').replace('props.', '')}; inputs: ${allProps}; content: myContent; "> `; str += `</ng-container>`; } else { let element = null, classNames = [], attributes; const isComponent = childComponents.find((impName) => impName === json.name); const tagName = (0, get_tag_name_1.getBuilderTagName)(json); const selector = json.meta.selector || (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.selector); if (selector) { try { ({ element, classNames, attributes } = (0, parse_selector_1.parseSelector)(`${selector}`)); } catch (_r) { element = tagName !== null && tagName !== void 0 ? tagName : (0, lodash_1.kebabCase)(json.name); } } if (!element) { if (isComponent) { element = tagName !== null && tagName !== void 0 ? tagName : (0, lodash_1.kebabCase)(json.name); } else { element = tagName !== null && tagName !== void 0 ? tagName : json.name; } } str += `<${element} `; // TODO: merge with existing classes/bindings if (classNames.length) { str += `class="${classNames.join(' ')}" `; } // TODO: Merge with existing properties if (attributes) { Object.entries(attributes).forEach(([key, value]) => { if (value) { str += `${key}=${JSON.stringify(value)} `; } else { str += `${key} `; } }); } for (const key in json.properties) { if (key.startsWith('$')) { continue; } const value = json.properties[key]; str += ` ${key}="${value}" `; } for (const key in json.bindings) { if (((_k = json.bindings[key]) === null || _k === void 0 ? void 0 : _k.type) === 'spread' && html_tags_1.VALID_HTML_TAGS.includes(json.name.trim())) { if (((_l = json.bindings[key]) === null || _l === void 0 ? void 0 : _l.code) === 'this') { // if its an arbitrary { ...props } spread then we skip because Angular needs a named prop to be defined continue; } let refName = ''; if ((_m = json.bindings['spreadRef']) === null || _m === void 0 ? void 0 : _m.code) { refName = json.bindings['spreadRef'].code; } else { const spreadRefIndex = root.meta._spreadRefIndex || 0; refName = `elRef${spreadRefIndex}`; root.meta._spreadRefIndex = spreadRefIndex + 1; json.bindings['spreadRef'] = (0, bindings_1.createSingleBinding)({ code: refName }); root.refs[refName] = { argument: '' }; } json.bindings['spreadRef'] = (0, bindings_1.createSingleBinding)({ code: refName }); root.refs[refName] = { argument: '' }; root.meta.onViewInit = (root.meta.onViewInit || { code: '' }); let spreadCode = ''; let changesCode = ''; if ((_o = json.bindings[key]) === null || _o === void 0 ? void 0 : _o.code.startsWith('{')) { json.meta._spreadStateRef = json.meta._spreadStateRef || 0; const name = `${refName}_state_${json.meta._spreadStateRef}`; json.meta._spreadStateRef = json.meta._spreadStateRef + 1; (0, hooks_1.makeReactiveState)(root, name, `this.${name} = ${(_p = json.bindings[key]) === null || _p === void 0 ? void 0 : _p.code};`); spreadCode = `this.${name}`; changesCode = `changes['${spreadCode.replace('this.', '')}']?.currentValue`; } else { spreadCode = `${(_q = json.bindings[key]) === null || _q === void 0 ? void 0 : _q.code}`; changesCode = `changes['${spreadCode.replace('this.', '')}']?.currentValue`; } (0, hooks_1.addCodeNgAfterViewInit)(root, `\nthis.setAttributes(this.${refName}?.nativeElement, ${spreadCode});`); (0, hooks_1.addCodeToOnUpdate)(root, `this.setAttributes(this.${refName}?.nativeElement, ${spreadCode}${changesCode ? `, ${changesCode}` : ''});`); if (!root.state['setAttributes']) { root.state['setAttributes'] = { code: (0, helpers_1.HELPER_FUNCTIONS)(options === null || options === void 0 ? void 0 : options.typescript).setAttributes, type: 'method', }; } } } const stringifiedBindings = Object.entries(json.bindings) .map(stringifyBinding(json, options, blockOptions)) .join(''); str += stringifiedBindings; if (rootRef && !str.includes(`#${rootRef}`)) { // Add ref for passing attributes str += `#${rootRef}`; } if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(json.name)) { return str + ' />'; } str += '>'; if (json.children) { str += json.children .map((item) => (0, exports.blockToAngular)({ root, json: item, options, blockOptions })) .join('\n'); } str += `</${element}>`; } return str; }; exports.blockToAngular = blockToAngular;