feat: add checkbox and checkbox-group component

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/581/head
Ryan Wang 3 years ago
parent d66ee9c391
commit 6d45b96e4e

@ -0,0 +1,56 @@
<script lang="ts" setup>
const props = defineProps({
checked: {
type: Boolean,
default: false,
},
value: {
type: [String, Number, Boolean],
},
label: {
type: String,
},
name: {
type: String,
},
});
const id = ["checkbox", props.name, props.value]
.filter((item) => !!item)
.join("-");
const emit = defineEmits(["update:checked", "change"]);
function handleChange(e: Event) {
const { checked } = e.target as HTMLInputElement;
emit("update:checked", checked);
emit("change", e);
}
</script>
<template>
<div
:class="{ 'checkbox-wrapper-checked': checked }"
class="checkbox-wrapper"
>
<div class="checkbox-inner">
<input
:id="id"
:checked="checked"
:value="value"
type="checkbox"
@change="handleChange"
/>
</div>
<label v-if="label" :for="id" class="checkbox-label">
{{ label }}
</label>
</div>
</template>
<style lang="scss">
.checkbox-wrapper {
@apply flex;
@apply items-center;
@apply box-border;
@apply flex-grow-0;
}
</style>

@ -0,0 +1,59 @@
<script lang="ts" setup>
import { VCheckbox } from "./index";
import type { PropType } from "vue";
const props = defineProps({
modelValue: {
type: Object as PropType<Array<string>>,
default: () => {
return [];
},
},
options: {
type: Object as PropType<Array<Record<string, string>>>,
},
valueKey: {
type: String,
default: "value",
},
labelKey: {
type: String,
default: "label",
},
name: {
type: String,
},
});
const emit = defineEmits(["update:modelValue", "change"]);
function handleChange(e: Event) {
const { value, checked } = e.target as HTMLInputElement;
const checkedValues = [...props.modelValue];
if (checked) {
checkedValues.push(value);
} else {
checkedValues.splice(checkedValues.indexOf(value), 1);
}
emit("update:modelValue", checkedValues);
emit("change", checkedValues);
}
</script>
<template>
<div class="checkbox-group-wrapper">
<VCheckbox
v-for="(option, index) in options"
:key="index"
:checked="modelValue.includes(option[valueKey])"
:label="option[labelKey]"
:name="name"
:value="option[valueKey]"
@change="handleChange"
/>
</div>
</template>
<style lang="scss">
.checkbox-group-wrapper {
}
</style>

@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { VCheckbox } from "../index";
import { mount } from "@vue/test-utils";
describe("CheckBox", () => {
it("should render", () => {
expect(VCheckbox).toBeDefined();
expect(mount(VCheckbox).html()).toMatchSnapshot();
});
it("should work with v-model:checked", async function () {
const wrapper = mount({
data() {
return {
checked: false,
};
},
template: `
<v-checkbox v-model:checked="checked"/>
`,
components: {
VCheckbox,
},
});
expect(wrapper.find("input").element.checked).toBe(false);
expect(wrapper.findComponent(VCheckbox).classes()).not.toContain(
"checkbox-wrapper-checked"
);
// change checked value
await wrapper.setData({ checked: true });
expect(wrapper.find("input").element.checked).toBe(true);
expect(wrapper.findComponent(VCheckbox).classes()).toContain(
"checkbox-wrapper-checked"
);
// click on checkbox
await wrapper.find("input").setValue(false);
expect(wrapper.vm.checked).toBe(false);
expect(wrapper.find("input").element.checked).toBe(false);
expect(wrapper.findComponent(VCheckbox).classes()).not.toContain(
"checkbox-wrapper-checked"
);
});
it("should work with label prop", async function () {
const wrapper = mount(VCheckbox, {
props: {
checked: true,
},
});
expect(wrapper.html()).not.toContain("label");
await wrapper.setProps({ label: "label" });
expect(wrapper.html()).toContain("label");
expect(wrapper.find("label").text()).toBe("label");
});
});

