Skip to content

Commit

Permalink
feat: checkbox group
Browse files Browse the repository at this point in the history
  • Loading branch information
daief committed Oct 18, 2022
1 parent 4a78185 commit 73949c8
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function render() {
- [x] toggle
- [x] tooltip - 🛠 refactor
- [x] form-checkbox
- [ ] checkbox-group
- [x] checkbox-group
- [ ] form-input
- [ ] form-radio
- [ ] form-range
Expand Down
27 changes: 26 additions & 1 deletion docs/src/pages/components/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,39 @@ Indeterminate(style only, do not affect the value)
Group

```tsx :::run
import { ref } from 'vue';

export default {
setup: () => {
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'pear', label: 'Pear' },
{ value: 'orange', label: 'Orange' },
];
return () => <dv-checkbox-group options={options} />;
const value = ref(['apple', 'pear']);
const disabled = ref(false);
const onChange = (array) => {
value.value = array;
};
return () => (
<div>
<div class="flex items-center">
<label class="mr-4">Disabled:</label>
<dv-toggle
checked={disabled.value}
onChange={(e) => (disabled.value = e.target.checked)}
/>
</div>
<dv-divider />
<dv-checkbox-group
size="lg"
options={options}
disabled={disabled.value}
value={value.value}
onChange={onChange}
/>
</div>
);
},
};
```
Expand Down
15 changes: 12 additions & 3 deletions docs/src/pages/demo.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Demo for development

```tsx :::run
import * as d from 'daisyui-vue';
import * as x from 'vue';
import { ref } from 'vue';

export default {
setup: () => {
Expand All @@ -11,7 +10,17 @@ export default {
{ value: 'pear', label: 'Pear' },
{ value: 'orange', label: 'Orange' },
];
return () => <dv-checkbox-group options={options} />;
const value = ref(['apple', 'pear']);
const onChange = (values) => {
value.value = values;
};
return () => (
<dv-checkbox-group
options={options}
value={value.value}
onChange={onChange}
/>
);
},
};
```
54 changes: 46 additions & 8 deletions src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import { InputChangeEvent } from 'daisyui-vue/@types/dom';
import { brandTypeProps, sizeProps } from 'daisyui-vue/shared/constants';
import { useCheckbox } from 'daisyui-vue/shared/hooks';
import { componentV2 } from 'daisyui-vue/shared/styled';
import { ExtractFromProps } from 'daisyui-vue/shared/types/common';
import { computed, HTMLAttributes, PropType } from 'vue';
import { ExtractFromProps, IText } from 'daisyui-vue/shared/types/common';
import {
computed,
ComputedRef,
HTMLAttributes,
inject,
PropType,
toRef,
watch,
} from 'vue';
import { checkboxCtxKey, ICheckboxContext } from './state';
import style from './style';

export const checkProps = {
Expand All @@ -24,7 +33,7 @@ export const checkProps = {
readOnly: Boolean,
indeterminate: Boolean,
value: {
type: [String, Number] as PropType<string | number>,
type: [String, Number] as PropType<IText>,
default: void 0,
},
name: String,
Expand All @@ -37,26 +46,55 @@ export const Checkbox = componentV2<ICheckProps, HTMLAttributes>(
name: 'Checkbox',
props: checkProps,
setup(props, { slots }) {
const { checked, handleOnChange } = useCheckbox(props);
const ctx = inject<ComputedRef<ICheckboxContext>>(checkboxCtxKey, null);

const size = computed(() => ctx?.value.size || props.size);
const disabled = computed(() => ctx?.value.disabled ?? props.disabled);

const { checked, handleOnChange } = useCheckbox({
checked: computed(
() => ctx?.value.value.includes(props.value) ?? props.checked,
),
defaultChecked: toRef(props, 'defaultChecked'),
onChange: computed(() => (e: InputChangeEvent) => {
ctx?.value.onChange(props.value);
props.onChange?.(e);
}),
});

const wrapperCls = computed(() => ({
'dv-checkbox-wrapper': true,
[`dv-checkbox-wrapper-${props.size}`]: props.size,
'dv-checkbox-wrapper-disabled': props.disabled,
[`dv-checkbox-wrapper-${size.value}`]: size.value,
'dv-checkbox-wrapper-disabled': disabled.value,
}));

const inputCls = computed(() => ({
'dv-checkbox': true,
[`dv-checkbox-${props.size}`]: props.size,
[`dv-checkbox-${size.value}`]: size.value,
[`dv-checkbox-${props.type}`]: props.type,
'dv-checkbox-indeterminate': props.indeterminate,
}));

watch(
() => props.value,
(newValue, _, onInvalidate) => {
ctx?.value.register(newValue);
onInvalidate(() => {
ctx?.value.cancel(newValue);
});
},
{
immediate: true,
flush: 'post',
},
);

return () => {
return (
<label class={wrapperCls.value}>
<input
type="checkbox"
disabled={props.disabled}
disabled={disabled.value}
checked={checked.value}
class={inputCls.value}
onChange={handleOnChange}
Expand Down
67 changes: 65 additions & 2 deletions src/components/checkbox/group.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import { brandTypeProps, sizeProps } from 'daisyui-vue/shared/constants';
import { componentV2 } from 'daisyui-vue/shared/styled';
import { ExtractFromProps } from 'daisyui-vue/shared/types/common';
import { ExtractFromProps, IText } from 'daisyui-vue/shared/types/common';
import { IOption } from 'daisyui-vue/shared/types/components';
import { HTMLAttributes, PropType } from 'vue';
import { computed, HTMLAttributes, PropType, provide, reactive } from 'vue';
import { Checkbox } from './checkbox';
import { checkboxCtxKey, ICheckboxContext } from './state';

export const checkGroupProps = {
...sizeProps,
...brandTypeProps,
disabled: Boolean,
options: {
type: Array as PropType<IOption[]>,
default: [],
},
value: {
type: Array as PropType<IText[]>,
default: void 0,
},
defaultValue: {
type: Array as PropType<IText[]>,
default: [],
},
onChange: {
type: Function as PropType<(values: IText[]) => void>,
},
};

export type ICheckGroupProps = ExtractFromProps<typeof checkGroupProps>;
Expand All @@ -17,6 +33,53 @@ export const CheckboxGroup = componentV2<ICheckGroupProps, HTMLAttributes>({
name: 'CheckboxGroup',
props: checkGroupProps,
setup: (props, { slots }) => {
const state = reactive({
value: props.value || props.defaultValue || [],
registeredValues: new Set<IText>(),
});

const finalValue = computed(() =>
Array.isArray(props.value) ? props.value : state.value,
);

const handleChange = (v: IText) => {
const optionIndex = finalValue.value.indexOf(v);
const newValue = finalValue.value.filter((it) =>
state.registeredValues.has(it),
);

if (optionIndex === -1) {
newValue.push(v);
} else {
newValue.splice(optionIndex, 1);
}

if (!Array.isArray(props.value)) {
state.value = newValue;
}

props.onChange?.(newValue);
};

const register = (v: IText) => {
state.registeredValues.add(v);
};

const cancel = (v: IText) => {
state.registeredValues.delete(v);
};

const ctxVal = computed<ICheckboxContext>(() => ({
size: props.size,
disabled: props.disabled,
value: finalValue.value,
onChange: handleChange,
register,
cancel,
}));

provide(checkboxCtxKey, ctxVal);

return () => (
<div class="dv-checkbox-group">
{props.options?.length
Expand Down
12 changes: 12 additions & 0 deletions src/components/checkbox/state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IBrandColor, ISize, IText } from 'daisyui-vue/shared/types/common';

export interface ICheckboxContext {
value: IText[];
size: ISize;
disabled: boolean;
onChange: (value: IText) => void;
register: (value: IText) => void;
cancel: (value: IText) => void;
}

export const checkboxCtxKey = Symbol('CheckboxGroup');
14 changes: 8 additions & 6 deletions src/shared/hooks/use-checkbox.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { InputChangeEvent } from 'daisyui-vue/@types/dom';
import { computed, nextTick, reactive } from 'vue';
import { isBool, isUndefined } from '../utils';
import { computed, nextTick, reactive, ToRefs } from 'vue';
import { isBool } from '../utils';

export interface IUseCheckboxOptions {
defaultChecked?: boolean;
checked?: boolean;
onChange?: (e: InputChangeEvent) => void;
}

export function useCheckbox(props: IUseCheckboxOptions) {
export function useCheckbox(props: ToRefs<IUseCheckboxOptions>) {
const state = reactive({
checked: isBool(props.checked) ? props.checked : !!props.defaultChecked,
checked: isBool(props.checked.value)
? props.checked.value
: !!props.defaultChecked.value,
});

const finalVal = computed(() =>
isBool(props.checked) ? props.checked : state.checked,
isBool(props.checked.value) ? props.checked.value : state.checked,
);

const handleOnChange = (e: InputChangeEvent) => {
const newVal = !finalVal.value;
state.checked = newVal;
props.onChange?.(e);
props.onChange.value?.(e);
nextTick(() => {
// force sync with finalVal
e.target.checked = finalVal.value;
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export interface IStyleProps {
}

export type IMaybeRef<T> = T | Ref<T>;

export type IText = string | number;
2 changes: 1 addition & 1 deletion src/shared/types/components.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface IOption {
value: any;
value: string | number;
label: any;
}
4 changes: 4 additions & 0 deletions src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function isString(v: any): v is string {
return typeof v === 'string';
}

export function isNil(v: any) {
return [null, void 0].includes(v);
}

/**
* 从 props、slots 中解析 render 方法的内容
* @param key
Expand Down

0 comments on commit 73949c8

Please sign in to comment.