Skip to content

Commit

Permalink
feat(VsForm): create vs-form component & form composable (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
smithoo authored Dec 19, 2023
1 parent c08943c commit cbabf95
Show file tree
Hide file tree
Showing 19 changed files with 690 additions and 13 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/vlossom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,8 @@
"vite": "^4.4.5",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.5"
},
"dependencies": {
"nanoid": "^5.0.4"
}
}
1 change: 1 addition & 0 deletions packages/vlossom/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './vs-button';
export * from './vs-container';
export * from './vs-divider';
export * from './vs-form';
export * from './vs-input';
export * from './vs-input-wrapper';
export * from './vs-message';
Expand Down
77 changes: 77 additions & 0 deletions packages/vlossom/src/components/vs-form/VsForm/VsForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<template>
<div class="vs-form">
<vs-container>
<slot />
</vs-container>
</div>
</template>

<script lang="ts">
import { computed, defineComponent, nextTick, provide, watch } from 'vue';
import { VsComponent, VsFormProvide } from '@/declaration/types';
import VsContainer from '@/components/vs-container/VsContainer/VsContainer.vue';
import { useFormProvide } from '@/composables';
export const name = VsComponent.VsForm;
const VsForm = defineComponent({
name: 'vs-form',
components: { VsContainer },
props: {
// v-model
changed: { type: Boolean, default: false },
valid: { type: Boolean, default: true },
},
emits: ['update:changed', 'update:valid', 'error'],
expose: ['validate', 'clear'],
setup(_, { emit }) {
const { labelObj, validObj, changedObj, validateFlag, clearFlag, getFormProvide } = useFormProvide();
provide<VsFormProvide>('vs-form', getFormProvide());
const isValid = computed(() => Object.values(validObj.value).every((v) => v));
const isChanged = computed(() => Object.values(changedObj.value).some((v) => v));
async function validate() {
validateFlag.value = !validateFlag.value;
await nextTick();
if (!isValid.value) {
// on error callback with invalid labels
const invalidIds = Object.keys(validObj.value).filter((id) => !validObj.value[id]);
const invalidLabels = invalidIds.map((id) => labelObj.value[id]);
emit('error', invalidLabels);
}
return isValid.value;
}
function clear() {
clearFlag.value = !clearFlag.value;
}
watch(isValid, (valid) => {
emit('update:valid', valid);
});
watch(isChanged, (changed) => {
emit('update:changed', changed);
});
return {
labelObj,
changedObj,
validObj,
validateFlag,
clearFlag,
isValid,
isChanged,
validate,
clear,
};
},
});
export default VsForm;
export type VsFormInstance = InstanceType<typeof VsForm>;
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { beforeEach, afterEach, describe, it, expect } from 'vitest';
import VsForm from './../VsForm.vue';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';

function mountComponent() {
return mount(VsForm);
}

describe('vs-form', () => {
let wrapper: ReturnType<typeof mountComponent>;

beforeEach(() => {
wrapper = mount(VsForm, {
props: {
changed: false,
'onUpdate:changed': (v: boolean) => wrapper.setProps({ changed: v }),
valid: false,
'onUpdate:valid': (v: boolean) => wrapper.setProps({ valid: v }),
},
});
});

afterEach(() => {
wrapper.unmount();
});

describe('validate', () => {
it('validate 함수를 실행하면, validateFlag가 바뀐다', async () => {
// when
const valid = await wrapper.vm.validate();

// then
expect(wrapper.vm.validateFlag).toBe(true);
expect(valid).toBe(true);
});

it('valid 여부가 바뀌면 update:valid event가 emit된다', async () => {
// when
wrapper.vm.validObj = {
test: false,
};
await nextTick();

// then
expect(wrapper.emitted('update:valid')?.[0][0]).toBe(false);
});

it('유효하지 않은 input의 label을 error 이벤트로 emit한다', async () => {
// given
wrapper.vm.validObj = {
test: false,
};
wrapper.vm.labelObj = {
test: 'test',
};

// when
const valid = await wrapper.vm.validate();

// then
expect(valid).toBe(false);
expect(wrapper.emitted('error')?.[0][0]).toEqual(['test']);
});
});

describe('changed', () => {
it('변경이 있으면 update:changed event가 emit된다', async () => {
// when
wrapper.vm.changedObj = {
test: true,
};
await nextTick();

// then
expect(wrapper.emitted('update:changed')?.[0][0]).toBe(true);
});
});

describe('clear', () => {
it('clear 함수를 실행하면, clearFlag가 바뀐다', async () => {
// when
await wrapper.vm.clear();

// then
expect(wrapper.vm.clearFlag).toBe(true);
});
});
});
9 changes: 9 additions & 0 deletions packages/vlossom/src/components/vs-form/VsForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { App } from 'vue';
import VsForm from './VsForm.vue';