@ -0,0 +1,124 @@
import { describe, expect, it } from "vitest";
import { VCheckbox, VCheckboxGroup } from "../index";
import { mount } from "@vue/test-utils";
const options = [
{
value: "foo",
label: "foo",
},
{
value: "bar",
label: "bar",
},
];
describe("CheckBoxGroup", () => {
it("should render", () => {
expect(VCheckboxGroup).toBeDefined();
expect(
mount(VCheckboxGroup, {
props: {
options,
},
}).html()
).toMatchSnapshot();
});
it("should work with options prop", function () {
const wrapper = mount({
data() {
return {
options: options,
};
},
template: `
<v-checkbox-group :options="options" />
`,
components: {
VCheckboxGroup,
},
});
expect(wrapper.findAllComponents(VCheckbox).length).toBe(2);
expect(wrapper.findAllComponents(VCheckbox)[0].vm.$props.value).toBe("foo");
expect(wrapper.findAllComponents(VCheckbox)[0].vm.$props.label).toBe("foo");
expect(wrapper.findAllComponents(VCheckbox)[1].vm.$props.value).toBe("bar");
expect(wrapper.findAllComponents(VCheckbox)[1].vm.$props.label).toBe("bar");
});
it("should work with v-model", async function () {
const wrapper = mount({
data() {
return {
value: ["foo"],
options: options,
};
},
template: `
<v-checkbox-group v-model="value" :options="options" />
`,
components: {
VCheckboxGroup,
},
});
expect(wrapper.findAllComponents(VCheckbox)[0].classes()).toContain(
"checkbox-wrapper-checked"
);
expect(
wrapper.findAllComponents(VCheckbox)[0].find("input").element.checked
).toBe(true);
// mock click event
await wrapper.findAllComponents(VCheckbox)[1].find("input").setValue(true);
expect(wrapper.findAllComponents(VCheckbox)[1].classes()).toContain(
"checkbox-wrapper-checked"
);
expect(
wrapper.findAllComponents(VCheckbox)[1].find("input").element.checked
).toBe(true);
expect(wrapper.vm.value).toEqual(["foo", "bar"]);
});
it("should work with valueKey and labelKey props", async function () {
const wrapper = mount({
data() {
return {
value: ["foo"],
options: [
{
id: "foo",
name: "foo",
},
{
id: "bar",
name: "bar",
},
],
};
},
template: `
<v-checkbox-group v-model="value" :options="options" value-key="id" label-key="name" />
`,
components: {
VCheckboxGroup,
},
});
expect(
wrapper.findAllComponents(VCheckbox)[0].find("input").attributes("value")
).toBe("foo");
expect(
wrapper.findAllComponents(VCheckbox)[0].find(".checkbox-label").text()
).toBe("foo");
await wrapper.findAllComponents(VCheckbox)[1].find("input").setValue(true);
expect(wrapper.findAllComponents(VCheckbox)[1].classes()).toContain(
"checkbox-wrapper-checked"
);
expect(wrapper.vm.value).toEqual(["foo", "bar"]);
});
});

@ -0,0 +1,8 @@
// Vitest Snapshot v1
exports[`CheckBox > should render 1`] = `
"<div class=\\"checkbox-wrapper\\">
<div class=\\"checkbox-inner\\"><input id=\\"checkbox\\" type=\\"checkbox\\" value=\\"false\\"></div>
<!--v-if-->
</div>"
`;

@ -0,0 +1,12 @@
// Vitest Snapshot v1
exports[`CheckBoxGroup > should render 1`] = `
"<div class=\\"checkbox-group-wrapper\\">
<div class=\\"checkbox-wrapper\\">
<div class=\\"checkbox-inner\\"><input id=\\"checkbox-foo\\" type=\\"checkbox\\" value=\\"foo\\"></div><label for=\\"checkbox-foo\\" class=\\"checkbox-label\\">foo</label>
</div>
<div class=\\"checkbox-wrapper\\">
<div class=\\"checkbox-inner\\"><input id=\\"checkbox-bar\\" type=\\"checkbox\\" value=\\"bar\\"></div><label for=\\"checkbox-bar\\" class=\\"checkbox-label\\">bar</label>
</div>
</div>"
`;

@ -0,0 +1,2 @@
export { default as VCheckbox } from "./CheckBox.vue";
export { default as VCheckboxGroup } from "./CheckBoxGroup.vue";

@ -31,18 +31,18 @@ function handleChange(e: Event) {
} }
</script> </script>
<template> <template>
<div class="radio-wrapper" :class="{ 'radio-wrapper-checked': checked }"> <div :class="{ 'radio-wrapper-checked': checked }" class="radio-wrapper">
<div class="radio-inner"> <div class="radio-inner">
<input <input
type="radio"
:id="id" :id="id"
:value="value"
:checked="checked" :checked="checked"
:name="name" :name="name"
:value="value"
type="radio"
@change="handleChange" @change="handleChange"
/> />
</div> </div>
<label class="radio-label" :for="id" v-if="label"> <label v-if="label" :for="id" class="radio-label">
{{ label }} {{ label }}
</label> </label>
</div> </div>

@ -1,8 +1,8 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`Radio > should render 1`] = ` exports[`Radio > should render 1`] = `
"<div class=\\"radio-wrapper radio-wrapper-checked\\"> "<div class=\\"radio-wrapper-checked radio-wrapper\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio\\" value=\\"false\\"></div> <div class=\\"radio-inner\\"><input id=\\"radio\\" type=\\"radio\\" value=\\"false\\"></div>
<!--v-if--> <!--v-if-->
</div>" </div>"
`; `;

