Browse Source

fix(@vben/common-ui): bridge custom ApiComponent model props (#8268)

pull/8334/head
kilisamemarisaaa 4 weeks ago
parent
commit
5abd820578
  1. 5
      .changeset/calm-apes-select.md
  2. 131
      packages/effects/common-ui/src/components/api-component/__tests__/api-component.test.ts
  3. 34
      packages/effects/common-ui/src/components/api-component/api-component.vue

5
.changeset/calm-apes-select.md

@ -0,0 +1,5 @@
---
'@vben/common-ui': patch
---
fix: forward ApiComponent updates for custom model value props

131
packages/effects/common-ui/src/components/api-component/__tests__/api-component.test.ts

@ -0,0 +1,131 @@
import { mount } from '@vue/test-utils';
import { defineComponent, h, markRaw, nextTick, ref } from 'vue';
import { describe, expect, it, vi } from 'vitest';
import ApiComponent from '../api-component.vue';
const ValueInput = defineComponent({
name: 'ValueInput',
props: {
value: { type: String, default: undefined },
},
emits: ['update:value'],
setup(props, { emit }) {
return () =>
h(
'button',
{ onClick: () => emit('update:value', 'selected') },
props.value,
);
},
});
const ModelValueInput = defineComponent({
name: 'ModelValueInput',
props: {
modelValue: { type: String, default: undefined },
},
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(
'button',
{ onClick: () => emit('update:modelValue', 'selected') },
props.modelValue,
);
},
});
const KebabModelInput = defineComponent({
name: 'KebabModelInput',
props: {
modelValue: { type: String, default: undefined },
},
emits: ['update:modelValue'],
setup(props, { emit }) {
return () =>
h(
'button',
{ onClick: () => emit('update:modelValue', 'selected') },
props.modelValue,
);
},
});
describe('api-component.vue', () => {
it('bridges a custom model prop in both directions', async () => {
const outerValue = ref('initial');
const handleUpdate = vi.fn((value: string) => {
outerValue.value = value;
});
const Harness = defineComponent({
setup() {
return () =>
h(ApiComponent, {
component: markRaw(ValueInput),
modelPropName: 'value',
value: outerValue.value,
'onUpdate:value': handleUpdate,
});
},
});
const wrapper = mount(Harness);
const input = wrapper.findComponent(ValueInput);
expect(input.props('value')).toBe('initial');
await input.trigger('click');
await nextTick();
expect(handleUpdate).toHaveBeenCalledWith('selected');
expect(input.props('value')).toBe('selected');
outerValue.value = 'external';
await nextTick();
expect(input.props('value')).toBe('external');
});
it('preserves the default modelValue protocol', async () => {
const wrapper = mount(ApiComponent, {
props: {
component: markRaw(ModelValueInput),
modelValue: 'initial',
},
});
const input = wrapper.findComponent(ModelValueInput);
expect(input.props('modelValue')).toBe('initial');
await input.trigger('click');
await nextTick();
expect(wrapper.emitted('update:modelValue')).toEqual([['selected']]);
expect(input.props('modelValue')).toBe('selected');
});
it('bridges a kebab-case custom model prop', async () => {
const outerValue = ref('initial');
const handleUpdate = vi.fn((value: string) => {
outerValue.value = value;
});
const Harness = defineComponent({
setup() {
return () =>
h(ApiComponent, {
component: markRaw(KebabModelInput),
modelPropName: 'model-value',
'model-value': outerValue.value,
'onUpdate:model-value': handleUpdate,
});
},
});
const wrapper = mount(Harness);
const input = wrapper.findComponent(KebabModelInput);
expect(input.props('modelValue')).toBe('initial');
await input.trigger('click');
await nextTick();
expect(handleUpdate).toHaveBeenCalledWith('selected');
expect(input.props('modelValue')).toBe('selected');
});
});

34
packages/effects/common-ui/src/components/api-component/api-component.vue

@ -44,6 +44,14 @@ const emit = defineEmits<{
const modelValue = defineModel<any>({ default: undefined });
const attrs = useAttrs();
const usesDefaultModelValue = computed(() => {
return ['model-value', 'modelValue'].includes(props.modelPropName);
});
const currentModelValue = computed(() => {
return usesDefaultModelValue.value
? modelValue.value
: attrs[props.modelPropName];
});
const innerParams = ref({});
const refOptions = ref<OptionsItem[]>([]);
const loading = ref(false);
@ -89,13 +97,14 @@ const getOptions = computed(() => {
});
const bindProps = computed(() => {
const updateEvent = `onUpdate:${props.modelPropName}`;
return {
[props.modelPropName]: unref(modelValue),
[props.modelPropName]: unref(currentModelValue),
[props.optionsPropName]: unref(getOptions),
[`onUpdate:${props.modelPropName}`]: (val: string) => {
modelValue.value = val;
[updateEvent]: (val: string) => {
updateModelValue(val);
},
...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]),
...objectOmit(attrs, [props.modelPropName, updateEvent]),
...(props.visibleEvent
? {
[props.visibleEvent]: handleFetchForVisible,
@ -104,6 +113,17 @@ const bindProps = computed(() => {
};
});
function updateModelValue(value: any) {
if (usesDefaultModelValue.value) {
modelValue.value = value;
return;
}
const updateHandler = attrs[`onUpdate:${props.modelPropName}`];
if (isFunction(updateHandler)) {
updateHandler(value);
}
}
async function fetchApi() {
const { api, beforeFetch, shouldFetch, afterFetch, resultField } = props;
@ -192,7 +212,7 @@ watch(
function emitChange() {
if (
modelValue.value === undefined &&
currentModelValue.value === undefined &&
props.autoSelect &&
unref(getOptions).length > 0
) {
@ -218,7 +238,7 @@ function emitChange() {
}
}
if (firstOption) modelValue.value = firstOption.value;
if (firstOption) updateModelValue(firstOption.value);
}
emit('optionsChange', unref(getOptions));
}
@ -227,7 +247,7 @@ defineExpose({
/** 获取options数据 */
getOptions: () => unref(getOptions),
/** 获取当前值 */
getValue: () => unref(modelValue),
getValue: () => unref(currentModelValue),
/** 获取被包装的组件实例 */
getComponentRef: <T = any>() => componentRef.value as T,
/** 更新Api参数 */

Loading…
Cancel
Save