export default {
install(app: App<Element>) {
app.component('vs-form', VsForm);
},
};
export * from './VsForm.vue';
2 changes: 2 additions & 0 deletions packages/vlossom/src/components/vs-form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as VsForm } from './VsForm';
export type { VsFormInstance } from './VsForm';
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import { useFormProvide } from '@/composables';

describe('form-provide-composable', () => {
it('labelObj에 label 값이 업데이트 된다', () => {
// given
const { labelObj, updateLabel } = useFormProvide();
const id = 'test';
const label = 'Test label';

// when
updateLabel(id, label);

// then
expect(labelObj.value[id]).toBe(label);
});

it('changedObj에 changed 값이 업데이트 된다', () => {
// given
const { changedObj, updateChanged } = useFormProvide();
const id = 'test';
const changed = true;

// when
updateChanged(id, changed);

// then
expect(changedObj.value[id]).toBe(changed);
});

it('validObj에 valid 값이 업데이트 된다', () => {
// given
const { validObj, updateValid } = useFormProvide();
const id = 'test';
const valid = true;

// when
updateValid(id, valid);

// then
expect(validObj.value[id]).toBe(valid);
});

it('form에서 제거된다', () => {
// given
const { labelObj, changedObj, validObj, removeFromForm } = useFormProvide();
const id = 'test';
labelObj.value[id] = 'test';
changedObj.value[id] = true;
validObj.value[id] = true;

// when
removeFromForm(id);

// then
expect(labelObj.value[id]).toBeUndefined();
expect(changedObj.value[id]).toBeUndefined();
expect(validObj.value[id]).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { getInputProps, useInput } from '@/composables/input-composable';
import { StateMessage, UIState } from '@/declaration/types';

describe('input composable', () => {
const inputValue = ref('');
let onMountedSpy = vi.fn();
let onChangeSpy = vi.fn();
const inputValue = ref('');
let onClearSpy = vi.fn(() => {
inputValue.value = '';
});

const InputComponent = defineComponent({
render: () => null,
Expand All @@ -16,13 +19,14 @@ describe('input composable', () => {
...getInputProps<string>(),
},
setup(props, ctx) {
const { modelValue, messages, rules } = toRefs(props);
const { modelValue, label, messages, rules } = toRefs(props);

return {
...useInput(inputValue, modelValue, ctx, {
...useInput(inputValue, modelValue, ctx, label, {
callbacks: {
onMounted: onMountedSpy,
onChange: onChangeSpy,
onClear: onClearSpy,
},
messages,
rules,
Expand All @@ -35,6 +39,9 @@ describe('input composable', () => {
inputValue.value = '';
onMountedSpy = vi.fn();
onChangeSpy = vi.fn();
onClearSpy = vi.fn(() => {
inputValue.value = '';
});
});

afterEach(() => {
Expand Down Expand Up @@ -386,4 +393,29 @@ describe('input composable', () => {
});
});
});

describe('clear', () => {
it('clear 함수를 호출하면 onClear callback이 실행된다', async () => {
// given
const wrapper = mount(InputComponent, {
props: {
modelValue: '',
'onUpdate:modelValue': (v: string) => wrapper.setProps({ modelValue: v }),
},
});

// when
await nextTick();
inputValue.value = 'test';
await nextTick();
wrapper.vm.clear();
await nextTick();

// then
expect(wrapper.vm.changed).toBe(false);
expect(wrapper.vm.modelValue).toBe('');
expect(inputValue.value).toBe('');
expect(onClearSpy).toHaveBeenCalledTimes(1);
});
});
});
Loading

0 comments on commit cbabf95

Please sign in to comment.