Skip to content

Commit 092bb47

Browse files
committed
feat(web-core): create useCollection composable
1 parent 54e0092 commit 092bb47

File tree

5 files changed

+425
-0
lines changed

5 files changed

+425
-0
lines changed
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# `useCollection` composable
2+
3+
The `useCollection` composable helps you manage a collection of items with flags (boolean states) and computed properties. It provides a type-safe way to filter, flag, and manipulate items in a collection.
4+
5+
## Usage
6+
7+
```typescript
8+
const { items, useSubset, useFlag } = useCollection(sources, {
9+
identifier: source => source.id,
10+
flags: 'selected',
11+
properties: source => ({
12+
isAvailable: computed(() => source.status === 'available'),
13+
fullName: computed(() => `${source.firstName} ${source.lastName}`),
14+
}),
15+
})
16+
```
17+
18+
## Core Concepts
19+
20+
- **Collection Item**: An object with a unique identifier, a reference to its source object, flags, computed properties, and methods to manipulate flags
21+
- **Flags**: Boolean states attached to items (like 'selected', 'active', 'highlighted')
22+
- **Properties**: Computed values derived from the source object
23+
24+
## `useCollection` parameters
25+
26+
| Name | Type | Required | Description |
27+
| --------- | --------------------------------- | :------: | ---------------------------------------------------- |
28+
| `sources` | `MaybeRefOrGetter<TSource[]>` || Array of source objects for the collection |
29+
| `options` | `CollectionOptions<TSource, TId>` || Configuration options for the collection (see below) |
30+
31+
### `options` object
32+
33+
| Name | Type | Required | Description |
34+
| ------------ | -------------------------------------------------- | :------: | ----------------------------------------------------------------------- |
35+
| `identifier` | `(source: TSource) => TId` || Function to extract a unique identifier from each source |
36+
| `flags` | `MaybeArray<string \| Record<string, FlagConfig>>` | | Flags that can be applied to items in the collection |
37+
| `properties` | `(source: TSource) => Record<string, ComputedRef>` | | Function that returns computed properties for each item |
38+
| `context` | `Reactive<{ flags, registeredFlags }>` | | Shared context for multiple collections (usually handled automatically) |
39+
40+
### `FlagConfig` object
41+
42+
| Name | Type | Default | Description |
43+
| ---------- | --------- | ------- | ------------------------------------------------------------------------ |
44+
| `multiple` | `boolean` | `true` | Whether multiple items can have this flag set (false = single selection) |
45+
| `default` | `boolean` | `false` | Default value for the flag when not explicitly set |
46+
47+
## Return Value
48+
49+
| Name | Type | Description |
50+
| ----------- | ------------------------------------------- | ----------------------------------------------------------------------------- |
51+
| `items` | `ComputedRef<CollectionItem[]>` | Array of collection items with flags and properties |
52+
| `useSubset` | `(filter: (item) => boolean) => Collection` | Creates a new collection that's a subset of the original, with shared context |
53+
| `useFlag` | `(flag: TFlag) => FlagGroup` | Utilities for working with a specific flag |
54+
55+
### `CollectionItem` object
56+
57+
| Name | Type | Description |
58+
| ------------ | ------------------------------ | ------------------------------------------------------- |
59+
| `id` | `TId` | Unique identifier for the item |
60+
| `source` | `TSource` | The original source object |
61+
| `flags` | `Record<TFlag, boolean>` | Object containing the state of all flags for this item |
62+
| `properties` | `TProperties` | Object containing all computed properties for this item |
63+
| `toggleFlag` | `(flag, forcedValue?) => void` | Method to toggle a flag on this item |
64+
65+
### Return value of `useFlag`
66+
67+
| Name | Type | Description |
68+
| ----------- | ------------------------------- | ------------------------------------------------------ |
69+
| `items` | `ComputedRef<CollectionItem[]>` | Array of items that have this flag set |
70+
| `ids` | `ComputedRef<TId[]>` | Array of IDs of items that have this flag set |
71+
| `count` | `ComputedRef<number>` | Number of items that have this flag set |
72+
| `areAllOn` | `ComputedRef<boolean>` | Whether all items in the collection have this flag set |
73+
| `areSomeOn` | `ComputedRef<boolean>` | Whether at least one item has this flag set |
74+
| `areNoneOn` | `ComputedRef<boolean>` | Whether no items have this flag set |
75+
| `toggle` | `(forcedValue?) => void` | Toggle this flag on all items in the collection |
76+
77+
## Examples
78+
79+
### Basic Usage
80+
81+
```typescript
82+
// Source type
83+
interface User {
84+
id: string
85+
firstName: string
86+
lastName: string
87+
status: 'active' | 'inactive'
88+
role: 'admin' | 'user'
89+
}
90+
91+
// Source data
92+
const users = ref<User[]>([
93+
{
94+
id: '1',
95+
firstName: 'John',
96+
lastName: 'Doe',
97+
status: 'active',
98+
role: 'admin',
99+
},
100+
{
101+
id: '2',
102+
firstName: 'Jane',
103+
lastName: 'Smith',
104+
status: 'active',
105+
role: 'user',
106+
},
107+
{
108+
id: '3',
109+
firstName: 'Bob',
110+
lastName: 'Johnson',
111+
status: 'inactive',
112+
role: 'user',
113+
},
114+
])
115+
116+
// Create a collection
117+
const { items: userItems, useFlag } = useCollection(users, {
118+
identifier: user => user.id,
119+
flags: ['selected', 'active'],
120+
properties: user => ({
121+
fullName: computed(() => `${user.firstName} ${user.lastName}`),
122+
isAdmin: computed(() => user.role === 'admin'),
123+
}),
124+
})
125+
126+
// Work with a specific flag
127+
const { areAllOn: areAllUsersSelected, count: selectedUsersCount, toggle: toggleAllSelectedUsers } = useFlag('selected')
128+
```
129+
130+
```vue
131+
<!-- Basic usage in a template -->
132+
<template>
133+
<div>
134+
<div>Selected: {{ selectedUsersCount }}</div>
135+
<div>All selected: {{ areAllUsersSelected }}</div>
136+
<button @click="toggleAllSelectedUsers()">Toggle All</button>
137+
138+
<ul>
139+
<li v-for="user in userItems" :key="user.id">
140+
<input type="checkbox" v-model="user.flags.selected" />
141+
{{ user.properties.fullName }}
142+
<span v-if="user.properties.isAdmin">(Admin)</span>
143+
</li>
144+
</ul>
145+
</div>
146+
</template>
147+
```
148+
149+
### Using Subsets
150+
151+
```typescript
152+
const { items: userItems, useSubset } = useCollection(users, {
153+
identifier: user => user.id,
154+
flags: ['selected'],
155+
properties: user => ({
156+
isActive: computed(() => user.status === 'active'),
157+
}),
158+
})
159+
160+
// Create a subset of only active users
161+
const { items: activeUserItems, useFlag: useActiveUsersFlag } = useSubset(item => item.properties.isActive)
162+
163+
// Work with a specific flag on the subset
164+
const { count: selectedActiveUsersCount } = useActiveUsersFlag('selected')
165+
166+
// Now you can work with just the active users
167+
console.log(`${activeUserItems.value.length} active users`)
168+
console.log(`${selectedActiveUsersCount.value} selected active users`)
169+
```
170+
171+
### Exclusive Selection (Radio Button Behavior)
172+
173+
```typescript
174+
const { items } = useCollection(users, {
175+
identifier: user => user.id,
176+
flags: {
177+
current: { multiple: false },
178+
},
179+
})
180+
```
181+
182+
When an item activate its `current` flag, it is deactivated on every other items
183+
184+
```vue
185+
<template>
186+
<MyComponent v-for="item in items" :key="item.id" :current="item.flags.current" @click="items.flags.current = true">
187+
{{ item.source.firstName }}
188+
</MyComponent>
189+
</template>
190+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { CollectionItem } from '@core/packages/collection/types.ts'
2+
import { useMemoize } from '@vueuse/core'
3+
import type { ComputedRef } from 'vue'
4+
5+
export function createItemBuilder<
6+
TSource,
7+
TId extends string,
8+
TFlag extends string,
9+
TProperties extends Record<string, any>,
10+
>({
11+
hasItemFlag,
12+
toggleItemFlag,
13+
propertiesOption,
14+
}: {
15+
hasItemFlag: (id: TId, flag: TFlag) => boolean
16+
toggleItemFlag: (id: TId, flag: TFlag, value?: boolean) => void
17+
propertiesOption: ((source: TSource) => Record<string, ComputedRef>) | undefined
18+
}) {
19+
return useMemoize((id: TId, source: TSource): CollectionItem<TSource, TId, TFlag, TProperties> => {
20+
const properties = propertiesOption?.(source)
21+
22+
return {
23+
id,
24+
source,
25+
toggleFlag(flag: TFlag, forcedValue?: boolean) {
26+
toggleItemFlag(id, flag, forcedValue)
27+
},
28+
flags: new Proxy({} as Record<TFlag, boolean>, {
29+
has: () => true,
30+
get: (_, flag: TFlag) => hasItemFlag(id, flag),
31+
set: (_, flag: TFlag, value: boolean) => {
32+
toggleItemFlag(id, flag, value)
33+
34+
return true
35+
},
36+
}),
37+
properties: new Proxy({} as TProperties, {
38+
has: (_, property) => {
39+
return properties !== undefined ? property in properties : false
40+
},
41+
get: (_, property) => {
42+
return properties?.[property as string]?.value
43+
},
44+
}),
45+
}
46+
})
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { FlagConfig } from '@core/packages/collection/types.ts'
2+
import type { MaybeArray } from '@core/types/utility.type.ts'
3+
import { toArray } from '@core/utils/to-array.utils.ts'
4+
import { toValue } from 'vue'
5+
6+
export function parseFlagsOption(_flags: MaybeArray<string | Record<string, FlagConfig>> | undefined) {
7+
const flagsMap = new Map<string, FlagConfig>()
8+
const flags = toArray(toValue(_flags))
9+
10+
for (const flag of flags) {
11+
if (typeof flag === 'string') {
12+
flagsMap.set(flag, {})
13+
} else {
14+
Object.entries<FlagConfig>(flag).forEach(([flag, config]) => {
15+
flagsMap.set(flag, config)
16+
})
17+
}
18+
}
19+
20+
return flagsMap
21+
}
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { useCollection } from '@core/packages/collection/use-collection.ts'
2+
import type { MaybeArray } from '@core/types/utility.type.ts'
3+
import type { ComputedRef, Reactive, UnwrapRef } from 'vue'
4+
5+
export type FlagConfig = {
6+
multiple?: boolean
7+
default?: boolean
8+
}
9+
10+
export type CollectionOptions<TSource, TId extends string> = {
11+
identifier: (source: TSource) => TId
12+
flags?: MaybeArray<string | Record<string, FlagConfig>>
13+
properties?: (source: TSource) => Record<string, ComputedRef>
14+
context?: Reactive<{
15+
flags: ComputedRef<Map<string, FlagConfig>>
16+
registeredFlags: Map<string, Map<TId, boolean>>
17+
}>
18+
}
19+
20+
export type ExtractFlags<TOptions extends CollectionOptions<any, any>> = TOptions['flags'] extends (infer U)[]
21+
? U extends string
22+
? U
23+
: U extends Record<string, FlagConfig>
24+
? keyof U
25+
: never
26+
: TOptions['flags'] extends string
27+
? TOptions['flags']
28+
: never
29+
30+
export type ExtractProperties<TOptions extends CollectionOptions<any, any>> = TOptions['properties'] extends (
31+
source: any
32+
) => infer TProps
33+
? {
34+
[K in keyof TProps]: UnwrapRef<TProps[K]>
35+
}
36+
: never
37+
38+
export type CollectionItem<TSource, TId, TFlag extends string, TProperties extends Record<string, any>> = {
39+
readonly id: TId
40+
readonly source: TSource
41+
readonly flags: Record<TFlag, boolean>
42+
readonly properties: TProperties
43+
toggleFlag(flag: TFlag, forcedValue?: boolean): void
44+
}
45+
46+
export type Collection<
47+
TSource,
48+
TId extends string,
49+
TFlag extends string,
50+
TProperties extends Record<string, any>,
51+
> = ReturnType<typeof useCollection<TSource, TId, any, TFlag, TProperties>>

0 commit comments

Comments
 (0)