@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
JavaScript
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;
;