Browse Source

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

pull/8343/head
俞可 · YU KE 4 weeks ago
committed by GitHub
parent
commit
4cac6d04cd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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 modelValue = defineModel<any>({ default: undefined });
const attrs = useAttrs(); 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 innerParams = ref({});
const refOptions = ref<OptionsItem[]>([]); const refOptions = ref<OptionsItem[]>([]);
const loading = ref(false); const loading = ref(false);
@ -89,13 +97,14 @@ const getOptions = computed(() => {
}); });
const bindProps = computed(() => { const bindProps = computed(() => {
const updateEvent = `onUpdate:${props.modelPropName}`;
return { return {
[props.modelPropName]: unref(modelValue), [props.modelPropName]: unref(currentModelValue),
[props.optionsPropName]: unref(getOptions), [props.optionsPropName]: unref(getOptions),
[`onUpdate:${props.modelPropName}`]: (val: string) => { [updateEvent]: (val: string) => {
modelValue.value = val; updateModelValue(val);
}, },
...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]), ...objectOmit(attrs, [props.modelPropName, updateEvent]),
...(props.visibleEvent ...(props.visibleEvent
? { ? {
[props.visibleEvent]: handleFetchForVisible, [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() { async function fetchApi() {
const { api, beforeFetch, shouldFetch, afterFetch, resultField } = props; const { api, beforeFetch, shouldFetch, afterFetch, resultField } = props;
@ -192,7 +212,7 @@ watch(
function emitChange() { function emitChange() {
if ( if (
modelValue.value === undefined && currentModelValue.value === undefined &&
props.autoSelect && props.autoSelect &&
unref(getOptions).length > 0 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)); emit('optionsChange', unref(getOptions));
} }
@ -227,7 +247,7 @@ defineExpose({
/** 获取options数据 */ /** 获取options数据 */
getOptions: () => unref(getOptions), getOptions: () => unref(getOptions),
/** 获取当前值 */ /** 获取当前值 */
getValue: () => unref(modelValue), getValue: () => unref(currentModelValue),
/** 获取被包装的组件实例 */ /** 获取被包装的组件实例 */
getComponentRef: <T = any>() => componentRef.value as T, getComponentRef: <T = any>() => componentRef.value as T,
/** 更新Api参数 */ /** 更新Api参数 */

Loading…
Cancel
Save