diff --git a/core/package-lock.json b/core/package-lock.json index 32f39c5e9b..00cb7c513a 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -27,7 +27,7 @@ "@stencil/angular-output-target": "^0.8.3", "@stencil/react-output-target": "^0.5.3", "@stencil/sass": "^3.0.7", - "@stencil/vue-output-target": "^0.8.6", + "@stencil/vue-output-target": "^0.8.7", "@types/jest": "^29.5.6", "@types/node": "^14.6.0", "@typescript-eslint/eslint-plugin": "^6.7.2", @@ -1793,9 +1793,9 @@ } }, "node_modules/@stencil/vue-output-target": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.6.tgz", - "integrity": "sha512-B1gQW8FWeU7x/KBPm9R28jYFGN5NsZTZR4jwfDMhKBmU1Q2dIxFY52ARhbrfj5tJQxKoxr2tQJD2S14r9t1v7w==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.7.tgz", + "integrity": "sha512-hgOzbKKgLdCFrhLpmaw/qQrPSXl6hZ09K+j3p/iWh3esq6sxnwuW1PJKLniwkT4Z/JlDIk6stGPGQYi+WE5I2Q==", "dev": true, "peerDependencies": { "@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0" @@ -11248,9 +11248,9 @@ "requires": {} }, "@stencil/vue-output-target": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.6.tgz", - "integrity": "sha512-B1gQW8FWeU7x/KBPm9R28jYFGN5NsZTZR4jwfDMhKBmU1Q2dIxFY52ARhbrfj5tJQxKoxr2tQJD2S14r9t1v7w==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.7.tgz", + "integrity": "sha512-hgOzbKKgLdCFrhLpmaw/qQrPSXl6hZ09K+j3p/iWh3esq6sxnwuW1PJKLniwkT4Z/JlDIk6stGPGQYi+WE5I2Q==", "dev": true, "requires": {} }, diff --git a/core/package.json b/core/package.json index 95a2b1d1dd..e061c41a94 100644 --- a/core/package.json +++ b/core/package.json @@ -49,7 +49,7 @@ "@stencil/angular-output-target": "^0.8.3", "@stencil/react-output-target": "^0.5.3", "@stencil/sass": "^3.0.7", - "@stencil/vue-output-target": "^0.8.6", + "@stencil/vue-output-target": "^0.8.7", "@types/jest": "^29.5.6", "@types/node": "^14.6.0", "@typescript-eslint/eslint-plugin": "^6.7.2", diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 0107c8f76b..8356ddb059 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -197,20 +197,17 @@ export const config: Config = { { elements: ['ion-checkbox', 'ion-toggle'], targetAttr: 'checked', - event: 'v-ion-change', - externalEvent: 'ionChange' + event: 'ion-change' }, { elements: ['ion-datetime', 'ion-radio-group', 'ion-radio', 'ion-range', 'ion-segment', 'ion-segment-button', 'ion-select', 'ion-accordion-group'], targetAttr: 'value', - event: 'v-ion-change', - externalEvent: 'ionChange' + event: 'ion-change', }, { elements: ['ion-input', 'ion-searchbar', 'ion-textarea'], targetAttr: 'value', - event: 'v-ion-input', - externalEvent: 'ionInput' + event: 'ion-input', } ], }), diff --git a/packages/vue/src/ionic-vue.ts b/packages/vue/src/ionic-vue.ts index 7c3a2c48a8..8f8260389e 100644 --- a/packages/vue/src/ionic-vue.ts +++ b/packages/vue/src/ionic-vue.ts @@ -4,24 +4,10 @@ import type { App, Plugin } from "vue"; // TODO(FW-2969): types -/** - * We need to make sure that the web component fires an event - * that will not conflict with the user's @ionChange binding, - * otherwise the binding's callback will fire before any - * v-model values have been updated. - */ const toKebabCase = (eventName: string) => { - const kebabConvert = (name: string) => - name.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase(); - - switch (eventName) { - case "ionInput": - return "v-ion-input"; - case "ionChange": - return "v-ion-change"; - default: - return kebabConvert(eventName); - } + return eventName + .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2") + .toLowerCase(); }; const getHelperFunctions = () => { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 99d6dd3dcd..5a3451f861 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -95,7 +95,7 @@ export const IonAccordionGroup = /*@__PURE__*/ defineContainer('ion-avatar', defineIonAvatar); @@ -219,7 +219,7 @@ export const IonCheckbox = /*@__PURE__*/ defineContainer('ion-chip', defineIonChip, [ @@ -308,7 +308,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer('ion-datetime-button', defineIonDatetimeButton, [ @@ -435,7 +435,7 @@ export const IonInput = /*@__PURE__*/ defineContainer('ion-item', defineIonItem, [ @@ -591,7 +591,7 @@ export const IonRadio = /*@__PURE__*/ defineContainer('ion-radio-group', defineIonRadioGroup, [ @@ -601,7 +601,7 @@ export const IonRadioGroup = /*@__PURE__*/ defineContainer('ion-range', defineIonRange, [ @@ -630,7 +630,7 @@ export const IonRange = /*@__PURE__*/ defineContainer('ion-refresher', defineIonRefresher, [ @@ -699,7 +699,7 @@ export const IonSearchbar = /*@__PURE__*/ defineContainer('ion-segment', defineIonSegment, [ @@ -713,7 +713,7 @@ export const IonSegment = /*@__PURE__*/ defineContainer('ion-segment-button', defineIonSegmentButton, [ @@ -722,7 +722,7 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer('ion-select', defineIonSelect, [ @@ -753,7 +753,7 @@ export const IonSelect = /*@__PURE__*/ defineContainer('ion-select-option', defineIonSelectOption, [ @@ -824,7 +824,7 @@ export const IonTextarea = /*@__PURE__*/ defineContainer('ion-thumbnail', defineIonThumbnail); @@ -853,7 +853,7 @@ export const IonToggle = /*@__PURE__*/ defineContainer('ion-toolbar', defineIonToolbar, [ diff --git a/packages/vue/src/vue-component-lib/utils.ts b/packages/vue/src/vue-component-lib/utils.ts index 826e78e992..136041073e 100644 --- a/packages/vue/src/vue-component-lib/utils.ts +++ b/packages/vue/src/vue-component-lib/utils.ts @@ -1,6 +1,6 @@ // @ts-nocheck // It's easier and safer for Volar to disable typechecking and let the return type inference do its job. -import { VNode, defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue'; +import { defineComponent, getCurrentInstance, h, inject, ref, Ref, withDirectives } from 'vue'; export interface InputProps { modelValue?: T; @@ -53,16 +53,13 @@ const getElementClasses = ( * to customElements.define. Only set if `includeImportCustomElements: true` in your config. * @prop modelProp - The prop that v-model binds to (i.e. value) * @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange) - * @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been - * correctly updated when a user's event callback fires. */ export const defineContainer = ( name: string, defineCustomElement: any, componentProps: string[] = [], modelProp?: string, - modelUpdateEvent?: string, - externalModelUpdateEvent?: string + modelUpdateEvent?: string ) => { /** * Create a Vue component wrapper around a Web Component. @@ -78,29 +75,27 @@ export const defineContainer = ( let modelPropValue = props[modelProp]; const containerRef = ref(); const classes = new Set(getComponentClasses(attrs.class)); - const onVnodeBeforeMount = (vnode: VNode) => { - // Add a listener to tell Vue to update the v-model - if (vnode.el) { + + /** + * This directive is responsible for updating any reactive + * reference associated with v-model on the component. + * This code must be run inside of the "created" callback. + * Since the following listener callbacks as well as any potential + * event callback defined in the developer's app are set on + * the same element, we need to make sure the following callbacks + * are set first so they fire first. If the developer's callback fires first + * then the reactive reference will not have been updated yet. + */ + const vModelDirective = { + created: (el: HTMLElement) => { const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent]; eventsNames.forEach((eventName: string) => { - vnode.el!.addEventListener(eventName.toLowerCase(), (e: Event) => { + el.addEventListener(eventName.toLowerCase(), (e: Event) => { modelPropValue = (e?.target as any)[modelProp]; emit(UPDATE_VALUE_EVENT, modelPropValue); - - /** - * We need to emit the change event here - * rather than on the web component to ensure - * that any v-model bindings have been updated. - * Otherwise, the developer will listen on the - * native web component, but the v-model will - * not have been updated yet. - */ - if (externalModelUpdateEvent) { - emit(externalModelUpdateEvent, e); - } }); }); - } + }, }; const currentInstance = getCurrentInstance(); @@ -146,7 +141,6 @@ export const defineContainer = ( ref: containerRef, class: getElementClasses(containerRef, classes), onClick: handleClick, - onVnodeBeforeMount: modelUpdateEvent ? onVnodeBeforeMount : undefined, }; /** @@ -182,7 +176,12 @@ export const defineContainer = ( } } - return h(name, propsToAdd, slots.default && slots.default()); + /** + * vModelDirective is only needed on components that support v-model. + * As a result, we conditionally call withDirectives with v-model components. + */ + const node = h(name, propsToAdd, slots.default && slots.default()); + return modelProp === undefined ? node : withDirectives(node, [[vModelDirective]]); }; }); @@ -199,7 +198,7 @@ export const defineContainer = ( if (modelProp) { Container.props[MODEL_VALUE] = DEFAULT_EMPTY_PROP; - Container.emits = [UPDATE_VALUE_EVENT, externalModelUpdateEvent]; + Container.emits = [UPDATE_VALUE_EVENT]; } }