LinkDesk/frontend/docs/checkbox-selection-refactor.md

4.4 KiB

Checkbox Selection Refactor

Overview

Refactored the AssetBrowser component to use a cleaner, more Vue-friendly approach for checkbox selection using object-based state instead of array-based state. This leverages the native v-model support in shadcn-vue Checkbox components (based on reka-ui).

Changes Made

1. Selection State Structure

Before:

const selectedAssets = ref<number[]>([]);

After:

const selectedAssets = ref<Record<number, boolean>>({});

2. Select All Checkbox

Before:

<Checkbox
  :checked="isAllSelected"
  @update:checked="toggleSelectAll"
/>

After:

<Checkbox v-model="selectAllChecked" />

With computed property (getter/setter pattern):

const selectAllChecked = computed({
  get: () => {
    return filteredAssets.value.length > 0 && 
           filteredAssets.value.every(asset => selectedAssets.value[asset.id]);
  },
  set: (checked: boolean) => {
    toggleSelectAll(checked);
  }
});

The computed property's setter is automatically called by v-model, eliminating the need for a separate event handler.

3. Row Checkboxes

Before:

<Checkbox
  :checked="selectedAssets.includes(asset.id)"
  @update:checked="(checked) => toggleAssetSelection(asset.id, checked)"
  @click.stop
/>

After:

<Checkbox
  v-model="selectedAssets[asset.id]"
  @click.stop
/>

4. Helper Methods

New method to get selected IDs:

const getSelectedAssetIds = () => {
  return Object.keys(selectedAssets.value)
    .filter(id => selectedAssets.value[Number(id)])
    .map(id => Number(id));
};

Simplified toggleSelectAll:

const toggleSelectAll = (checked: boolean) => {
  filteredAssets.value.forEach(asset => {
    selectedAssets.value[asset.id] = checked;
  });
};

5. Row Selection Logic

Updated to work with object-based state:

const handleRowClick = (asset: Asset, event: MouseEvent) => {
  if (event.ctrlKey || event.metaKey) {
    // Multi-select with Ctrl/Cmd - toggle selection
    selectedAssets.value[asset.id] = !selectedAssets.value[asset.id];
  } else if (event.shiftKey && getSelectedAssetIds().length > 0) {
    // Range select with Shift
    const selectedIds = getSelectedAssetIds();
    const lastSelectedId = selectedIds[selectedIds.length - 1];
    // ... range selection logic
  } else {
    // Single select
    selectAsset(asset);
  }
};

Benefits

  1. Direct v-model Binding: The shadcn-vue Checkbox (based on reka-ui) supports v-model natively when bound to a boolean value (reka-ui issue #1017)
  2. Simpler Code: No need for manual event handlers - v-model handles everything
  3. Better Performance: Direct property access (selectedAssets[id]) is faster than array operations (.includes(), .push(), .splice())
  4. More Reactive: Vue's reactivity system handles object property changes efficiently
  5. Cleaner API: The checkbox state is directly tied to the data structure
  6. No Redundant Handlers: Using v-model eliminates the need for separate event handlers

Testing

A test file (frontend/test-asset-selection.html) has been created to demonstrate the pattern working with plain Vue and native HTML checkboxes.

To test:

  1. Open frontend/test-asset-selection.html in a browser
  2. Try selecting individual checkboxes
  3. Try the "Select All" checkbox
  4. Try the action buttons (Select All, Deselect All, Select First 3)
  5. Verify the selection info updates correctly

Migration Notes

Any code that previously used selectedAssets.value as an array needs to be updated:

  • Use getSelectedAssetIds() to get an array of selected IDs
  • Use selectedAssets.value[id] to check if an asset is selected
  • Use selectedAssets.value = {} to clear all selections

Important Notes

v-model vs v-model:checked

According to reka-ui issue #1017, the correct syntax is:

  • Use v-model (not v-model:checked)
  • Works with boolean values directly
  • No need for :checked + @update:checked patterns

When to Use Event Handlers

When using Checkbox with props that need to emit to parent (like in ColumnVisibilityControl), you may still need @update:checked handlers to emit changes to the parent component, since props are not directly mutable. However, for local state management (like in AssetBrowser), pure v-model is sufficient.