radix-vue / shadcn-vue

Vue port of shadcn-ui
https://www.shadcn-vue.com/
MIT License
4.46k stars 255 forks source link

[Bug]: Checkboxes don't work with Array #759

Open sugoidesune opened 1 week ago

sugoidesune commented 1 week ago

Reproduction

https://stackblitz.com/edit/nce3t7-yo8fpa?file=src%2FApp.vue

Describe the bug

Checkbox forces true/false value instead of adding/removing from array. This behavior persists with v-model and v-model:checked.

Connected: https://github.com/radix-vue/shadcn-vue/issues/622

replicate:

<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox';
import { ref } from 'vue';
const checkedNames = ref([]);
</script>

<template>
  <div class="flex items-center space-x-2">
    <div>Checked names: {{ checkedNames }}</div>
    <br />

    <Checkbox
      type="checkbox"
      id="jack"
      value="Jack"
      v-model:checked="checkedNames"
    />
    <label for="jack">Jack</label>

    <Checkbox
      type="checkbox"
      id="john"
      value="John"
      v-model:checked="checkedNames"
    />
    <label for="john">John</label>

    <Checkbox
      type="checkbox"
      id="mike"
      value="Mike"
      v-model:checked="checkedNames"
    />
    <label for="mike">Mike</label>
  </div>
</template>

System Info

windows 11

Contributes

sugoidesune commented 1 week ago

I asked AI to fix the component and now it works using v-model and arrays.

<script setup lang="ts">
import { type HTMLAttributes, computed } from "vue";
import type { CheckboxRootEmits, CheckboxRootProps } from "radix-vue";
import {
  CheckboxIndicator,
  CheckboxRoot,
  useForwardPropsEmits,
} from "radix-vue";
import { CheckIcon } from "@radix-icons/vue";
import { cn } from "@/lib/utils";

const props = defineProps<
  CheckboxRootProps & { class?: HTMLAttributes["class"] } & {
    modelValue?: boolean | any[];
    value?: any;
  }
>();

const emits = defineEmits<
  CheckboxRootEmits & { "update:modelValue": [value: boolean | any[]] }
>();

const isChecked = computed(() => {
  if (Array.isArray(props.modelValue)) {
    return props.modelValue.includes(props.value);
  }
  return props.modelValue;
});

const toggleCheckbox = (checked: boolean) => {
  if (Array.isArray(props.modelValue)) {
    const newValue = checked
      ? [...props.modelValue, props.value]
      : props.modelValue.filter((v) => v !== props.value);
    emits("update:modelValue", newValue);
  } else {
    emits("update:modelValue", checked);
  }
};

const delegatedProps = computed(() => {
  const { class: _, modelValue: __, value: ___, ...delegated } = props;
  return delegated;
});

const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>

<template>
  <CheckboxRoot
    v-bind="forwarded"
    :checked="isChecked"
    @update:checked="toggleCheckbox"
    :class="
      cn(
        'peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
        props.class
      )
    "
  >
    <CheckboxIndicator
      class="flex h-full w-full items-center justify-center text-current"
    >
      <slot>
        <CheckIcon class="h-4 w-4" />
      </slot>
    </CheckboxIndicator>
  </CheckboxRoot>
</template>