Skip to content

Commit 81d0c15

Browse files
fix(combinator): DLT-3171 update v-model bindings from rendered component events (#1299)
1 parent cf064f1 commit 81d0c15

2 files changed

Lines changed: 99 additions & 1 deletion

File tree

packages/combinator/src/components/combinator.test.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import DtcCombinator from './combinator.vue';
22

33
import { assert } from 'chai';
44
import { shallowMount } from '@vue/test-utils';
5+
import { nextTick } from 'vue';
56
import { getSupportedComponents } from '@/src/lib/test/utils_test';
6-
import documentation from '@/node_modules/@dialpad/dialtone-vue/dist/component-documentation.json';
7+
import { DtInput, DtToggle } from '@dialpad/dialtone-vue';
8+
import allDocs from '@/node_modules/@dialpad/dialtone-vue/dist/component-documentation.json';
9+
10+
const documentation = allDocs;
11+
const toggleDoc = allDocs.find(d => d.displayName === 'DtToggle');
12+
const inputDoc = allDocs.find(d => d.displayName === 'DtInput');
713

814
describe('combinator.vue test', function () {
915
const testComponents = getSupportedComponents();
@@ -33,4 +39,78 @@ describe('combinator.vue test', function () {
3339
});
3440
});
3541
});
42+
43+
describe('v-model writeback', function () {
44+
describe('boolean modelValue (DtToggle)', function () {
45+
beforeEach(function () {
46+
wrapper = shallowMount(DtcCombinator, {
47+
props: {
48+
component: DtToggle,
49+
documentation: toggleDoc,
50+
variants: {},
51+
},
52+
});
53+
});
54+
55+
afterEach(function () {
56+
wrapper.unmount();
57+
});
58+
59+
it('updates options.props when the renderer emits an update:modelValue event', async function () {
60+
const renderer = wrapper.findComponent({ name: 'DtcRenderer' });
61+
const initialValue = renderer.props('options').props.modelValue;
62+
63+
await renderer.vm.$emit('event', 'update:modelValue', !initialValue);
64+
await nextTick();
65+
66+
assert.strictEqual(renderer.props('options').props.modelValue, !initialValue);
67+
});
68+
69+
it('does not mutate options.props for non-update events (e.g. "change")', async function () {
70+
const renderer = wrapper.findComponent({ name: 'DtcRenderer' });
71+
const before = JSON.stringify(renderer.props('options').props);
72+
73+
await renderer.vm.$emit('event', 'change', 'something');
74+
await nextTick();
75+
76+
assert.strictEqual(JSON.stringify(renderer.props('options').props), before);
77+
});
78+
});
79+
80+
describe('string modelValue (DtInput)', function () {
81+
beforeEach(function () {
82+
wrapper = shallowMount(DtcCombinator, {
83+
props: {
84+
component: DtInput,
85+
documentation: inputDoc,
86+
variants: {},
87+
},
88+
});
89+
});
90+
91+
afterEach(function () {
92+
wrapper.unmount();
93+
});
94+
95+
it('updates options.props when the renderer emits an update:modelValue event', async function () {
96+
const renderer = wrapper.findComponent({ name: 'DtcRenderer' });
97+
98+
await renderer.vm.$emit('event', 'update:modelValue', 'hello');
99+
await nextTick();
100+
101+
assert.strictEqual(renderer.props('options').props.modelValue, 'hello');
102+
});
103+
104+
it('reflects subsequent updates (simulates typing character by character)', async function () {
105+
const renderer = wrapper.findComponent({ name: 'DtcRenderer' });
106+
107+
for (const value of ['h', 'he', 'hel', 'hell', 'hello']) {
108+
await renderer.vm.$emit('event', 'update:modelValue', value);
109+
await nextTick();
110+
}
111+
112+
assert.strictEqual(renderer.props('options').props.modelValue, 'hello');
113+
});
114+
});
115+
});
36116
});

packages/combinator/src/components/combinator.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
:options="options"
123123
:library="library"
124124
:disabled-members="disabledMembers"
125+
@event="onComponentEvent"
125126
/>
126127
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
127128
<div
@@ -354,6 +355,23 @@ const settings = computedModel(
354355
355356
watch(() => settings.value.root.theme, clearTokenCache);
356357
358+
/**
359+
* Handles events emitted by the rendered target component.
360+
* For v-model events (`update:<prop>`), writes the new value back into the
361+
* reactive options model so the preview and the generated code stay in sync.
362+
*
363+
* @param {string} name - The emitted event name (e.g. 'update:modelValue').
364+
* @param {*} value - The emitted value.
365+
*/
366+
function onComponentEvent (name, value) {
367+
if (!name?.startsWith('update:')) return;
368+
const prop = name.slice('update:'.length);
369+
options.value = (model) => {
370+
if (model.props && prop in model.props) model.props[prop] = value;
371+
else if (model.attributes && prop in model.attributes) model.attributes[prop] = value;
372+
};
373+
}
374+
357375
function updateVariant (e) {
358376
_presetChanging = true;
359377
selectedVariant.value = e;

0 commit comments

Comments
 (0)