vueform-plugin-checkbox-select-all 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +22 -0
  2. package/readme.md +238 -0
  3. package/src/index.js +144 -0
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "vueform-plugin-checkbox-select-all",
3
+ "version": "1.0.0",
4
+ "description": "Bi-directional Select All logic for Vueform's Checkbox and Checkboxgroup elements",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "peerDependencies": {
11
+ "vue": "^3.0.0",
12
+ "@vueform/vueform": "^1.0.0"
13
+ },
14
+ "keywords": [
15
+ "vueform",
16
+ "plugin",
17
+ "checkbox",
18
+ "select-all"
19
+ ],
20
+ "author": "Mohieb Al-Hesan",
21
+ "license": "MIT"
22
+ }
package/readme.md ADDED
@@ -0,0 +1,238 @@
1
+ # Vueform Select All Checkboxes Plugin
2
+
3
+ This plugin enables complex "Select All" logic for Vueform. It handles bi-directional synchronization (Parent $\leftrightarrow$ Child) and prevents infinite loops using an internal locking mechanism. It supports standard checkboxes, checkbox groups, and nested structures.
4
+
5
+ ## 📦 Installation
6
+
7
+ ### 1. Create the Plugin File
8
+
9
+ Create a file named `CheckboxSelectAll.js` inside your plugins directory (e.g., `./vueform/plugins/CheckboxSelectAll.js`) and paste the plugin code into it.
10
+
11
+ ### 2. Register in `vueform.config.js`
12
+
13
+ Import the plugin and add it to the `plugins` array in your Vueform configuration file.
14
+
15
+ ```javascript
16
+ // vueform.config.js
17
+ import en from "@vueform/vueform/locales/en";
18
+ import vueform from "@vueform/vueform/dist/vueform";
19
+ import { defineConfig } from "@vueform/vueform";
20
+ import "@vueform/vueform/dist/vueform.css";
21
+
22
+ // 1. Import the custom plugin
23
+ import CheckboxSelectAll from "./vueform/plugins/CheckboxSelectAll";
24
+
25
+ export default defineConfig({
26
+ theme: vueform,
27
+ locales: { en },
28
+ locale: "en",
29
+
30
+ // 2. Add it to the plugins array
31
+ plugins: [CheckboxSelectAll],
32
+ });
33
+ ```
34
+
35
+ ## ⚙️ Configuration & Concepts
36
+
37
+ To set up the logic, you only need to configure three properties in your schema.
38
+
39
+ ### A. Downstream (Parent $\rightarrow$ Children)
40
+
41
+ **Used on the Parent (Controller).**
42
+ When the parent is clicked, it forces all defined children to match its state.
43
+
44
+ - **Prop:** `controls`
45
+ - **Type:** `Array<String>`
46
+ - **Value:** A list of **full paths** to the child elements.
47
+
48
+ ### B. Upstream (Child $\rightarrow$ Parent)
49
+
50
+ **Used on the Child (Controlled).**
51
+ When a child is changed, it checks if all its siblings are selected. If yes, it checks the parent. If no, it unchecks the parent.
52
+
53
+ - **Prop:** `controller`
54
+ - **Type:** `String`
55
+ - **Value:** The **full path** to the parent element.
56
+
57
+ ### C. Internal Group "All"
58
+
59
+ **Used strictly inside a `checkboxgroup`.**
60
+ It designates one specific option within the `items` array as the "Select All" for that specific group.
61
+
62
+ - **Prop:** `groupController`
63
+ - **Type:** `Boolean`
64
+ - **Value:** `true`
65
+ - **Location:** Inside an object in the `items` array.
66
+
67
+ ---
68
+
69
+ ## 🚀 Usage Examples
70
+
71
+ ### Example 1: Standard Checkbox Controlling Others
72
+
73
+ A standalone "Select All" checkbox controlling separate checkbox elements.
74
+
75
+ ```javascript
76
+ {
77
+ // 1. The Parent
78
+ selectAll: {
79
+ type: 'checkbox',
80
+ text: 'Select All',
81
+ // Tell parent who to control (Downstream)
82
+ // NOTE: Must be the full path to find the element via form$.el(fullPath)
83
+ controls: ['container.option1', 'container.option2']
84
+ },
85
+ container: {
86
+ type: 'group',
87
+ schema: {
88
+ // 2. The Children
89
+ option1: {
90
+ type: 'checkbox',
91
+ text: 'Option 1',
92
+ // Tell child who controls it (Upstream)
93
+ controller: 'selectAll'
94
+ },
95
+ option2: {
96
+ type: 'checkbox',
97
+ text: 'Option 2',
98
+ // Tell child who controls it (Upstream)
99
+ controller: 'selectAll'
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Example 2: Parent Controlling a CheckboxGroup
107
+
108
+ A standalone checkbox controlling a whole group list.
109
+
110
+ ```javascript
111
+ {
112
+ // 1. The Parent
113
+ selectAll: {
114
+ type: 'checkbox',
115
+ controls: ['myGroup'] // Points to the group element
116
+ },
117
+
118
+ // 2. The Child (Group)
119
+ myGroup: {
120
+ type: 'checkboxgroup',
121
+ controller: 'selectAll', // Points back to parent
122
+ items: [
123
+ { value: 'a', label: 'A' },
124
+ { value: 'b', label: 'B' }
125
+ ]
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Example 3: CheckboxGroup with Internal "Select All"
131
+
132
+ A group that contains its own "All" option inside the list.
133
+
134
+ ```javascript
135
+ {
136
+ myGroup: {
137
+ type: 'checkboxgroup',
138
+ items: [
139
+ // 1. The Internal Controller
140
+ {
141
+ value: 'all',
142
+ label: 'Select All',
143
+ groupController: true // <--- The Magic Prop
144
+ },
145
+ // 2. The Siblings
146
+ { value: 'a', label: 'Item A' },
147
+ { value: 'b', label: 'Item B' }
148
+ ]
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Example 4: Complex Nested Structure
154
+
155
+ A group that contains its own "All" option inside the list, and a main checkbox controlling the group.
156
+
157
+ ```javascript
158
+ {
159
+ selectAllText: {
160
+ type: "static",
161
+ tag: "p",
162
+ content: "<div>Select Fields to be Exported</div>",
163
+ columns: { container: 9 },
164
+ },
165
+
166
+ // --- MAIN CONTROLLER ---
167
+ selectAll: {
168
+ type: "checkbox",
169
+ text: "All Fields",
170
+ columns: { container: 3 },
171
+ align: "left",
172
+ // EXTERNAL CONTROL: Controls the two groups
173
+ controls: [
174
+ "container.reqDetailsCheckboxes",
175
+ "container.reqFieldsCheckboxes",
176
+ ],
177
+ },
178
+
179
+ container: {
180
+ type: "group",
181
+ class: "dimBackground_50 scrollable",
182
+ schema: {
183
+ reqDetailsLabel: {
184
+ type: "static",
185
+ tag: "p",
186
+ content: "<p>Requisition Details</p>",
187
+ },
188
+
189
+ // --- GROUP 1 ---
190
+ reqDetailsCheckboxes: {
191
+ type: "checkboxgroup",
192
+ class: "flex-container",
193
+ // EXTERNAL LINK: Points to Main Controller
194
+ controller: "selectAll",
195
+ items: [
196
+ {
197
+ // INTERNAL CONTROL: Marks this as the group's "All" button
198
+ value: "0",
199
+ label: "All",
200
+ groupController: true,
201
+ },
202
+ { value: "1", label: "Requisition ID" },
203
+ { value: "2", label: "Date Created" },
204
+ ],
205
+ },
206
+
207
+ reqFieldsLabel: {
208
+ type: "static",
209
+ tag: "p",
210
+ content: "<p>Requisition Fields</p>",
211
+ },
212
+
213
+ // --- GROUP 2 ---
214
+ reqFieldsCheckboxes: {
215
+ type: "checkboxgroup",
216
+ class: "flex-container",
217
+ // EXTERNAL LINK: Points to Main Controller
218
+ controller: "selectAll",
219
+ items: [
220
+ {
221
+ // INTERNAL CONTROL
222
+ value: "0",
223
+ label: "All",
224
+ groupController: true,
225
+ },
226
+ { value: "1", label: "Field ID" },
227
+ { value: "2", label: "Field Created" },
228
+ ],
229
+ },
230
+ },
231
+ },
232
+
233
+ saveSelections: {
234
+ type: "checkbox",
235
+ text: "Save My Selections",
236
+ },
237
+ }
238
+ ```
package/src/index.js ADDED
@@ -0,0 +1,144 @@
1
+ import { watch, onMounted, nextTick } from "vue";
2
+
3
+ export default function CheckboxController() {
4
+ return {
5
+ apply: ["CheckboxElement", "CheckboxgroupElement"],
6
+ props: {
7
+ controls: { type: Array, default: null }, // External: Downstream
8
+ controller: { type: String, default: null }, // External: Upstream
9
+ },
10
+ setup(props, context, component) {
11
+ const { form$, el$ } = component;
12
+
13
+ // --- Helpers ---
14
+ const isComplete = (el) => {
15
+ if (!el) return false;
16
+ if (el.type === "checkbox") return el.value === true;
17
+ if (el.type === "checkboxgroup") {
18
+ // Complete if selected count equals total options count
19
+ return el.value?.length > 0 && el.value?.length === el.items?.length;
20
+ }
21
+ return false;
22
+ };
23
+
24
+ const setElementState = (el, makeChecked) => {
25
+ if (!el) return;
26
+ if (el.type === "checkbox") {
27
+ if (makeChecked && el.value !== true) el.check();
28
+ if (!makeChecked && el.value !== false) el.uncheck();
29
+ } else if (el.type === "checkboxgroup") {
30
+ if (makeChecked && !isComplete(el)) el.checkAll();
31
+ else if (!makeChecked && el.value?.length > 0) el.uncheckAll();
32
+ }
33
+ };
34
+
35
+ onMounted(() => {
36
+ nextTick(() => {
37
+ // ---------------------------------------------------------
38
+ // 1. EXTERNAL DOWNSTREAM (Main Controller -> This Element)
39
+ // ---------------------------------------------------------
40
+ if (props.controls && props.controls.length > 0) {
41
+ watch(component.value, () => {
42
+ if (el$.value.__locked) {
43
+ el$.value.__locked = false;
44
+ return;
45
+ }
46
+ const shouldCheck = isComplete(el$.value);
47
+ props.controls.forEach((path) => {
48
+ const child = form$.value.el$(path);
49
+ if (child) setElementState(child, shouldCheck);
50
+ });
51
+ });
52
+ }
53
+
54
+ // ---------------------------------------------------------
55
+ // 2. EXTERNAL UPSTREAM (This Element -> Main Controller)
56
+ // ---------------------------------------------------------
57
+ if (props.controller) {
58
+ watch(component.value, () => {
59
+ if (el$.value.__locked) {
60
+ el$.value.__locked = false;
61
+ return;
62
+ }
63
+ const parent = form$.value.el$(props.controller);
64
+ if (!parent || !parent.controls) return;
65
+
66
+ const allSiblingsChecked = parent.controls.every((path) => {
67
+ const sibling = form$.value.el$(path);
68
+ return sibling ? isComplete(sibling) : false;
69
+ });
70
+
71
+ const parentIsChecked = isComplete(parent);
72
+ if (allSiblingsChecked !== parentIsChecked) {
73
+ parent.__locked = true;
74
+ setElementState(parent, allSiblingsChecked);
75
+ }
76
+ });
77
+ }
78
+
79
+ // ---------------------------------------------------------
80
+ // 3. INTERNAL GROUP LOGIC (CheckboxGroup Only)
81
+ // Handles the "All" option inside the items array
82
+ // ---------------------------------------------------------
83
+ if (el$.value.type === "checkboxgroup") {
84
+ // Find the "All" option based on the custom property 'groupController'
85
+ const controllerItem = el$.value.items.find(
86
+ (option) => option.groupController
87
+ );
88
+
89
+ if (controllerItem) {
90
+ const ctrlVal = controllerItem.value;
91
+ const otherVals = el$.value.items
92
+ .filter((i) => i.value !== ctrlVal)
93
+ .map((i) => i.value);
94
+
95
+ watch(component.value, (newVal, oldVal) => {
96
+ // If locked by external logic, skip internal calculation
97
+ if (el$.value.__lockedInternal) {
98
+ el$.value.__lockedInternal = false;
99
+ return;
100
+ }
101
+
102
+ // Determine what happened
103
+ const wasChecked = oldVal.includes(ctrlVal);
104
+ const isChecked = newVal.includes(ctrlVal);
105
+
106
+ // Scenario A: User clicked "All" (Checked it)
107
+ if (!wasChecked && isChecked) {
108
+ // Lock to prevent infinite recursion
109
+ el$.value.__lockedInternal = true;
110
+ el$.value.checkAll();
111
+ return;
112
+ }
113
+
114
+ // Scenario B: User clicked "All" (Unchecked it)
115
+ if (wasChecked && !isChecked) {
116
+ el$.value.__lockedInternal = true;
117
+ el$.value.uncheckAll();
118
+ return;
119
+ }
120
+
121
+ // Scenario C: User clicked a Sibling (Check logic)
122
+ const othersComplete = otherVals.every((v) =>
123
+ newVal.includes(v)
124
+ );
125
+
126
+ if (isChecked && !othersComplete) {
127
+ // "All" is checked, but a sibling is missing -> Uncheck "All"
128
+ el$.value.__lockedInternal = true;
129
+ el$.value.value = newVal.filter((v) => v !== ctrlVal);
130
+ } else if (!isChecked && othersComplete) {
131
+ // "All" is unchecked, but all siblings are there -> Check "All"
132
+ el$.value.__lockedInternal = true;
133
+ el$.value.value = [...newVal, ctrlVal];
134
+ }
135
+ });
136
+ }
137
+ }
138
+ });
139
+ });
140
+
141
+ return component;
142
+ },
143
+ };
144
+ }