@ -3,10 +3,10 @@
exports[`RadioGroup > should render 1`] = ` exports[`RadioGroup > should render 1`] = `
"<div class=\\"radio-group-wrapper\\"> "<div class=\\"radio-group-wrapper\\">
<div class=\\"radio-wrapper\\"> <div class=\\"radio-wrapper\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio-foo\\" value=\\"foo\\"></div><label class=\\"radio-label\\" for=\\"radio-foo\\">foo</label> <div class=\\"radio-inner\\"><input id=\\"radio-foo\\" type=\\"radio\\" value=\\"foo\\"></div><label for=\\"radio-foo\\" class=\\"radio-label\\">foo</label>
</div> </div>
<div class=\\"radio-wrapper\\"> <div class=\\"radio-wrapper\\">
<div class=\\"radio-inner\\"><input type=\\"radio\\" id=\\"radio-bar\\" value=\\"bar\\"></div><label class=\\"radio-label\\" for=\\"radio-bar\\">bar</label> <div class=\\"radio-inner\\"><input id=\\"radio-bar\\" type=\\"radio\\" value=\\"bar\\"></div><label for=\\"radio-bar\\" class=\\"radio-label\\">bar</label>
</div> </div>
</div>" </div>"
`; `;

@ -90,7 +90,7 @@
<VTextarea v-model="inputValue" :rows="4" disabled /> <VTextarea v-model="inputValue" :rows="4" disabled />
</div> </div>
</section> </section>
<section class="box border-2 rounded p-2"> <section class="box border-2 rounded p-2 mb-3">
<h1 class="text-xl font-bold mb-2">Select</h1> <h1 class="text-xl font-bold mb-2">Select</h1>
<h2 class="mb-1">Size:</h2> <h2 class="mb-1">Size:</h2>
<div class="mb-3"> <div class="mb-3">
@ -117,7 +117,7 @@
</VSelect> </VSelect>
</div> </div>
</section> </section>
<section class="box border-2 rounded p-2"> <section class="box border-2 rounded p-2 mb-3">
<h1 class="text-xl font-bold mb-2">Radio</h1> <h1 class="text-xl font-bold mb-2">Radio</h1>
<h2 class="mb-1">Radio:</h2> <h2 class="mb-1">Radio:</h2>
<div class="mb-3"> <div class="mb-3">
@ -132,7 +132,25 @@
</div> </div>
<h2 class="mb-1">Radio Group:</h2> <h2 class="mb-1">Radio Group:</h2>
<div class="mb-3"> <div class="mb-3">
<VRadioGroup v-model="radioValue" :options="radioData"> </VRadioGroup> <VRadioGroup v-model="radioValue" :options="radioData"></VRadioGroup>
</div>
</section>
<section class="box border-2 rounded p-2 mb-3">
<h1 class="text-xl font-bold mb-2">CheckBox</h1>
<h2 class="mb-1">CheckBox:</h2>
<div class="mb-3">
<VCheckbox v-model:checked="checkboxValue" label="Apple" />
</div>
<h2 class="mb-1">CheckBox Group:</h2>
<div class="mb-3">
<VCheckboxGroup
v-model="checkboxGroupValue"
:options="checkboxData"
name="fruit"
/>
<span class="mr-1 text-gray-500">
Checked: {{ checkboxGroupValue }}
</span>
</div> </div>
</section> </section>
</div> </div>
@ -146,11 +164,14 @@ import { VInput } from "@/components/base/input";
import { VOption, VSelect } from "@/components/base/select"; import { VOption, VSelect } from "@/components/base/select";
import { VTextarea } from "@/components/base/textarea"; import { VTextarea } from "@/components/base/textarea";
import { VRadio, VRadioGroup } from "@/components/base/radio"; import { VRadio, VRadioGroup } from "@/components/base/radio";
import { VCheckbox, VCheckboxGroup } from "@/components/base/checkbox";
import { ref } from "vue"; import { ref } from "vue";
const inputValue = ref(); const inputValue = ref();
const selectValue = ref(); const selectValue = ref();
const radioValue = ref("apple"); const radioValue = ref("apple");
const checkboxValue = ref(false);
const checkboxGroupValue = ref(["apple"]);
const selectData = [ const selectData = [
{ {
@ -177,4 +198,19 @@ const radioData = [
label: "Orange", label: "Orange",
}, },
]; ];
const checkboxData = [
{
value: "apple",
label: "Apple",
},
{
value: "banana",
label: "Banana",
},
{
value: "orange",
label: "Orange",
},
];
</script> </script>

Loading…
Cancel
Save