Skip to content

Commit bc385bb

Browse files
feat(vue): implement custom event property as source for v-model (#689)
* feat: implement custom event property as source for v-model * feat: add `useEventDetail` option * docs: update README.md & grammar * style: format code * test: implement tests for new v-model event attribute use * feat: refactor code to support path dot syntax & remove `useEventDetail` * feat: simplify resolve path function * feat(vue): add nested event to checkbox component to showcase nested v-model support * test(vue): add e2e tests for checkbox component with nested v-model support --------- Co-authored-by: John Jenkins <johnljenkins@Hotmail.com>
1 parent 16f1fd1 commit bc385bb

17 files changed

Lines changed: 229 additions & 119 deletions

File tree

example-project/component-library-angular/projects/library/src/directives/proxies.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ export declare interface MyButtonScoped extends Components.MyButtonScoped {
106106
template: '<ng-content></ng-content>',
107107
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
108108
inputs: ['alignment', 'checked', 'color', 'disabled', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
109-
outputs: ['ionChange', 'ionFocus', 'ionBlur'],
109+
outputs: ['ionChange', 'ionChangeNested', 'ionFocus', 'ionBlur'],
110110
})
111111
export class MyCheckbox {
112112
protected el: HTMLMyCheckboxElement;
113113
@Output() ionChange = new EventEmitter<CustomEvent<IMyCheckboxCheckboxChangeEventDetail>>();
114+
@Output() ionChangeNested = new EventEmitter<CustomEvent<IMyCheckboxCheckboxChangeNestedEventDetail>>();
114115
@Output() ionFocus = new EventEmitter<CustomEvent<void>>();
115116
@Output() ionBlur = new EventEmitter<CustomEvent<void>>();
116117
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -121,6 +122,7 @@ export class MyCheckbox {
121122

122123

123124
import type { CheckboxChangeEventDetail as IMyCheckboxCheckboxChangeEventDetail } from 'component-library/components';
125+
import type { CheckboxChangeNestedEventDetail as IMyCheckboxCheckboxChangeNestedEventDetail } from 'component-library/components';
124126

125127
export declare interface MyCheckbox extends Components.MyCheckbox {
126128
/**
@@ -129,6 +131,11 @@ export declare interface MyCheckbox extends Components.MyCheckbox {
129131
This event will not emit when programmatically setting the `checked` property.
130132
*/
131133
ionChange: EventEmitter<CustomEvent<IMyCheckboxCheckboxChangeEventDetail>>;
134+
/**
135+
* Same as `ionChange`, but with a nested object for the value.
136+
For demonstration purposes to be able to test ways to handle more complex events.
137+
*/
138+
ionChangeNested: EventEmitter<CustomEvent<IMyCheckboxCheckboxChangeNestedEventDetail>>;
132139
/**
133140
* Emitted when the checkbox has focus.
134141
*/

example-project/component-library-react/src/components.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as clientComponents from 'component-library-react';
1010

1111
import type { EventName, StencilReactComponent } from '@stencil/react-output-target/runtime';
1212
import { createComponent, type HydrateModule, type ReactWebComponent, type SerializeShadowRootOptions } from '@stencil/react-output-target/ssr';
13-
import { type CheckboxChangeEventDetail, type IMyComponent, type InputChangeEventDetail, type MyCheckboxCustomEvent, type MyComponentScopedCustomEvent, type MyInputCustomEvent, type MyInputScopedCustomEvent, type MyPopoverCustomEvent, type MyRadioGroupCustomEvent, type MyRangeCustomEvent, type OverlayEventDetail, type RadioGroupChangeEventDetail, type RangeChangeEventDetail } from "component-library";
13+
import { type CheckboxChangeEventDetail, type CheckboxChangeNestedEventDetail, type IMyComponent, type InputChangeEventDetail, type MyCheckboxCustomEvent, type MyComponentScopedCustomEvent, type MyInputCustomEvent, type MyInputScopedCustomEvent, type MyPopoverCustomEvent, type MyRadioGroupCustomEvent, type MyRangeCustomEvent, type OverlayEventDetail, type RadioGroupChangeEventDetail, type RangeChangeEventDetail } from "component-library";
1414
import { MyButtonScoped as MyButtonScopedElement } from "component-library/components/my-button-scoped.js";
1515
import { MyButton as MyButtonElement } from "component-library/components/my-button.js";
1616
import { MyCheckbox as MyCheckboxElement } from "component-library/components/my-checkbox.js";
@@ -90,6 +90,7 @@ export const MyButtonScoped: StencilReactComponent<MyButtonScopedElement, MyButt
9090

9191
export type MyCheckboxEvents = {
9292
onIonChange: EventName<MyCheckboxCustomEvent<CheckboxChangeEventDetail>>,
93+
onIonChangeNested: EventName<MyCheckboxCustomEvent<CheckboxChangeNestedEventDetail>>,
9394
onIonFocus: EventName<CustomEvent<void>>,
9495
onIonBlur: EventName<CustomEvent<void>>
9596
};

example-project/component-library-react/src/components.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type { EventName, StencilReactComponent } from '@stencil/react-output-target/runtime';
1111
import { createComponent } from '@stencil/react-output-target/runtime';
12-
import { type CheckboxChangeEventDetail, type IMyComponent, type InputChangeEventDetail, type MyCheckboxCustomEvent, type MyComponentScopedCustomEvent, type MyInputCustomEvent, type MyInputScopedCustomEvent, type MyPopoverCustomEvent, type MyRadioGroupCustomEvent, type MyRangeCustomEvent, type OverlayEventDetail, type RadioGroupChangeEventDetail, type RangeChangeEventDetail } from "component-library";
12+
import { type CheckboxChangeEventDetail, type CheckboxChangeNestedEventDetail, type IMyComponent, type InputChangeEventDetail, type MyCheckboxCustomEvent, type MyComponentScopedCustomEvent, type MyInputCustomEvent, type MyInputScopedCustomEvent, type MyPopoverCustomEvent, type MyRadioGroupCustomEvent, type MyRangeCustomEvent, type OverlayEventDetail, type RadioGroupChangeEventDetail, type RangeChangeEventDetail } from "component-library";
1313
import { MyButtonScoped as MyButtonScopedElement, defineCustomElement as defineMyButtonScoped } from "component-library/components/my-button-scoped.js";
1414
import { MyButton as MyButtonElement, defineCustomElement as defineMyButton } from "component-library/components/my-button.js";
1515
import { MyCheckbox as MyCheckboxElement, defineCustomElement as defineMyCheckbox } from "component-library/components/my-checkbox.js";
@@ -68,6 +68,7 @@ export const MyButtonScoped: StencilReactComponent<MyButtonScopedElement, MyButt
6868

6969
export type MyCheckboxEvents = {
7070
onIonChange: EventName<MyCheckboxCustomEvent<CheckboxChangeEventDetail>>,
71+
onIonChangeNested: EventName<MyCheckboxCustomEvent<CheckboxChangeNestedEventDetail>>,
7172
onIonFocus: EventName<CustomEvent<void>>,
7273
onIonBlur: EventName<CustomEvent<void>>
7374
};
@@ -79,6 +80,7 @@ export const MyCheckbox: StencilReactComponent<MyCheckboxElement, MyCheckboxEven
7980
react: React,
8081
events: {
8182
onIonChange: 'ionChange',
83+
onIonChangeNested: 'ionChangeNested',
8284
onIonFocus: 'ionFocus',
8385
onIonBlur: 'ionBlur'
8486
} as MyCheckboxEvents,

example-project/component-library-vue/src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,16 @@ export const MyCheckbox: StencilVueComponent<JSX.MyCheckbox, JSX.MyCheckbox["che
122122
'justify',
123123
'alignment',
124124
'ionChange',
125+
'ionChangeNested',
125126
'ionFocus',
126127
'ionBlur'
127128
], [
128129
'ionChange',
130+
'ionChangeNested',
129131
'ionFocus',
130132
'ionBlur'
131133
],
132-
'checked', 'myChange') : defineStencilSSRComponent<JSX.MyCheckbox, JSX.MyCheckbox["checked"]>({
134+
'checked', 'ionChangeNested', 'detail.nested.checked') : defineStencilSSRComponent<JSX.MyCheckbox, JSX.MyCheckbox["checked"]>({
133135
tagName: 'my-checkbox',
134136
hydrateModule: import('component-library/hydrate'),
135137
props: {
@@ -142,6 +144,7 @@ export const MyCheckbox: StencilVueComponent<JSX.MyCheckbox, JSX.MyCheckbox["che
142144
'justify': [String, "justify"],
143145
'alignment': [String, "alignment"],
144146
'onIonChange': [Function],
147+
'onIonChangeNested': [Function],
145148
'onIonFocus': [Function],
146149
'onIonBlur': [Function]
147150
}
@@ -264,7 +267,7 @@ export const MyInput: StencilVueComponent<JSX.MyInput, JSX.MyInput["value"]> = /
264267
'myBlur',
265268
'myFocus'
266269
],
267-
'value', 'myChange') : defineStencilSSRComponent<JSX.MyInput, JSX.MyInput["value"]>({
270+
'value', 'myChange', undefined) : defineStencilSSRComponent<JSX.MyInput, JSX.MyInput["value"]>({
268271
tagName: 'my-input',
269272
hydrateModule: import('component-library/hydrate'),
270273
props: {
@@ -488,7 +491,7 @@ export const MyRadioGroup: StencilVueComponent<JSX.MyRadioGroup, JSX.MyRadioGrou
488491
'myChange',
489492
'myValueChange'
490493
],
491-
'value', 'myChange') : defineStencilSSRComponent<JSX.MyRadioGroup, JSX.MyRadioGroup["value"]>({
494+
'value', 'myChange', undefined) : defineStencilSSRComponent<JSX.MyRadioGroup, JSX.MyRadioGroup["value"]>({
492495
tagName: 'my-radio-group',
493496
hydrateModule: import('component-library/hydrate'),
494497
props: {
@@ -524,7 +527,7 @@ export const MyRange: StencilVueComponent<JSX.MyRange, JSX.MyRange["value"]> = /
524527
'myFocus',
525528
'myBlur'
526529
],
527-
'value', 'myChange') : defineStencilSSRComponent<JSX.MyRange, JSX.MyRange["value"]>({
530+
'value', 'myChange', undefined) : defineStencilSSRComponent<JSX.MyRange, JSX.MyRange["value"]>({
528531
tagName: 'my-range',
529532
hydrateModule: import('component-library/hydrate'),
530533
props: {

example-project/component-library/src/components.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
88
import { AutocompleteTypes, Color, ComponentProps, ComponentRef, OverlayEventDetail, TextFieldTypes } from "./interfaces";
9-
import { CheckboxChangeEventDetail } from "./components/my-checkbox/my-checkbox";
9+
import { CheckboxChangeEventDetail, CheckboxChangeNestedEventDetail } from "./components/my-checkbox/my-checkbox";
1010
import { Baz, Foo, Grault, Quux, Waldo } from "./components/my-complex-props/my-complex-props";
1111
import { Baz as Baz1, Foo as Foo1, Grault as Grault1, Quux as Quux1, Waldo as Waldo1 } from "./components/my-complex-props-scoped/my-complex-props-scoped";
1212
import { IMyComponent } from "./components/helpers";
@@ -16,7 +16,7 @@ import { RadioGroupChangeEventDetail } from "./components/my-radio-group/my-radi
1616
import { Color as Color1, StyleEventDetail } from "./components/element-interface";
1717
import { RangeChangeEventDetail, RangeValue } from "./components/my-range/my-range";
1818
export { AutocompleteTypes, Color, ComponentProps, ComponentRef, OverlayEventDetail, TextFieldTypes } from "./interfaces";
19-
export { CheckboxChangeEventDetail } from "./components/my-checkbox/my-checkbox";
19+
export { CheckboxChangeEventDetail, CheckboxChangeNestedEventDetail } from "./components/my-checkbox/my-checkbox";
2020
export { Baz, Foo, Grault, Quux, Waldo } from "./components/my-complex-props/my-complex-props";
2121
export { Baz as Baz1, Foo as Foo1, Grault as Grault1, Quux as Quux1, Waldo as Waldo1 } from "./components/my-complex-props-scoped/my-complex-props-scoped";
2222
export { IMyComponent } from "./components/helpers";
@@ -824,6 +824,7 @@ declare global {
824824
};
825825
interface HTMLMyCheckboxElementEventMap {
826826
"ionChange": CheckboxChangeEventDetail;
827+
"ionChangeNested": CheckboxChangeNestedEventDetail;
827828
"ionFocus": void;
828829
"ionBlur": void;
829830
}
@@ -1259,6 +1260,10 @@ declare namespace LocalJSX {
12591260
* Emitted when the checked property has changed as a result of a user action such as a click. This event will not emit when programmatically setting the `checked` property.
12601261
*/
12611262
"onIonChange"?: (event: MyCheckboxCustomEvent<CheckboxChangeEventDetail>) => void;
1263+
/**
1264+
* Same as `ionChange`, but with a nested object for the value. For demonstration purposes to be able to test ways to handle more complex events.
1265+
*/
1266+
"onIonChangeNested"?: (event: MyCheckboxCustomEvent<CheckboxChangeNestedEventDetail>) => void;
12621267
/**
12631268
* Emitted when the checkbox has focus.
12641269
*/

example-project/component-library/src/components/my-checkbox/my-checkbox.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
2+
import { Component, Element, Event, h, Host, Method, Prop } from '@stencil/core';
33
import { createColorClasses, hasShadowDom, hostContext } from '../helpers';
44

55
export interface CheckboxChangeEventDetail<T = any> {
66
value: T;
77
checked: boolean;
88
}
99

10+
export interface CheckboxChangeNestedEventDetail<T = any> {
11+
nested: {
12+
value: T;
13+
checked: boolean;
14+
intermediate: boolean;
15+
};
16+
}
17+
1018
/**
1119
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
1220
*
@@ -100,6 +108,12 @@ export class Checkbox implements ComponentInterface {
100108
*/
101109
@Event() ionChange!: EventEmitter<CheckboxChangeEventDetail>;
102110

111+
/**
112+
* Same as `ionChange`, but with a nested object for the value.
113+
* For demonstration purposes to be able to test ways to handle more complex events.
114+
*/
115+
@Event() ionChangeNested: EventEmitter<CheckboxChangeNestedEventDetail>;
116+
103117
/**
104118
* Emitted when the checkbox has focus.
105119
*/
@@ -134,6 +148,14 @@ export class Checkbox implements ComponentInterface {
134148
checked: isChecked,
135149
value: this.value,
136150
});
151+
152+
this.ionChangeNested.emit({
153+
nested: {
154+
value: isChecked,
155+
checked: isChecked,
156+
intermediate: this.indeterminate,
157+
},
158+
});
137159
};
138160

139161
private toggleChecked = (ev: Event) => {

example-project/component-library/src/components/my-checkbox/readme.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323

2424
## Events
2525

26-
| Event | Description | Type |
27-
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
28-
| `ionBlur` | Emitted when the checkbox loses focus. | `CustomEvent<void>` |
29-
| `ionChange` | Emitted when the checked property has changed as a result of a user action such as a click. This event will not emit when programmatically setting the `checked` property. | `CustomEvent<CheckboxChangeEventDetail<any>>` |
30-
| `ionFocus` | Emitted when the checkbox has focus. | `CustomEvent<void>` |
26+
| Event | Description | Type |
27+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
28+
| `ionBlur` | Emitted when the checkbox loses focus. | `CustomEvent<void>` |
29+
| `ionChange` | Emitted when the checked property has changed as a result of a user action such as a click. This event will not emit when programmatically setting the `checked` property. | `CustomEvent<CheckboxChangeEventDetail<any>>` |
30+
| `ionChangeNested` | Same as `ionChange`, but with a nested object for the value. For demonstration purposes to be able to test ways to handle more complex events. | `CustomEvent<CheckboxChangeNestedEventDetail<any>>` |
31+
| `ionFocus` | Emitted when the checkbox has focus. | `CustomEvent<void>` |
3132

3233

3334
## Slots

example-project/component-library/stencil.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ const vueComponentModels: ComponentModelConfig[] = [
4545
},
4646
{
4747
elements: ['my-checkbox'],
48-
event: 'myChange',
48+
event: 'ionChangeNested',
4949
targetAttr: 'checked',
50+
eventAttr: 'detail.nested.checked'
5051
},
5152
{
5253
elements: ['my-range', 'my-radio-group'],
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script>
2+
import { defineComponent, ref } from 'vue';
3+
import { MyCheckbox } from 'component-library-vue';
4+
5+
export default defineComponent({
6+
name: 'Checkbox',
7+
components: {
8+
MyCheckbox,
9+
},
10+
setup() {
11+
const inputValue = ref(null);
12+
const changeEvent = ref(null);
13+
14+
const handleChange = (ev) => {
15+
changeEvent.value = ev.detail.nested.value;
16+
};
17+
18+
return {
19+
inputValue,
20+
changeEvent,
21+
handleChange,
22+
};
23+
},
24+
});
25+
</script>
26+
27+
<style scoped>
28+
.inputResultCheckbox {
29+
color: green;
30+
}
31+
</style>
32+
33+
<template>
34+
<div>
35+
<MyCheckbox v-model="inputValue" @ionChangeNested="handleChange" />
36+
<div class="inputResultCheckbox">
37+
<p>Input v-model: {{ inputValue }}</p>
38+
<p>Change Event: {{ changeEvent }}</p>
39+
</div>
40+
</div>
41+
</template>

example-project/vue-app/src/views/BasicComposition.vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
/>
1818
<p data-testid="mycomponent-click" v-show="isClicked">MyComponent was clicked</p>
1919
<Input />
20+
<Checkbox />
2021
<MyCheckbox @ionChange="console.log">Checkbox!!!</MyCheckbox>
2122
<hr />
2223
<my-input v-model="input" />
@@ -35,13 +36,17 @@ import { defineComponent, ref } from 'vue'
3536
import HelloWorld from '../components/HelloWorld.vue'
3637
// @ts-ignore
3738
import Input from '../components/Input.vue'
39+
// @ts-ignore
40+
import Checkbox from '../components/Checkbox.vue'
3841
import { MyComponent, MyCheckbox, MyInput, MyRadio, MyRadioGroup } from 'component-library-vue'
3942
43+
4044
export default defineComponent({
4145
name: 'BasicComposition',
4246
components: {
4347
HelloWorld,
4448
Input,
49+
Checkbox,
4550
MyComponent,
4651
MyCheckbox,
4752
MyInput,
@@ -64,10 +69,15 @@ export default defineComponent({
6469
isClicked,
6570
handleCustomEvent,
6671
kidsNames,
67-
radioGroupValue
68-
}
69-
}
70-
})
72+
radioGroupValue,
73+
};
74+
},
75+
data() {
76+
return {
77+
checked: false,
78+
};
79+
},
80+
});
7181
</script>
7282

7383
<style scoped>

0 commit comments

Comments
 (0)