UNPKG

@builder.io/mitosis

Version:

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

380 lines (379 loc) 16.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.blockToAngularSignals = void 0; const html_tags_1 = require("../../../constants/html_tags"); const helpers_1 = require("../../../generators/angular/helpers"); const parse_selector_1 = require("../../../generators/angular/helpers/parse-selector"); const babel_transform_1 = require("../../../helpers/babel-transform"); 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 slots_1 = require("../../../helpers/slots"); const symbol_processor_1 = require("../../../symbols/symbol-processor"); const types_1 = require("@babel/types"); const function_1 = require("fp-ts/function"); const lodash_1 = require("lodash"); const hooks_1 = require("../helpers/hooks"); const getChildren = (root, json, options, blockOptions) => { var _a; return (_a = json.children) === null || _a === void 0 ? void 0 : _a.map((item) => (0, exports.blockToAngularSignals)({ root, json: item, options, blockOptions })).join('\n'); }; const MAPPERS = { Fragment: (root, json, options, blockOptions) => { const children = getChildren(root, json, options, blockOptions); // TODO: Handle `key`? return `<ng-container>${children}</ng-container>`; }, Slot: (root, json, options, blockOptions) => { const children = getChildren(root, json, options, blockOptions); const namedSlotTransform = 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'); return `<ng-content ${namedSlotTransform}> ${children} </ng-content>`; }, For: (root, json, options, blockOptions) => { var _a, _b, _c; const forNode = json; const indexName = forNode.scope.indexName; const forName = forNode.scope.forName; let trackByFnName; // Check if "key" is present for the first child of the for loop if ((0, helpers_1.hasFirstChildKeyAttribute)(forNode)) { const fnIndex = ((_a = root.meta) === null || _a === void 0 ? void 0 : _a._trackByForIndex) || 0; trackByFnName = `trackBy${forName ? forName.charAt(0).toUpperCase() + forName.slice(1) : ''}${fnIndex}`; root.meta._trackByForIndex = fnIndex + 1; let code = (_b = forNode.children[0].bindings.key) === null || _b === void 0 ? void 0 : _b.code; root.state[trackByFnName] = { code: `${trackByFnName}(${indexName !== null && indexName !== void 0 ? indexName : '_'}: number, ${forName}: any) { return ${code}; }`, type: 'method', }; } const children = getChildren(root, json, options, blockOptions); const item = forName !== null && forName !== void 0 ? forName : '_'; const of = (_c = forNode.bindings.each) === null || _c === void 0 ? void 0 : _c.code; const track = `track ${trackByFnName ? `${trackByFnName}(${indexName !== null && indexName !== void 0 ? indexName : 'i'}, ${forName})` : indexName ? indexName : 'i'};`; const index = indexName ? `let ${indexName} = $index` : 'let i = $index'; return ` @for (${item} of ${of};${track}${index}) { ${children} } `; }, Show: (root, json, options, blockOptions) => { var _a, _b; let condition = (_a = json.bindings.when) === null || _a === void 0 ? void 0 : _a.code; const children = getChildren(root, json, options, blockOptions); let elseBlock = ''; // else condition if ((0, is_mitosis_node_1.isMitosisNode)((_b = json.meta) === null || _b === void 0 ? void 0 : _b.else)) { elseBlock = `@else{ ${(0, exports.blockToAngularSignals)({ root, json: json.meta.else, options, blockOptions })} }`; } if (condition === null || condition === void 0 ? void 0 : condition.includes('children()')) { console.error(` ${json.name}: You can't use children() in a Show block for \`when\` targeting angular. Try to invert it like this: "<Show when={props.label} else={props.children}>{props.label}</Show>" `); } return `@if(${condition}){ ${children} }${elseBlock}`; }, }; // TODO: Maybe in the future allow defining `string | function` as values const BINDINGS_MAPPER = { innerHTML: 'innerHTML', style: 'ngStyle', }; const handleDynamicComponentBindings = (node) => { var _a; let allProps = ''; for (const key in node.properties) { if (key.startsWith('$') || key === 'key') { continue; } const value = node.properties[key]; allProps += `${key}: '${value}', `; } for (const key in node.bindings) { if (key.startsWith('"') || key.startsWith('$') || key === 'key') { continue; } const { code } = node.bindings[key]; if (key === 'ref') { allProps += `${key}: this.${code}(), `; continue; } 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 eventName = (0, event_handlers_1.getEventNameWithoutOn)(key); allProps += `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}: ${code.replace(/\(.*?\)/g, '')}.bind(this), `; } else { allProps += `${keyToUse}: ${code}, `; } } if (allProps.endsWith(', ')) { allProps = allProps.slice(0, -2); } if (allProps.startsWith(', ')) { allProps = allProps.slice(2); } return allProps; }; const codeSetAttributes = (refName, code) => { return `this.setAttributes(this.${refName}()?.nativeElement, ${code});`; }; const saveSpreadRef = (root, refName) => { root.compileContext = root.compileContext || {}; root.compileContext.angular = root.compileContext.angular || { extra: {} }; root.compileContext.angular.extra = root.compileContext.angular.extra || {}; root.compileContext.angular.extra.spreadRefs = root.compileContext.angular.extra.spreadRefs || []; root.compileContext.angular.extra.spreadRefs.push(refName); }; const handleSpreadBinding = (node, binding, root) => { if (html_tags_1.VALID_HTML_TAGS.includes(node.name.trim())) { if (binding.code === 'this') { // if its an arbitrary { ...props } spread then we skip because Angular needs a named prop to be defined return; } let refName = ''; if (node.meta._spreadRefName) { refName = node.meta._spreadRefName; const shouldAddRef = !node.meta._spreadRefAdded; node.meta._spreadRefAdded = true; (0, hooks_1.addCodeToOnUpdate)(root, codeSetAttributes(refName, binding.code)); (0, hooks_1.addCodeNgAfterViewInit)(root, codeSetAttributes(refName, binding.code)); return shouldAddRef ? `#${refName} ` : ''; } const spreadRefIndex = root.meta._spreadRefIndex || 0; refName = `elRef${spreadRefIndex}`; root.meta._spreadRefIndex = spreadRefIndex + 1; node.meta._spreadRefName = refName; node.meta._spreadRefAdded = true; node.bindings['spreadRef'] = (0, bindings_1.createSingleBinding)({ code: refName }); if (!root.refs[refName]) { root.refs[refName] = { argument: '' }; } if (!root.state['_listenerFns']) { root.state['_listenerFns'] = { code: 'new Map()', type: 'property', }; } (0, hooks_1.addCodeToOnUpdate)(root, codeSetAttributes(refName, binding.code)); (0, hooks_1.addCodeNgAfterViewInit)(root, codeSetAttributes(refName, binding.code)); if (!root.state['setAttributes']) { root.state['setAttributes'] = { code: (0, helpers_1.HELPER_FUNCTIONS)(true).setAttributes, type: 'method', }; } if (!root.hooks.onUnMount) { root.hooks.onUnMount = { code: ` for (const fn of this._listenerFns.values()) { fn(); } `, }; } saveSpreadRef(root, refName); return `#${refName} `; } }; const stringifyBinding = (node, blockOptions, root) => ([key, binding]) => { var _a, _b; if (key.startsWith('$') || key === 'key') { return; } if ((binding === null || binding === void 0 ? void 0 : binding.type) === 'spread') { return handleSpreadBinding(node, binding, root); } const keyToUse = BINDINGS_MAPPER[key] || key; if (!binding) return ''; const { code } = binding; if ((0, event_handlers_1.checkIsEvent)(keyToUse)) { const args = binding.arguments || []; const event = (0, event_handlers_1.getEventNameWithoutOn)(keyToUse); if (code.includes('event.target.')) { console.error(` Component ${node.name} has an event ${event} that uses 'event.target.xxx'. This will cause an error in Angular. Please create a new function with the EventTarget and use e.g: '(event.target as HTMLInputElement).value'`); } const value = (0, babel_transform_1.babelTransformExpression)(code, { Identifier(path) { // Only change arguments inside a call expression or event if (((0, types_1.isCallExpression)(path.parent) && args.includes(path.node.name)) || path.node.name === 'event') { path.node.name = '$event'; } }, }); // 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 { return `[${keyToUse}]="${code}"`; } }; const getElementTag = (json, blockOptions) => { const childComponents = (blockOptions === null || blockOptions === void 0 ? void 0 : blockOptions.childComponents) || []; let element, 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 (_a) { 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; } } let additionalString = ''; // TODO: merge with existing classes/bindings if (classNames.length) { additionalString += `class="${classNames.join(' ')}" `; } // TODO: Merge with existing properties if (attributes) { Object.entries(attributes).forEach(([key, value]) => { if (value) { additionalString += `${key}=${JSON.stringify(value)} `; } else { additionalString += `${key} `; } }); } return { element, additionalString }; }; const blockToAngularSignals = ({ root, json, options = {}, blockOptions = { nativeAttributes: [], nativeEvents: [], }, rootRef, }) => { var _a, _b; 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) { return `{{${textCode}}}`; } let str = ''; // Handle dynamic components, state.MyComponent / props.MyComponent if (json.name.includes('.')) { const elSelector = ((_b = blockOptions.childComponents) === null || _b === void 0 ? void 0 : _b.find((impName) => impName === json.name)) ? (0, lodash_1.kebabCase)(json.name) : json.name; const elSelectorProcessed = elSelector.replace('state.', '').replace('props.', ''); const dynamicComponentRef = elSelectorProcessed.replace(/^this\.([^.]+)/, '$1()'); let allProps = handleDynamicComponentBindings(json); const computedName = `dynamicProps_${(0, symbol_processor_1.hashCodeAsString)(allProps)}`; if (allProps) { if (!root.state[computedName]) { root.state[computedName] = { code: `get ${computedName}() { return { ${allProps} }; }`, type: 'getter', }; } } str += `<ng-container *ngComponentOutlet=" ${dynamicComponentRef};${allProps ? `\ninputs: ${computedName}();` : ''} content: myContent(); "> `; str += `</ng-container>`; return str; } const { element, additionalString } = getElementTag(json, blockOptions); str += `<${element} ${additionalString}`; for (const key in json.properties) { const value = json.properties[key]; str += ` ${key}="${value}" `; } const stringifiedBindings = Object.entries(json.bindings) .map(stringifyBinding(json, blockOptions, root)) .join(''); str += stringifiedBindings; if (rootRef && !str.includes(`#${rootRef}`)) { // Add ref for passing attributes str += `#${rootRef}`; } if (html_tags_1.SELF_CLOSING_HTML_TAGS.has(element)) { return str + ' />'; } str += '>'; if (json.children) { str += json.children .map((item) => (0, exports.blockToAngularSignals)({ root, json: item, options, blockOptions })) .join('\n'); } str += `</${element}>`; return str; }; exports.blockToAngularSignals = blockToAngularSignals;