UNPKG

@builder.io/mitosis

Version:

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

378 lines (363 loc) 20.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.componentToAngularSignals = void 0; const helpers_1 = require("../../../generators/angular/helpers"); const format_1 = require("../../../generators/angular/helpers/format"); const get_outputs_1 = require("../../../generators/angular/helpers/get-outputs"); const get_refs_1 = require("../../../generators/angular/helpers/get-refs"); const get_styles_1 = require("../../../generators/angular/helpers/get-styles"); const hooks_1 = require("../../../generators/angular/helpers/hooks"); const blocks_1 = require("../../../generators/angular/signals/blocks"); const helpers_2 = require("../../../generators/angular/signals/helpers"); const get_computed_1 = require("../../../generators/angular/signals/helpers/get-computed"); const get_inputs_1 = require("../../../generators/angular/signals/helpers/get-inputs"); const get_code_processor_plugins_1 = require("../../../generators/angular/signals/plugins/get-code-processor-plugins"); const types_1 = require("../../../generators/angular/types"); const dedent_1 = require("../../../helpers/dedent"); const event_handlers_1 = require("../../../helpers/event-handlers"); const fast_clone_1 = require("../../../helpers/fast-clone"); const get_child_components_1 = require("../../../helpers/get-child-components"); const get_components_used_1 = require("../../../helpers/get-components-used"); const get_props_1 = require("../../../helpers/get-props"); const get_state_object_string_1 = require("../../../helpers/get-state-object-string"); const is_hook_empty_1 = require("../../../helpers/is-hook-empty"); const is_upper_case_1 = require("../../../helpers/is-upper-case"); const merge_options_1 = require("../../../helpers/merge-options"); const render_imports_1 = require("../../../helpers/render-imports"); const strip_meta_properties_1 = require("../../../helpers/strip-meta-properties"); const attribute_passing_1 = require("../../../helpers/web-components/attribute-passing"); const plugins_1 = require("../../../modules/plugins"); const lodash_1 = require("lodash"); const get_dynamic_template_refs_1 = require("./helpers/get-dynamic-template-refs"); const componentToAngularSignals = (userOptions = {}) => { return ({ component }) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10; // Make a copy we can safely mutate, similar to babel's toolchain let json = (0, fast_clone_1.fastClone)(component); // Init compileContext json.compileContext = { angular: { hooks: { ngAfterViewInit: { code: '', }, ngAfterContentInit: { code: '', }, }, extra: { importCalls: [], }, }, }; const options = (0, merge_options_1.initializeOptions)({ target: 'angular', component, defaults: types_1.DEFAULT_ANGULAR_OPTIONS, userOptions, }); options.typescript = true; // Angular uses ts all the time options.api = 'signals'; if (options.plugins) { json = (0, plugins_1.runPreJsonPlugins)({ json, plugins: options.plugins }); } const withAttributePassing = (0, attribute_passing_1.shouldAddAttributePassing)(json, options); const rootRef = (0, attribute_passing_1.getAddAttributePassingRef)(json, options); const domRefs = (0, get_refs_1.getDomRefs)({ json, options, rootRef, withAttributePassing }); let props = Array.from((0, get_props_1.getProps)(json)); const events = props.filter((prop) => (0, event_handlers_1.checkIsEvent)(prop)); const childComponents = (0, get_child_components_1.getChildComponents)(json); props = props.filter((prop) => { // Best practise for Angular is to use Events without "on" // Stencil doesn't need children as a prop return prop !== 'children' && !(0, event_handlers_1.checkIsEvent)(prop); }); const processBindingOptions = { events, props, target: 'angular', skipAppendEmit: true, }; options.plugins = (0, get_code_processor_plugins_1.getCodeProcessorPlugins)(json, options, processBindingOptions); if (options.plugins) { json = (0, plugins_1.runPostJsonPlugins)({ json, plugins: options.plugins }); } // CSS const styles = (0, get_styles_1.getAngularStyles)({ json, options }); // Mitosis Metadata const useMetadata = (_a = json.meta) === null || _a === void 0 ? void 0 : _a.useMetadata; const onPush = ((_b = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _b === void 0 ? void 0 : _b.changeDetection) == 'OnPush'; const writeableSignals = ((_d = (_c = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _c === void 0 ? void 0 : _c.signals) === null || _d === void 0 ? void 0 : _d.writeable) || []; const requiredSignals = ((_f = (_e = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _e === void 0 ? void 0 : _e.signals) === null || _f === void 0 ? void 0 : _f.required) || []; // Context & Injectables const injectables = Object.entries(((_g = json === null || json === void 0 ? void 0 : json.context) === null || _g === void 0 ? void 0 : _g.get) || {}).map(([variableName, { name }]) => { return `public ${variableName} : ${name}`; }); const shouldUseSanitizer = !((_h = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _h === void 0 ? void 0 : _h.sanitizeInnerHTML) && (0, helpers_1.traverseAndCheckIfInnerHTMLIsUsed)(json); if (shouldUseSanitizer) { injectables.push('protected sanitizer: DomSanitizer'); } // HTML let template = json.children .map((item) => { var _a, _b, _c, _d; return (0, blocks_1.blockToAngularSignals)({ root: json, json: item, options, rootRef: withAttributePassing && rootRef === attribute_passing_1.ROOT_REF ? rootRef : undefined, // only pass rootRef if it's not the default blockOptions: { childComponents, nativeAttributes: (_b = (_a = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _a === void 0 ? void 0 : _a.nativeAttributes) !== null && _b !== void 0 ? _b : [], nativeEvents: (_d = (_c = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _c === void 0 ? void 0 : _c.nativeEvents) !== null && _d !== void 0 ? _d : [], sanitizeInnerHTML: !shouldUseSanitizer, }, }); }) .join('\n'); if (options.prettier !== false) { // We need to use 'strict' mode for Angular otherwise it could add spaces around some content template = (0, format_1.tryFormat)(template, 'html', 'strict'); } const { components: dynamicComponents, dynamicTemplate } = (0, helpers_1.traverseToGetAllDynamicComponents)(json, options, { childComponents, nativeAttributes: (_m = (_l = (_k = (_j = json.meta) === null || _j === void 0 ? void 0 : _j.useMetadata) === null || _k === void 0 ? void 0 : _k.angular) === null || _l === void 0 ? void 0 : _l.nativeAttributes) !== null && _m !== void 0 ? _m : [], nativeEvents: (_r = (_q = (_p = (_o = json.meta) === null || _o === void 0 ? void 0 : _o.useMetadata) === null || _p === void 0 ? void 0 : _p.angular) === null || _q === void 0 ? void 0 : _q.nativeEvents) !== null && _r !== void 0 ? _r : [], }, 'signals'); const hasDynamicComponents = dynamicComponents.size > 0; if (hasDynamicComponents) { injectables.push('private viewContainer: ViewContainerRef'); json.compileContext.angular.hooks.ngAfterContentInit.code = `this._updateView();` + json.compileContext.angular.hooks.ngAfterContentInit.code; } // Angular component settings const componentsUsed = Array.from((0, get_components_used_1.getComponentsUsed)(json)).filter((item) => item.length && (0, is_upper_case_1.isUpperCase)(item[0]) && !types_1.BUILT_IN_COMPONENTS.has(item)); const componentSettings = { selector: ((_s = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _s === void 0 ? void 0 : _s.selector) ? `'${(_t = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _t === void 0 ? void 0 : _t.selector}'` : `'${(0, lodash_1.kebabCase)(json.name || 'my-component')}'`, standalone: 'true', imports: `[${['CommonModule', ...componentsUsed].join(', ')}]`, template: `\`${dynamicTemplate}${(0, helpers_1.getTemplateFormat)(template)}\``, }; if (onPush) { componentSettings.changeDetection = 'ChangeDetectionStrategy.OnPush'; } if (styles) { componentSettings.styles = `\`${styles}\``; } if ((_u = useMetadata === null || useMetadata === void 0 ? void 0 : useMetadata.angular) === null || _u === void 0 ? void 0 : _u.skipHydration) { componentSettings.host = `{ ngSkipHydration: 'true' }`; } (0, strip_meta_properties_1.stripMetaProperties)(json); const dataString = (0, get_state_object_string_1.getStateObjectStringFromComponent)(json, { format: 'class', data: true, functions: false, getters: false, valueMapper: (code, _, typeParameter, key) => { var _a; if (typeParameter && !code.length) { console.error(` Component ${json.name} has state property without an initial value'. This will cause an error in Angular. Please add a initial value for every state property even if it's \`undefined\`.`); } // Special case for _listenerFns - don't wrap in signal() if (key === '_listenerFns') { return code; } if (key) { const propRefs = props.filter((prop) => code.includes(`this.${prop}()`)); if (propRefs.length > 0) { if (!((_a = json.hooks.onInit) === null || _a === void 0 ? void 0 : _a.code)) { json.hooks.onInit = { code: ` this.${key}.set(${code}); `, }; } else { json.hooks.onInit.code = `this.${key}.set(${code});`.concat(json.hooks.onInit.code); } return `signal${typeParameter ? `<${typeParameter}>` : ''}(undefined)`; } } return `signal${typeParameter ? `<${typeParameter}>` : ''}(${code})`; }, }); const methodsString = (0, get_state_object_string_1.getStateObjectStringFromComponent)(json, { format: 'class', data: false, functions: true, getters: false, onlyValueMapper: true, valueMapper: (code, type, _, key) => { return code.startsWith('function') ? code.replace('function', '').trim() : code; }, }); // Handle getters as computed signals const gettersString = (0, get_computed_1.getComputedGetters)({ json }); // Check if we need Renderer2 for spread attributes const usesRenderer2 = !!json.state['_listenerFns']; if (usesRenderer2) { injectables.push('private renderer: Renderer2'); } const importsViewChild = hasDynamicComponents || domRefs.size !== 0 || ((_y = (_x = (_w = (_v = json.compileContext) === null || _v === void 0 ? void 0 : _v.angular) === null || _w === void 0 ? void 0 : _w.extra) === null || _x === void 0 ? void 0 : _x.spreadRefs) === null || _y === void 0 ? void 0 : _y.length) > 0; // Imports const emptyOnMount = (0, is_hook_empty_1.isHookEmpty)(json.hooks.onMount); const emptyOnUnMount = (0, is_hook_empty_1.isHookEmpty)(json.hooks.onUnMount); const AfterViewInit = Boolean(!emptyOnMount || withAttributePassing); const OnDestroy = !emptyOnUnMount; const coreImports = (0, helpers_2.getAngularCoreImportsAsString)({ refs: domRefs.size !== 0, input: props.length !== 0, output: events.length !== 0, model: writeableSignals.length !== 0, effect: ((_z = json.hooks.onUpdate) === null || _z === void 0 ? void 0 : _z.length) !== 0, signal: dataString.length !== 0 || hasDynamicComponents, computed: gettersString.length !== 0, onPush, AfterViewInit, OnDestroy, viewChild: importsViewChild, viewContainerRef: hasDynamicComponents, templateRef: hasDynamicComponents, renderer: usesRenderer2, }); // Hooks if (!emptyOnMount) { (0, hooks_1.addCodeNgAfterViewInit)(json, json.hooks.onMount.map((onMount) => onMount.code).join('\n')); } // Angular interfaces const angularInterfaces = []; if (AfterViewInit) { angularInterfaces.push('AfterViewInit'); } if (OnDestroy) { angularInterfaces.push('OnDestroy'); } let str = (0, dedent_1.dedent) ` import { ${coreImports} } from '@angular/core'; import { CommonModule } from '@angular/common'; ${shouldUseSanitizer ? `import { DomSanitizer } from '@angular/platform-browser';` : ''} ${json.types ? json.types.join('\n') : ''} ${(0, helpers_1.getDefaultProps)(json)} ${(0, render_imports_1.renderPreComponent)({ explicitImportFileExtension: options.explicitImportFileExtension, component: json, target: 'angular', preserveFileExtensions: options.preserveFileExtensions, importMapper: (_, theImport, importedValues) => { if (options.defaultExportComponents) return undefined; const { defaultImport } = importedValues; const { path } = theImport; if (defaultImport && componentsUsed.includes(defaultImport)) { return `import { ${defaultImport} } from '${path}';`; } return undefined; }, })} @Component({ ${Object.entries(componentSettings) .map(([k, v]) => `${k}: ${v}`) .join(',')} }) export ${options.defaultExportComponents ? 'default ' : ''}class ${json.name} ${angularInterfaces.length ? ` implements ${angularInterfaces.join(',')}` : ''} { ${(0, lodash_1.uniq)(json.compileContext.angular.extra.importCalls) .map((importCall) => `protected readonly ${importCall} = ${importCall};`) .join('\n')} ${hasDynamicComponents ? (0, get_dynamic_template_refs_1.getDynamicTemplateRefs)(dynamicComponents) : ''} ${(0, get_inputs_1.getSignalInputs)({ json, writeableSignals, requiredSignals, props: Array.from(props), })} ${(0, get_outputs_1.getOutputs)({ json, outputVars: events, api: options.api })} ${Array.from(domRefs) .map((refName) => `${refName} = viewChild<ElementRef>("${refName}")`) .join('\n')} ${((_2 = (_1 = (_0 = json.compileContext) === null || _0 === void 0 ? void 0 : _0.angular) === null || _1 === void 0 ? void 0 : _1.extra) === null || _2 === void 0 ? void 0 : _2.spreadRefs) ? Array.from(new Set(json.compileContext.angular.extra.spreadRefs)) .filter((refName) => !Array.from(domRefs).includes(refName)) .map((refName) => `${refName} = viewChild<ElementRef>("${refName}")`) .join('\n') : ''} ${dataString} ${gettersString} ${methodsString} constructor(${injectables.join(',\n')}) { ${(0, is_hook_empty_1.isHookEmpty)(json.hooks.onUpdate) ? '' : `if (typeof window !== 'undefined') { ${(_3 = json.hooks.onUpdate) === null || _3 === void 0 ? void 0 : _3.map(({ code, depsArray }) => /** * We need allowSignalWrites only for Angular 17 https://angular.dev/api/core/CreateEffectOptions#allowSignalWrites * TODO: remove on 2025-05-15 https://angular.dev/reference/releases#actively-supported-versions */ `effect(() => { ${(depsArray === null || depsArray === void 0 ? void 0 : depsArray.length) ? ` // --- Mitosis: Workaround to make sure the effect() is triggered --- ${depsArray.join('\n')} // --- ` : ''} ${code} }, { allowSignalWrites: true, // Enable writing to signals inside effects } );`).join('\n')} } `}} ${withAttributePassing ? (0, attribute_passing_1.getAttributePassingString)(options.typescript) : ''} ${(0, is_hook_empty_1.isHookEmpty)(json.hooks.onInit) ? '' : `ngOnInit() { ${!((_4 = json.hooks) === null || _4 === void 0 ? void 0 : _4.onInit) ? '' : (_5 = json.hooks.onInit) === null || _5 === void 0 ? void 0 : _5.code} }`} ${!hasDynamicComponents ? '' : ` _updateView() { ${(0, get_dynamic_template_refs_1.getInitEmbedViewCode)(dynamicComponents)} }`} ${ // hooks specific to Angular ((_7 = (_6 = json.compileContext) === null || _6 === void 0 ? void 0 : _6.angular) === null || _7 === void 0 ? void 0 : _7.hooks) ? Object.entries((_9 = (_8 = json.compileContext) === null || _8 === void 0 ? void 0 : _8.angular) === null || _9 === void 0 ? void 0 : _9.hooks) .filter(([_, value]) => !(0, is_hook_empty_1.isHookEmpty)(value)) .map(([key, value]) => { return `${key}() { if (typeof window !== 'undefined') { ${value.code} } }`; }) .join('\n') : ''} ${emptyOnUnMount ? '' : `ngOnDestroy() { ${((_10 = json.hooks.onUnMount) === null || _10 === void 0 ? void 0 : _10.code) || ''} }`} } `; if (options.plugins) { str = (0, plugins_1.runPreCodePlugins)({ json, code: str, plugins: options.plugins }); } if (options.prettier !== false) { str = (0, format_1.tryFormat)(str, 'typescript'); } if (options.plugins) { str = (0, plugins_1.runPostCodePlugins)({ json, code: str, plugins: options.plugins }); } return str; }; }; exports.componentToAngularSignals = componentToAngularSignals;