diff --git a/.changeset/polite-pugs-pull.md b/.changeset/polite-pugs-pull.md new file mode 100644 index 0000000000..a15e74df56 --- /dev/null +++ b/.changeset/polite-pugs-pull.md @@ -0,0 +1,31 @@ +--- +'@builder.io/mitosis': patch +--- + +[angular]: Fix issue with events forced to become `toLowerCase()`. + +Based on [choosing-event-names](https://angular.dev/guide/components/outputs#choosing-event-names) custom events are camelCase. +[DOM events](https://www.w3schools.com/jsref/dom_obj_event.asp) are always lower-cased for Angular components. + +Checkout [event-handlers.ts](https://github.com/BuilderIO/mitosis/blob/main/packages/core/src/helpers/event-handlers.ts) for a list of all events that are automatically lower-cased. Everything else will be treated as a custom event and therefore camelCased. + +If you need some other event to be lower-cased you can use `useMetadata.angular.nativeEvents`: + +```tsx +import { useMetadata } from '@builder.io/mitosis'; + +useMetadata({ + angular: { + nativeEvents: ['onNativeEvent'], + }, +}); + +export default function MyComponent(props) { + return ( +
+ console.log(event)} /> + Hello! +
+ ); +} +``` diff --git a/packages/core/src/__tests__/__snapshots__/angular.import.test.ts.snap b/packages/core/src/__tests__/__snapshots__/angular.import.test.ts.snap index 2b45f19ad0..6754f349ba 100644 --- a/packages/core/src/__tests__/__snapshots__/angular.import.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/angular.import.test.ts.snap @@ -5345,7 +5345,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -13218,7 +13220,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/__tests__/__snapshots__/angular.mapper.test.ts.snap b/packages/core/src/__tests__/__snapshots__/angular.mapper.test.ts.snap index c9bec70a6b..6ba96e9d99 100644 --- a/packages/core/src/__tests__/__snapshots__/angular.mapper.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/angular.mapper.test.ts.snap @@ -5432,7 +5432,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -13435,7 +13437,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/__tests__/__snapshots__/angular.state.test.ts.snap b/packages/core/src/__tests__/__snapshots__/angular.state.test.ts.snap index 45acd5b34e..dab0bdc261 100644 --- a/packages/core/src/__tests__/__snapshots__/angular.state.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/angular.state.test.ts.snap @@ -5545,7 +5545,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -13695,7 +13697,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/__tests__/__snapshots__/angular.styles.test.ts.snap b/packages/core/src/__tests__/__snapshots__/angular.styles.test.ts.snap index 04cd7f381e..c9fc1eeb7c 100644 --- a/packages/core/src/__tests__/__snapshots__/angular.styles.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/angular.styles.test.ts.snap @@ -4792,7 +4792,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -11845,7 +11847,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/__tests__/__snapshots__/angular.test.ts.snap b/packages/core/src/__tests__/__snapshots__/angular.test.ts.snap index 7c26917739..60a69dc763 100644 --- a/packages/core/src/__tests__/__snapshots__/angular.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/angular.test.ts.snap @@ -10060,7 +10060,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -10098,7 +10100,9 @@ import { CommonModule } from \\"@angular/common\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -24878,7 +24882,9 @@ import { Component } from \\"@angular/core\\"; Hello! I can run in React, Vue, Solid, or Liquid! @@ -24916,7 +24922,9 @@ import { CommonModule } from \\"@angular/common\\"; Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/__tests__/data/angular/output-event-bindings.raw.tsx b/packages/core/src/__tests__/data/angular/output-event-bindings.raw.tsx index d2fc81aa76..34095f04cc 100644 --- a/packages/core/src/__tests__/data/angular/output-event-bindings.raw.tsx +++ b/packages/core/src/__tests__/data/angular/output-event-bindings.raw.tsx @@ -1,4 +1,10 @@ -import { useState } from '@builder.io/mitosis'; +import { useMetadata, useState } from '@builder.io/mitosis'; + +useMetadata({ + angular: { + nativeEvents: ['onFakeNative'], + }, +}); export default function MyComponent(props) { const [name, setName] = useState('Steve'); @@ -9,6 +15,8 @@ export default function MyComponent(props) { value={name} onChange={(event) => setName(event.target.value)} onChangeOrSomething={(event) => setName(event.target.value)} + onFakeNative={(event) => setName(event.target.value)} + onAnimationEnd={(event) => setName(event.target.value)} /> Hello! I can run in React, Vue, Solid, or Liquid! diff --git a/packages/core/src/generators/alpine/generate.ts b/packages/core/src/generators/alpine/generate.ts index f0a55519cb..93d11d9c11 100644 --- a/packages/core/src/generators/alpine/generate.ts +++ b/packages/core/src/generators/alpine/generate.ts @@ -1,6 +1,8 @@ +import { SELF_CLOSING_HTML_TAGS } from '@/constants/html_tags'; import { ToAlpineOptions } from '@/generators/alpine/types'; import { babelTransformCode } from '@/helpers/babel-transform'; import { dashCase } from '@/helpers/dash-case'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { fastClone } from '@/helpers/fast-clone'; import { getRefs } from '@/helpers/get-refs'; import { getStateObjectStringFromComponent } from '@/helpers/get-state-object-string'; @@ -10,18 +12,17 @@ import { replaceIdentifiers } from '@/helpers/replace-identifiers'; import { stripMetaProperties } from '@/helpers/strip-meta-properties'; import { stripStateAndPropsRefs } from '@/helpers/strip-state-and-props-refs'; import { collectCss } from '@/helpers/styles/collect-css'; -import { MitosisComponent } from '@/types/mitosis-component'; -import { checkIsForNode, ForNode, MitosisNode } from '@/types/mitosis-node'; -import { TranspilerGenerator } from '@/types/transpiler'; -import { camelCase, flowRight as compose, curry, flow } from 'lodash'; -import { format } from 'prettier/standalone'; -import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; import { runPostCodePlugins, runPostJsonPlugins, runPreCodePlugins, runPreJsonPlugins, -} from '../../modules/plugins'; +} from '@/modules/plugins'; +import { MitosisComponent } from '@/types/mitosis-component'; +import { ForNode, MitosisNode, checkIsForNode } from '@/types/mitosis-node'; +import { TranspilerGenerator } from '@/types/transpiler'; +import { camelCase, flowRight as compose, curry, flow } from 'lodash'; +import { format } from 'prettier/standalone'; import { renderMountHook } from './render-mount-hook'; import { hasRootUpdateHook, renderUpdateHooks } from './render-update-hooks'; @@ -167,7 +168,7 @@ const blockToAlpine = (json: MitosisNode | ForNode, options: ToAlpineOptions = { // TODO: proper babel transform to replace. Util for this const useValue = stripStateAndPropsRefs(value); - if (key.startsWith('on')) { + if (checkIsEvent(key)) { str += bindEventHandler(options)(key, value); } else if (key === 'ref') { str += ` x-ref="${useValue}"`; diff --git a/packages/core/src/generators/angular/index.ts b/packages/core/src/generators/angular/index.ts index b31710901d..20bd23ecf9 100644 --- a/packages/core/src/generators/angular/index.ts +++ b/packages/core/src/generators/angular/index.ts @@ -63,6 +63,7 @@ import { ToAngularOptions, } from './types'; +import { checkIsBindingNativeEvent, checkIsEvent } from '@/helpers/event-handlers'; import { parse } from './parse-selector'; const { types } = babel; @@ -226,10 +227,20 @@ const stringifyBinding = const { code, arguments: cusArgs = ['event'] } = binding!; // TODO: proper babel transform to replace. Util for this - if (keyToUse.startsWith('on')) { + if (checkIsEvent(keyToUse)) { const { event, value } = processEventBinding(keyToUse, code, node.name, cusArgs[0]); - // Angular events are all lowerCased - return ` (${event.toLowerCase()})="${value}"`; + + // native events are all lowerCased + const lowerCaseEvent = event.toLowerCase(); + const eventKey = + checkIsBindingNativeEvent(event) || + blockOptions.nativeEvents?.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') { @@ -281,7 +292,7 @@ const handleNgOutletBindings = (node: MitosisNode, options: ToAngularOptions) => let keyToUse = key.includes('-') ? `'${key}'` : key; keyToUse = keyToUse.replace('state.', '').replace('props.', ''); - if (key.startsWith('on')) { + if (checkIsEvent(key)) { const { event, value } = processEventBinding(key, code, node.name, cusArgs[0]); allProps += `on${event.charAt(0).toUpperCase() + event.slice(1)}: ${value.replace( /\(.*?\)/g, @@ -311,6 +322,7 @@ export const blockToAngular = ({ options = {}, blockOptions = { nativeAttributes: [], + nativeEvents: [], }, }: { root: MitosisComponent; @@ -659,7 +671,7 @@ const handleBindings = ( if (item.name === 'For') continue; if (key === 'key') continue; - if (key.startsWith('on')) { + if (checkIsEvent(key)) { const { arguments: cusArgs = ['event'] } = item.bindings[key]!; const eventBindingName = `${generateNewBindingName(index, item.name)}_event`; if ( @@ -691,7 +703,7 @@ const handleBindings = ( })`; } } else if (item.bindings[key]?.code) { - if (item.bindings[key]?.type !== 'spread' && !key.startsWith('on')) { + if (item.bindings[key]?.type !== 'spread' && !checkIsEvent(key)) { json.state[newBindingName] = { code: 'null', type: 'property' }; makeReactiveState( json, @@ -699,7 +711,7 @@ const handleBindings = ( `this.${newBindingName} = ${item.bindings[key]!.code}`, ); item.bindings[key]!.code = `state.${newBindingName}`; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const { arguments: cusArgs = ['event'] } = item.bindings[key]!; if ( item.bindings[key]?.code.trim().startsWith('{') && @@ -941,6 +953,7 @@ export const componentToAngular: TranspilerGenerator = blockOptions: { childComponents, nativeAttributes: useMetadata?.angular?.nativeAttributes ?? [], + nativeEvents: useMetadata?.angular?.nativeEvents ?? [], }, }); if (options.state === 'inline-with-wrappers') { @@ -964,6 +977,7 @@ export const componentToAngular: TranspilerGenerator = { childComponents, nativeAttributes: useMetadata?.angular?.nativeAttributes ?? [], + nativeEvents: useMetadata?.angular?.nativeEvents ?? [], }, ); diff --git a/packages/core/src/generators/angular/types.ts b/packages/core/src/generators/angular/types.ts index ac4ef1cec2..9baa5e67e2 100644 --- a/packages/core/src/generators/angular/types.ts +++ b/packages/core/src/generators/angular/types.ts @@ -26,10 +26,19 @@ export const DEFAULT_ANGULAR_OPTIONS: ToAngularOptions = { }; export type AngularMetadata = { - /* Mitosis uses `attr.XXX` as default see https://angular.io/guide/attribute-binding. - If you want to skip some you can use the 'nativeAttributes'. */ + /** + * Mitosis uses `attr.XXX` as default see https://angular.io/guide/attribute-binding. + * If you want to skip some you can use the 'nativeAttributes'. + */ nativeAttributes?: string[]; - /* Overwrite default selector for component. Default will be kebab case (MyComponent -> my-component) */ + /** + * If you encounter some native events which aren't generated in lower-case. + * Create a new PR inside [event-handlers.ts](https://github.com/BuilderIO/mitosis/blob/main/packages/core/src/helpers/event-handlers.ts) to fix it for all. + */ + nativeEvents?: string[]; + /** + * Overwrite default selector for component. Default will be kebab case (MyComponent -> my-component) + */ selector?: string; }; diff --git a/packages/core/src/generators/helpers/rsc.ts b/packages/core/src/generators/helpers/rsc.ts index 0b25b4f07c..a88dd8a7bb 100644 --- a/packages/core/src/generators/helpers/rsc.ts +++ b/packages/core/src/generators/helpers/rsc.ts @@ -1,3 +1,4 @@ +import { checkIsEvent } from '@/helpers/event-handlers'; import { isMitosisNode } from '@/helpers/is-mitosis-node'; import type { MitosisComponent } from '@/types/mitosis-component'; import type { MitosisNode } from '@/types/mitosis-node'; @@ -18,7 +19,7 @@ export const checkIfIsClientComponent = (json: MitosisComponent) => { let foundEventListener = false; traverse(json).forEach(function (node) { if (isMitosisNode(node) && !checkIsNodeAMitosisComponent(node)) { - if (Object.keys(node.bindings).filter((item) => item.startsWith('on')).length) { + if (Object.keys(node.bindings).filter((item) => checkIsEvent(item)).length) { foundEventListener = true; this.stop(); } diff --git a/packages/core/src/generators/html/generator.ts b/packages/core/src/generators/html/generator.ts index a23ca9ed25..911d4f42cf 100644 --- a/packages/core/src/generators/html/generator.ts +++ b/packages/core/src/generators/html/generator.ts @@ -1,48 +1,50 @@ +import { SELF_CLOSING_HTML_TAGS } from '@/constants/html_tags'; import { ToHtmlOptions } from '@/generators/html/types'; -import { NodePath, types } from '@babel/core'; -import { pipe } from 'fp-ts/lib/function'; -import { camelCase, kebabCase } from 'lodash'; -import traverse from 'neotraverse/legacy'; -import { format } from 'prettier/standalone'; -import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; -import { babelTransformExpression } from '../../helpers/babel-transform'; -import { dashCase } from '../../helpers/dash-case'; -import { fastClone } from '../../helpers/fast-clone'; -import { getPropFunctions } from '../../helpers/get-prop-functions'; -import { getProps } from '../../helpers/get-props'; -import { getPropsRef } from '../../helpers/get-props-ref'; -import { getRefs } from '../../helpers/get-refs'; -import { getStateObjectStringFromComponent } from '../../helpers/get-state-object-string'; -import { hasBindingsText } from '../../helpers/has-bindings-text'; -import { hasComponent } from '../../helpers/has-component'; -import { hasProps } from '../../helpers/has-props'; -import { hasStatefulDom } from '../../helpers/has-stateful-dom'; -import isChildren from '../../helpers/is-children'; -import { isHtmlAttribute } from '../../helpers/is-html-attribute'; -import { isMitosisNode } from '../../helpers/is-mitosis-node'; -import { mapRefs } from '../../helpers/map-refs'; -import { initializeOptions } from '../../helpers/merge-options'; -import { getForArguments } from '../../helpers/nodes/for'; -import { removeSurroundingBlock } from '../../helpers/remove-surrounding-block'; -import { renderPreComponent } from '../../helpers/render-imports'; -import { stripMetaProperties } from '../../helpers/strip-meta-properties'; +import { babelTransformExpression } from '@/helpers/babel-transform'; +import { dashCase } from '@/helpers/dash-case'; +import { checkIsEvent } from '@/helpers/event-handlers'; +import { fastClone } from '@/helpers/fast-clone'; +import { getPropFunctions } from '@/helpers/get-prop-functions'; +import { getProps } from '@/helpers/get-props'; +import { getPropsRef } from '@/helpers/get-props-ref'; +import { getRefs } from '@/helpers/get-refs'; +import { getStateObjectStringFromComponent } from '@/helpers/get-state-object-string'; +import { hasBindingsText } from '@/helpers/has-bindings-text'; +import { hasComponent } from '@/helpers/has-component'; +import { hasProps } from '@/helpers/has-props'; +import { hasStatefulDom } from '@/helpers/has-stateful-dom'; +import { isHtmlAttribute } from '@/helpers/is-html-attribute'; +import { isMitosisNode } from '@/helpers/is-mitosis-node'; +import { mapRefs } from '@/helpers/map-refs'; +import { initializeOptions } from '@/helpers/merge-options'; +import { getForArguments } from '@/helpers/nodes/for'; +import { removeSurroundingBlock } from '@/helpers/remove-surrounding-block'; +import { renderPreComponent } from '@/helpers/render-imports'; +import { stripMetaProperties } from '@/helpers/strip-meta-properties'; import { DO_NOT_USE_ARGS, DO_NOT_USE_CONTEXT_VARS_TRANSFORMS, DO_NOT_USE_VARS_TRANSFORMS, StripStateAndPropsRefsOptions, stripStateAndPropsRefs, -} from '../../helpers/strip-state-and-props-refs'; -import { collectCss } from '../../helpers/styles/collect-css'; +} from '@/helpers/strip-state-and-props-refs'; +import { collectCss } from '@/helpers/styles/collect-css'; import { runPostCodePlugins, runPostJsonPlugins, runPreCodePlugins, runPreJsonPlugins, -} from '../../modules/plugins'; -import { MitosisComponent } from '../../types/mitosis-component'; -import { MitosisNode, checkIsForNode } from '../../types/mitosis-node'; -import { TranspilerGenerator } from '../../types/transpiler'; +} from '@/modules/plugins'; +import { MitosisComponent } from '@/types/mitosis-component'; +import { MitosisNode, checkIsForNode } from '@/types/mitosis-node'; +import { TranspilerGenerator } from '@/types/transpiler'; +import { NodePath, types } from '@babel/core'; +import { pipe } from 'fp-ts/lib/function'; +import { camelCase, kebabCase } from 'lodash'; +import traverse from 'neotraverse/legacy'; + +import isChildren from '@/helpers/is-children'; +import { format } from 'prettier/standalone'; import { stringifySingleScopeOnMount } from '../helpers/on-mount'; type ScopeVars = Array; @@ -424,7 +426,7 @@ const blockToHtml = ( // TODO: proper babel transform to replace. Util for this const useValue = value; - if (key.startsWith('on')) { + if (checkIsEvent(key)) { let event = key.replace('on', '').toLowerCase(); const fnName = camelCase(`on-${elId}-${event}`); const codeContent: string = removeSurroundingBlock( diff --git a/packages/core/src/generators/liquid/generator.ts b/packages/core/src/generators/liquid/generator.ts index a2392e7ebf..bf9a43a0b2 100644 --- a/packages/core/src/generators/liquid/generator.ts +++ b/packages/core/src/generators/liquid/generator.ts @@ -1,4 +1,5 @@ import { ToLiquidOptions } from '@/generators/liquid/types'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { format } from 'prettier/standalone'; import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; import { fastClone } from '../../helpers/fast-clone'; @@ -113,7 +114,7 @@ const blockToLiquid = (json: MitosisNode, options: ToLiquidOptions = {}): string // TODO: proper babel transform to replace. Util for this const useValue = stripStateAndPropsRefs(value); - if (key.startsWith('on')) { + if (checkIsEvent(key)) { // Do nothing } else if (isValidLiquidBinding(useValue)) { str += ` ${key}="{{${useValue}}}" `; diff --git a/packages/core/src/generators/lit/generate.ts b/packages/core/src/generators/lit/generate.ts index a61b5be276..ace3cab4eb 100644 --- a/packages/core/src/generators/lit/generate.ts +++ b/packages/core/src/generators/lit/generate.ts @@ -1,6 +1,7 @@ import { ToLitOptions } from '@/generators/lit/types'; import { dashCase } from '@/helpers/dash-case'; import { dedent } from '@/helpers/dedent'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { fastClone } from '@/helpers/fast-clone'; import { filterEmptyTextNodes } from '@/helpers/filter-empty-text-nodes'; import { getProps } from '@/helpers/get-props'; @@ -92,7 +93,7 @@ const blockToLit = (json: MitosisNode, options: ToLitOptions = {}): string => { // TODO: maybe use ref directive instead // https://lit.dev/docs/templates/directives/#ref str += ` ref="${code}" `; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const asyncKeyword = json.bindings[key]?.async ? 'async ' : ''; const useKey = '@' + key.substring(2).toLowerCase(); diff --git a/packages/core/src/generators/marko/generate.ts b/packages/core/src/generators/marko/generate.ts index 257ac43b73..edd79ab8d8 100644 --- a/packages/core/src/generators/marko/generate.ts +++ b/packages/core/src/generators/marko/generate.ts @@ -2,6 +2,7 @@ import { SELF_CLOSING_HTML_TAGS } from '@/constants/html_tags'; import { ToMarkoOptions } from '@/generators/marko/types'; import { dashCase } from '@/helpers/dash-case'; import { dedent } from '@/helpers/dedent'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { fastClone } from '@/helpers/fast-clone'; import { filterEmptyTextNodes } from '@/helpers/filter-empty-text-nodes'; import { getRefs } from '@/helpers/get-refs'; @@ -90,7 +91,7 @@ const blockToMarko = (json: MitosisNode, options: InternalToMarkoOptions): strin str += ` ...(${code}) `; } else if (key === 'ref') { str += ` key="${camelCase(code)}" `; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const asyncKeyword = async ? 'async ' : ''; const useKey = key === 'onChange' && json.name === 'input' ? 'onInput' : key; str += ` ${dashCase(useKey)}=(${asyncKeyword}(${cusArgs.join(',')}) => ${processBinding( diff --git a/packages/core/src/generators/mitosis/generator.ts b/packages/core/src/generators/mitosis/generator.ts index 6dc1770eb5..98998f4f06 100644 --- a/packages/core/src/generators/mitosis/generator.ts +++ b/packages/core/src/generators/mitosis/generator.ts @@ -1,27 +1,29 @@ +import { HOOKS } from '@/constants/hooks'; +import { SELF_CLOSING_HTML_TAGS } from '@/constants/html_tags'; import { ToMitosisOptions } from '@/generators/mitosis/types'; +import { dedent } from '@/helpers/dedent'; +import { checkIsEvent } from '@/helpers/event-handlers'; +import { fastClone } from '@/helpers/fast-clone'; +import { getComponents } from '@/helpers/get-components'; +import { getRefs } from '@/helpers/get-refs'; +import { getStateObjectStringFromComponent } from '@/helpers/get-state-object-string'; import { isMitosisNode } from '@/helpers/is-mitosis-node'; +import { isRootTextNode } from '@/helpers/is-root-text-node'; +import { mapRefs } from '@/helpers/map-refs'; +import { renderPreComponent } from '@/helpers/render-imports'; +import { checkHasState } from '@/helpers/state'; import { runPostCodePlugins, runPostJsonPlugins, runPreCodePlugins, runPreJsonPlugins, } from '@/modules/plugins'; +import { MitosisComponent } from '@/types/mitosis-component'; +import { MitosisNode, checkIsForNode, checkIsShowNode } from '@/types/mitosis-node'; +import { TranspilerGenerator } from '@/types/transpiler'; import json5 from 'json5'; import { format } from 'prettier/standalone'; -import { HOOKS } from '../../constants/hooks'; -import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; -import { dedent } from '../../helpers/dedent'; -import { fastClone } from '../../helpers/fast-clone'; -import { getComponents } from '../../helpers/get-components'; -import { getRefs } from '../../helpers/get-refs'; -import { getStateObjectStringFromComponent } from '../../helpers/get-state-object-string'; -import { isRootTextNode } from '../../helpers/is-root-text-node'; -import { mapRefs } from '../../helpers/map-refs'; -import { renderPreComponent } from '../../helpers/render-imports'; -import { checkHasState } from '../../helpers/state'; -import { MitosisComponent } from '../../types/mitosis-component'; -import { MitosisNode, checkIsForNode, checkIsShowNode } from '../../types/mitosis-node'; -import { TranspilerGenerator } from '../../types/transpiler'; + import { blockToReact, componentToReact } from '../react'; export const DEFAULT_FORMAT: ToMitosisOptions['format'] = 'legacy'; @@ -144,7 +146,7 @@ export const blockToMitosis = ( if (json.bindings[key]?.type === 'spread') { str += ` {...(${json.bindings[key]?.code})} `; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const { arguments: cusArgs = ['event'], async } = json.bindings[key]!; const asyncKeyword = async ? 'async ' : ''; str += ` ${key}={${asyncKeyword}(${cusArgs.join(',')}) => ${value.replace(/\s*;$/, '')}} `; diff --git a/packages/core/src/generators/qwik/helpers/add-prevent-default.ts b/packages/core/src/generators/qwik/helpers/add-prevent-default.ts index 72776ca000..1592b9c757 100644 --- a/packages/core/src/generators/qwik/helpers/add-prevent-default.ts +++ b/packages/core/src/generators/qwik/helpers/add-prevent-default.ts @@ -1,6 +1,7 @@ +import { checkIsEvent } from '@/helpers/event-handlers'; +import { isMitosisNode } from '@/helpers/is-mitosis-node'; +import { MitosisComponent } from '@/types/mitosis-component'; import traverse from 'neotraverse/legacy'; -import { isMitosisNode } from '../../../helpers/is-mitosis-node'; -import { MitosisComponent } from '../../../types/mitosis-component'; /** * Find event handlers that explicitly call .preventDefault() and @@ -12,7 +13,7 @@ export function addPreventDefault(json: MitosisComponent) { if (isMitosisNode(node)) { if (node.bindings) { for (const key of Object.keys(node.bindings)) { - if (key.startsWith('on')) { + if (checkIsEvent(key)) { if (node.bindings[key]?.code.includes('.preventDefault()')) { const event = key.slice(2).toLowerCase(); node.properties['preventdefault:' + event] = ''; diff --git a/packages/core/src/generators/qwik/helpers/handlers.ts b/packages/core/src/generators/qwik/helpers/handlers.ts index 24abda603d..a4fdcfddd2 100644 --- a/packages/core/src/generators/qwik/helpers/handlers.ts +++ b/packages/core/src/generators/qwik/helpers/handlers.ts @@ -1,3 +1,4 @@ +import { checkIsEvent } from '@/helpers/event-handlers'; import { MitosisNode } from '../../../types/mitosis-node'; import { renderUseLexicalScope } from '../component'; import { arrowFnBlock, EmitFn, File, SrcBuilder } from '../src-generator'; @@ -61,5 +62,5 @@ function renderHandler(file: File, symbol: string, code: string) { } function isEventName(name: string) { - return name.startsWith('on') && name.charAt(2).toUpperCase() == name.charAt(2); + return checkIsEvent(name) && name.charAt(2).toUpperCase() == name.charAt(2); } diff --git a/packages/core/src/generators/qwik/src-generator.ts b/packages/core/src/generators/qwik/src-generator.ts index 5db00c84c0..abbf04ddd5 100644 --- a/packages/core/src/generators/qwik/src-generator.ts +++ b/packages/core/src/generators/qwik/src-generator.ts @@ -1,6 +1,7 @@ import parserTypeScript from 'prettier/parser-typescript'; import { format } from 'prettier/standalone'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; import { convertExportDefaultToReturn } from '../../parsers/builder'; import { stableJSONserialize } from './helpers/stable-serialize'; @@ -424,7 +425,7 @@ export class SrcBuilder { } function isEvent(name: string): boolean { - return name.startsWith('on') && isUppercase(name.charAt(2)) && !name.endsWith('$'); + return checkIsEvent(name) && isUppercase(name.charAt(2)) && !name.endsWith('$'); } function isUppercase(ch: string): boolean { diff --git a/packages/core/src/generators/react/blocks.ts b/packages/core/src/generators/react/blocks.ts index 3d7fc3736e..40dc24b97d 100644 --- a/packages/core/src/generators/react/blocks.ts +++ b/packages/core/src/generators/react/blocks.ts @@ -1,3 +1,4 @@ +import { checkIsEvent } from '@/helpers/event-handlers'; import { filterEmptyTextNodes } from '@/helpers/filter-empty-text-nodes'; import isChildren from '@/helpers/is-children'; import { isRootTextNode } from '@/helpers/is-root-text-node'; @@ -296,7 +297,7 @@ export const blockToReact = ( } if (json.bindings[key]?.type === 'spread') { str += ` {...(${value})} `; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const asyncKeyword = json.bindings[key]?.async ? 'async ' : ''; const { arguments: cusArgs = ['event'] } = json.bindings[key]!; const eventName = options.type === 'native' ? NATIVE_EVENT_MAPPER[key] || key : key; diff --git a/packages/core/src/generators/solid/blocks.ts b/packages/core/src/generators/solid/blocks.ts index 3f6e2f0039..8f3b57741d 100644 --- a/packages/core/src/generators/solid/blocks.ts +++ b/packages/core/src/generators/solid/blocks.ts @@ -1,4 +1,5 @@ import { babelTransformExpression } from '@/helpers/babel-transform'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { filterEmptyTextNodes } from '@/helpers/filter-empty-text-nodes'; import { isMitosisNode } from '@/helpers/is-mitosis-node'; import { objectHasKey } from '@/helpers/typescript'; @@ -89,7 +90,7 @@ export const blockToSolid = ( if (type === 'spread') { str += ` {...(${code})} `; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { const useKey = key === 'onChange' && json.name === 'input' ? 'onInput' : key; const asyncKeyword = json.bindings[key]?.async ? 'async ' : ''; str += ` ${useKey}={${asyncKeyword}(${cusArg.join(',')}) => ${code}} `; diff --git a/packages/core/src/generators/stencil/helpers/index.ts b/packages/core/src/generators/stencil/helpers/index.ts index 61c5bb06bd..f4dd441239 100644 --- a/packages/core/src/generators/stencil/helpers/index.ts +++ b/packages/core/src/generators/stencil/helpers/index.ts @@ -1,11 +1,12 @@ import { ToStencilOptions } from '@/generators/stencil/types'; import { dashCase } from '@/helpers/dash-case'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { renderPreComponent } from '@/helpers/render-imports'; import { stripStateAndPropsRefs } from '@/helpers/strip-state-and-props-refs'; import { MitosisComponent, MitosisState } from '@/types/mitosis-component'; import { MitosisNode } from '@/types/mitosis-node'; -export const isEvent = (key: string): boolean => key.startsWith('on'); +export const isEvent = (key: string): boolean => checkIsEvent(key); /** * We need to "emit" events those can be on multiple places, so we do it as post step diff --git a/packages/core/src/generators/svelte/blocks.ts b/packages/core/src/generators/svelte/blocks.ts index ae492cfbe1..7fe2388256 100644 --- a/packages/core/src/generators/svelte/blocks.ts +++ b/packages/core/src/generators/svelte/blocks.ts @@ -1,4 +1,5 @@ import { createSingleBinding } from '@/helpers/bindings'; +import { checkIsEvent } from '@/helpers/event-handlers'; import isChildren from '@/helpers/is-children'; import { isUpperCase } from '@/helpers/is-upper-case'; import { getForArguments } from '@/helpers/nodes/for'; @@ -222,7 +223,7 @@ const stringifyBinding = if (type === 'spread') { const spreadValue = key === 'props' ? '$$props' : code; return ` {...${spreadValue}} `; - } else if (key.startsWith('on') && isValidHtmlTag) { + } else if (checkIsEvent(key) && isValidHtmlTag) { const { async } = binding; // handle html native on[event] props const event = key.replace('on', '').toLowerCase(); @@ -236,7 +237,7 @@ const stringifyBinding = const asyncKeyword = async ? 'async ' : ''; return ` on:${event}="{${asyncKeyword}(${cusArgs.join(',')}) => {${valueWithoutBlock}}}" `; } - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { // handle on[custom event] props const valueWithoutBlock = removeSurroundingBlock(code); diff --git a/packages/core/src/generators/swift/generator.ts b/packages/core/src/generators/swift/generator.ts index 5e9370c853..af7a87ee47 100644 --- a/packages/core/src/generators/swift/generator.ts +++ b/packages/core/src/generators/swift/generator.ts @@ -1,4 +1,5 @@ import { ToSwiftOptions } from '@/generators/swift/types'; +import { checkIsEvent } from '@/helpers/event-handlers'; import traverse from 'neotraverse/legacy'; import { dedent } from '../../helpers/dedent'; import { fastClone } from '../../helpers/fast-clone'; @@ -149,7 +150,7 @@ const blockToSwift = (json: MitosisNode, options: ToSwiftOptions): string => { continue; } - if (key.startsWith('on')) { + if (checkIsEvent(key)) { if (key === 'onClick') { continue; } else { diff --git a/packages/core/src/generators/template/generator.ts b/packages/core/src/generators/template/generator.ts index 9c8325d4e1..4877477662 100644 --- a/packages/core/src/generators/template/generator.ts +++ b/packages/core/src/generators/template/generator.ts @@ -1,4 +1,5 @@ import { ToTemplateOptions } from '@/generators/template/types'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { format } from 'prettier/standalone'; import { SELF_CLOSING_HTML_TAGS } from '../../constants/html_tags'; import { dedent } from '../../helpers/dedent'; @@ -76,7 +77,7 @@ const blockToTemplate = (json: MitosisNode, options: ToTemplateOptions = {}) => // TODO: proper babel transform to replace. Util for this const useValue = value; - if (key.startsWith('on')) { + if (checkIsEvent(key)) { // Do nothing } else { str += ` ${key}="\${${useValue}}" `; diff --git a/packages/core/src/generators/vue/blocks.ts b/packages/core/src/generators/vue/blocks.ts index 639e7854f1..0f6f7e89fa 100644 --- a/packages/core/src/generators/vue/blocks.ts +++ b/packages/core/src/generators/vue/blocks.ts @@ -1,4 +1,5 @@ import { createSingleBinding } from '@/helpers/bindings'; +import { checkIsEvent } from '@/helpers/event-handlers'; import { filterEmptyTextNodes } from '@/helpers/filter-empty-text-nodes'; import isChildren from '@/helpers/is-children'; import { isMitosisNode } from '@/helpers/is-mitosis-node'; @@ -130,7 +131,7 @@ const stringifyBinding = // TODO: proper babel transform to replace. Util for this const useValue = value?.code || ''; - if (key.startsWith('on') && isValidHtmlTag) { + if (checkIsEvent(key) && isValidHtmlTag) { // handle html native on[event] props const { arguments: cusArgs = ['event'], async } = value; let event = key.replace('on', '').toLowerCase(); @@ -154,7 +155,7 @@ const stringifyBinding = const eventHandlerKey = `${SPECIAL_PROPERTIES.V_ON_AT}${event}`; return `${eventHandlerKey}="${eventHandlerValue}"`; - } else if (key.startsWith('on')) { + } else if (checkIsEvent(key)) { // handle on[custom event] props const { arguments: cusArgs = ['event'] } = node.bindings[key]!; return `:${key}="(${cusArgs.join(',')}) => ${encodeQuotes(useValue)}"`; diff --git a/packages/core/src/helpers/event-handlers.ts b/packages/core/src/helpers/event-handlers.ts index d263630434..0f938db322 100644 --- a/packages/core/src/helpers/event-handlers.ts +++ b/packages/core/src/helpers/event-handlers.ts @@ -1 +1,110 @@ -export const checkIsBindingEventHandler = (code: string) => code.startsWith('on'); +export const checkIsEvent = (code: string) => code.startsWith('on'); + +const nativeEvents = [ + 'abort', + 'animationcancel', + 'animationend', + 'animationiteration', + 'animationstart', + 'auxclick', + 'beforeinput', + 'beforetoggle', + 'blur', + 'cancel', + 'canplay', + 'canplaythrough', + 'change', + 'click', + 'close', + 'compositionend', + 'compositionstart', + 'compositionupdate', + 'contextlost', + 'contextmenu', + 'contextrestored', + 'copy', + 'cuechange', + 'cut', + 'dblclick', + 'drag', + 'dragend', + 'dragenter', + 'dragleave', + 'dragover', + 'dragstart', + 'drop', + 'durationchange', + 'emptied', + 'ended', + 'error', + 'focus', + 'focusin', + 'focusout', + 'formdata', + 'gotpointercapture', + 'input', + 'invalid', + 'keydown', + 'keypress', + 'keyup', + 'load', + 'loadeddata', + 'loadedmetadata', + 'loadstart', + 'lostpointercapture', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + 'paste', + 'pause', + 'play', + 'playing', + 'pointercancel', + 'pointerdown', + 'pointerenter', + 'pointerleave', + 'pointermove', + 'pointerout', + 'pointerover', + 'pointerup', + 'progress', + 'ratechange', + 'reset', + 'resize', + 'scroll', + 'scrollend', + 'securitypolicyviolation', + 'seeked', + 'seeking', + 'select', + 'selectionchange', + 'selectstart', + 'slotchange', + 'stalled', + 'submit', + 'suspend', + 'timeupdate', + 'toggle', + 'touchcancel', + 'touchend', + 'touchmove', + 'touchstart', + 'transitioncancel', + 'transitionend', + 'transitionrun', + 'transitionstart', + 'volumechange', + 'waiting', + 'webkitanimationend', + 'webkitanimationiteration', + 'webkitanimationstart', + 'webkittransitionend', + 'wheel', +]; + +export const checkIsBindingNativeEvent = (code: string) => + !!nativeEvents.find((nativeEvent) => nativeEvent === code.toLowerCase()); diff --git a/packages/core/src/helpers/is-children.ts b/packages/core/src/helpers/is-children.ts index acbceac1cd..7c7ef76a8b 100644 --- a/packages/core/src/helpers/is-children.ts +++ b/packages/core/src/helpers/is-children.ts @@ -1,4 +1,4 @@ -import { MitosisNode } from '../types/mitosis-node'; +import { MitosisNode } from '@/types/mitosis-node'; export const getTextValue = (node: MitosisNode) => { const textValue = node.bindings._text?.code || node.properties.__text || '';