@ekzo-dev/bootstrap-addons 5.3.21 → 5.3.22

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.
@@ -0,0 +1,324 @@
1
+ # Select Dropdown
2
+
3
+ An enhanced select component with improved styling and functionality, extending the base `BsSelect` component.
4
+
5
+ ## Overview
6
+
7
+ The Select Dropdown component provides a feature-rich alternative to native select elements with Bootstrap styling, multi-select support, and performance optimization for large datasets.
8
+
9
+ ## Features
10
+
11
+ - Single and multi-select support
12
+ - Multiple option formats (array, object, entries)
13
+ - Empty value handling
14
+ - Bootstrap form styling
15
+ - Floating label support
16
+ - Size variants (sm, lg)
17
+ - Performance optimized for large datasets
18
+ - Custom value matching
19
+ - Keyboard navigation
20
+ - Full Bootstrap form integration
21
+ - Inherits all BaseField functionality
22
+
23
+ ## Basic Usage
24
+
25
+ ```html
26
+ <bs-select-dropdown
27
+ value.bind="selectedValue"
28
+ options.bind="countries"
29
+ label="Country"
30
+ ></bs-select-dropdown>
31
+ ```
32
+
33
+ ```typescript
34
+ export class MyComponent {
35
+ selectedValue = 'us';
36
+
37
+ countries = [
38
+ { value: 'us', text: 'United States' },
39
+ { value: 'uk', text: 'United Kingdom' },
40
+ { value: 'ca', text: 'Canada' }
41
+ ];
42
+ }
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ ### Options as Array
48
+
49
+ ```typescript
50
+ export class MyComponent {
51
+ options = [
52
+ { value: 1, text: 'Option 1' },
53
+ { value: 2, text: 'Option 2', disabled: true },
54
+ { value: 3, text: 'Option 3' }
55
+ ];
56
+ }
57
+ ```
58
+
59
+ ### Options as Object
60
+
61
+ ```typescript
62
+ export class MyComponent {
63
+ options = {
64
+ '1': 'Option 1',
65
+ '2': 'Option 2',
66
+ '3': 'Option 3'
67
+ };
68
+ }
69
+ ```
70
+
71
+ ### Options as Entries
72
+
73
+ ```typescript
74
+ export class MyComponent {
75
+ options = [
76
+ ['us', 'United States'],
77
+ ['uk', 'United Kingdom'],
78
+ ['ca', 'Canada']
79
+ ];
80
+ }
81
+ ```
82
+
83
+ ### Multi-Select
84
+
85
+ ```html
86
+ <bs-select-dropdown
87
+ value.bind="selectedValues"
88
+ options.bind="colors"
89
+ label="Favorite Colors"
90
+ multiple.bind="true"
91
+ ></bs-select-dropdown>
92
+ ```
93
+
94
+ ```typescript
95
+ export class MyComponent {
96
+ selectedValues = ['red', 'blue'];
97
+
98
+ colors = [
99
+ { value: 'red', text: 'Red' },
100
+ { value: 'blue', text: 'Blue' },
101
+ { value: 'green', text: 'Green' }
102
+ ];
103
+ }
104
+ ```
105
+
106
+ ### With Floating Label
107
+
108
+ ```html
109
+ <bs-select-dropdown
110
+ value.bind="selectedValue"
111
+ options.bind="options"
112
+ label="Select Option"
113
+ floating-label.bind="true"
114
+ bs-size="lg"
115
+ ></bs-select-dropdown>
116
+ ```
117
+
118
+ ### With Validation
119
+
120
+ ```html
121
+ <bs-select-dropdown
122
+ value.bind="selectedCountry"
123
+ options.bind="countries"
124
+ label="Country"
125
+ required.bind="true"
126
+ valid.bind="isValid"
127
+ invalid-feedback="Please select a country"
128
+ ></bs-select-dropdown>
129
+ ```
130
+
131
+ ### With Grouped Options
132
+
133
+ ```typescript
134
+ export class MyComponent {
135
+ options = [
136
+ { value: 'usa', text: 'United States', group: 'North America' },
137
+ { value: 'can', text: 'Canada', group: 'North America' },
138
+ { value: 'mex', text: 'Mexico', group: 'North America' },
139
+ { value: 'uk', text: 'United Kingdom', group: 'Europe' },
140
+ { value: 'de', text: 'Germany', group: 'Europe' }
141
+ ];
142
+ }
143
+ ```
144
+
145
+ ### Large Dataset (Performance)
146
+
147
+ ```html
148
+ <bs-select-dropdown
149
+ value.bind="selectedId"
150
+ options.bind="largeDataset"
151
+ label="Select Item"
152
+ size.bind="10"
153
+ ></bs-select-dropdown>
154
+ ```
155
+
156
+ ```typescript
157
+ export class MyComponent {
158
+ largeDataset = Array.from({ length: 1000 }, (_, i) => ({
159
+ value: i,
160
+ text: `Item ${i + 1}`
161
+ }));
162
+ }
163
+ ```
164
+
165
+ ### With Custom Matcher
166
+
167
+ ```html
168
+ <bs-select-dropdown
169
+ value.bind="selectedUser"
170
+ options.bind="users"
171
+ matcher.bind="userMatcher"
172
+ label="Select User"
173
+ ></bs-select-dropdown>
174
+ ```
175
+
176
+ ```typescript
177
+ export class MyComponent {
178
+ selectedUser = { id: 1, name: 'John' };
179
+
180
+ users = [
181
+ { value: { id: 1, name: 'John' }, text: 'John Doe' },
182
+ { value: { id: 2, name: 'Jane' }, text: 'Jane Smith' }
183
+ ];
184
+
185
+ userMatcher = (a, b) => a?.id === b?.id;
186
+ }
187
+ ```
188
+
189
+ ### With Empty Value
190
+
191
+ ```html
192
+ <bs-select-dropdown
193
+ value.bind="selectedValue"
194
+ options.bind="options"
195
+ empty-value.bind="null"
196
+ label="Select Option"
197
+ ></bs-select-dropdown>
198
+ ```
199
+
200
+ ## Bindable Properties
201
+
202
+ | Property | Type | Default | Description |
203
+ |----------|------|---------|-------------|
204
+ | `value` | `any \| any[]` | - | Selected value(s), array for multiple mode |
205
+ | `options` | `ISelectOption[] \| [any, string][] \| Record<string, string>` | `[]` | Options as array of objects, entries, or key-value object |
206
+ | `multiple` | `boolean` | `false` | Enable multi-select mode |
207
+ | `floatingLabel` | `boolean` | `false` | Enable floating label style |
208
+ | `size` | `number` | - | Number of visible options (HTML size attribute) |
209
+ | `bsSize` | `'sm' \| 'lg'` | - | Bootstrap size variant |
210
+ | `autocomplete` | `string` | - | Autocomplete attribute value |
211
+ | `matcher` | `(a: any, b: any) => boolean` | - | Custom function to compare values |
212
+ | `emptyValue` | `any` | - | Value to use when no option is selected |
213
+
214
+ ### Inherited from BaseField
215
+
216
+ | Property | Type | Default | Description |
217
+ |----------|------|---------|-------------|
218
+ | `name` | `string` | - | Input name attribute |
219
+ | `label` | `string` | - | Label text |
220
+ | `title` | `string` | - | Title attribute |
221
+ | `disabled` | `boolean` | `false` | Disable the select |
222
+ | `required` | `boolean` | `false` | Mark as required |
223
+ | `valid` | `boolean` | - | Validation state |
224
+ | `validFeedback` | `string` | - | Valid feedback message |
225
+ | `invalidFeedback` | `string` | - | Invalid feedback message |
226
+ | `form` | `string` | - | Associated form id |
227
+ | `text` | `string \| HTMLElement` | - | Helper text |
228
+
229
+ ## ISelectOption Interface
230
+
231
+ ```typescript
232
+ interface ISelectOption<T = unknown> {
233
+ value: T; // The actual value
234
+ text: string; // Display text
235
+ disabled?: boolean; // Whether option is disabled
236
+ group?: string; // Optional group name
237
+ }
238
+ ```
239
+
240
+ ## Value Matching
241
+
242
+ By default, values are compared using strict equality (`===`). For complex objects, provide a custom `matcher` function:
243
+
244
+ ```typescript
245
+ // Match by ID
246
+ matcher = (a, b) => a?.id === b?.id;
247
+
248
+ // Match by multiple properties
249
+ matcher = (a, b) => a?.type === b?.type && a?.id === b?.id;
250
+
251
+ // Deep equality (use with caution for performance)
252
+ matcher = (a, b) => JSON.stringify(a) === JSON.stringify(b);
253
+ ```
254
+
255
+ ## Multi-Select Mode
256
+
257
+ When `multiple` is true:
258
+
259
+ - Value is an array
260
+ - Multiple options can be selected
261
+ - Ctrl/Cmd+Click to toggle selection
262
+ - Shift+Click for range selection
263
+
264
+ ```typescript
265
+ export class MyComponent {
266
+ selectedValues: string[] = ['option1', 'option2'];
267
+ }
268
+ ```
269
+
270
+ ## Empty Value Handling
271
+
272
+ Use `emptyValue` to specify what value should be used when no option is selected:
273
+
274
+ ```html
275
+ <!-- Use null for empty -->
276
+ <bs-select-dropdown empty-value.bind="null"></bs-select-dropdown>
277
+
278
+ <!-- Use undefined for empty -->
279
+ <bs-select-dropdown empty-value.bind="undefined"></bs-select-dropdown>
280
+
281
+ <!-- Use empty string for empty -->
282
+ <bs-select-dropdown empty-value.bind="''"></bs-select-dropdown>
283
+ ```
284
+
285
+ ## Styling
286
+
287
+ The component uses standard Bootstrap form classes and can be styled using Bootstrap utilities:
288
+
289
+ ```html
290
+ <bs-select-dropdown
291
+ value.bind="value"
292
+ options.bind="options"
293
+ label="Select"
294
+ class="mb-3"
295
+ bs-size="sm"
296
+ ></bs-select-dropdown>
297
+ ```
298
+
299
+ ## Accessibility
300
+
301
+ The component follows accessibility best practices:
302
+
303
+ - Proper label association
304
+ - ARIA attributes for validation states
305
+ - Keyboard navigation (Arrow keys, Enter, Space)
306
+ - Screen reader friendly
307
+ - Focus management
308
+
309
+ ## Performance Optimization
310
+
311
+ For large datasets:
312
+
313
+ 1. Use the `size` attribute to limit visible options
314
+ 2. Consider virtual scrolling for very large lists
315
+ 3. Avoid re-rendering by using stable option references
316
+ 4. Use primitive values instead of objects when possible
317
+
318
+ ## Browser Support
319
+
320
+ Requires browsers that support:
321
+
322
+ - ES2015+
323
+ - Custom Elements
324
+ - HTML5 form controls
@@ -29,7 +29,7 @@
29
29
  type="checkbox"
30
30
  checked.bind="value"
31
31
  matcher.bind="matcher"
32
- model.one-time="option.value"
32
+ value.one-time="option.value"
33
33
  disabled.one-time="option.disabled"
34
34
  id.one-time="optionId($index)"
35
35
  />
@@ -45,7 +45,7 @@
45
45
  type="checkbox"
46
46
  checked.bind="value"
47
47
  matcher.bind="matcher"
48
- model.one-time="option.value"
48
+ value.one-time="option.value"
49
49
  disabled.one-time="option.disabled"
50
50
  id.one-time="optionId($index, $parent.$index)"
51
51
  />
@@ -0,0 +1,137 @@
1
+ import { BsButton, BsOffcanvas } from '@ekzo-dev/bootstrap';
2
+
3
+ import { BsSelectDropdown } from '.';
4
+
5
+ const meta = {
6
+ title: 'Bootstrap Addons / Forms / Select',
7
+ component: BsSelectDropdown,
8
+ parameters: {
9
+ actions: {
10
+ handles: ['change', 'input'],
11
+ },
12
+ },
13
+ render: () => ({
14
+ template: `<bs-select-dropdown
15
+ value.bind='value'
16
+ options.bind='options'
17
+ multiple.bind='multiple'
18
+ floating-label.bind='floatingLabel'
19
+ size.bind='size'
20
+ bs-size.bind='bsSize'
21
+ autocomplete.bind='autocomplete'
22
+ matcher.bind='matcher'
23
+ empty-value.bind='emptyValue'
24
+ name.bind='name'
25
+ label.bind='label'
26
+ title.bind='title'
27
+ disabled.bind='disabled'
28
+ required.bind='required'
29
+ valid.bind='valid'
30
+ valid-feedback.bind='validFeedback'
31
+ invalid-feedback.bind='invalidFeedback'
32
+ form.bind='form'
33
+ text.bind='text'
34
+ ></bs-select-dropdown>`,
35
+ }),
36
+ argTypes: {
37
+ // BsSelectDropdown properties
38
+ value: { control: 'object' },
39
+ options: { control: 'object' },
40
+ multiple: { control: 'boolean' },
41
+ floatingLabel: { control: 'boolean' },
42
+ size: { control: 'number' },
43
+ bsSize: {
44
+ control: 'select',
45
+ options: ['sm', 'lg'],
46
+ },
47
+ autocomplete: { control: 'text' },
48
+ matcher: { control: 'object' },
49
+ emptyValue: { control: 'object' },
50
+
51
+ // BaseField properties
52
+ name: { control: 'text' },
53
+ label: { control: 'text' },
54
+ title: { control: 'text' },
55
+ disabled: { control: 'boolean' },
56
+ required: { control: 'boolean' },
57
+ valid: { control: 'boolean' },
58
+ validFeedback: { control: 'text' },
59
+ invalidFeedback: { control: 'text' },
60
+ form: { control: 'text' },
61
+ text: { control: 'text' },
62
+ },
63
+ };
64
+
65
+ export default meta;
66
+
67
+ export const Overview = {
68
+ args: {
69
+ label: 'Label',
70
+ floatingLabel: false,
71
+ valid: undefined,
72
+ options: [
73
+ { value: undefined, text: 'Select option' },
74
+ { value: '1', text: 'One', disabled: true },
75
+ { value: '2', text: 'Two' },
76
+ { value: '3', text: 'Three', group: 'Group' },
77
+ ],
78
+ value: '2',
79
+ },
80
+ };
81
+
82
+ export const Multiple = {
83
+ args: {
84
+ label: 'Label',
85
+ floatingLabel: false,
86
+ valid: undefined,
87
+ multiple: true,
88
+ value: ['2', '3'],
89
+ options: [
90
+ { value: '1', text: 'One', disabled: true },
91
+ { value: '2', text: 'Two' },
92
+ { value: '3', text: 'Three', group: 'Group' },
93
+ ],
94
+ },
95
+ };
96
+
97
+ export const LargeOptions = {
98
+ render: () => ({
99
+ template: `<bs-select-dropdown
100
+ value.bind='value'
101
+ options.bind='options'
102
+ label.bind='label'
103
+ style='width: 400px; max-width: 100%'
104
+ ></bs-select-dropdown>`,
105
+ }),
106
+ args: {
107
+ label: 'Label',
108
+ options: Array.from({ length: 1000 }).map((v, i) => ({
109
+ value: i.toString(),
110
+ text: `Option ${i} has long content which forces dropdown menu to scale larger that select box`,
111
+ })),
112
+ },
113
+ };
114
+
115
+ export const InModal = {
116
+ render: () => ({
117
+ template: `
118
+ <button bs-button click.trigger="offcanvas.toggle()">Open modal</button>
119
+ <bs-offcanvas component.ref="offcanvas">
120
+ <bs-select-dropdown
121
+ value.bind='value'
122
+ options.bind='options'
123
+ label.bind='label'
124
+ style='width: 100%'
125
+ ></bs-select-dropdown>
126
+ <div style="height: 2000px"></div>
127
+ </bs-offcanvas>`,
128
+ components: [BsOffcanvas, BsButton],
129
+ }),
130
+ args: {
131
+ label: 'Label',
132
+ options: Array.from({ length: 1000 }).map((v, i) => ({
133
+ value: i.toString(),
134
+ text: `Option ${i} has long content which forces dropdown menu to scale larger that select box`,
135
+ })),
136
+ },
137
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from './forms/duration-input';
2
- export * from './forms/json-input';
3
2
  export * from './forms/select-dropdown';
@@ -1 +0,0 @@
1
- export * from './json-input';
@@ -1,15 +0,0 @@
1
- <template>
2
- <input ref="input" required.bind="inputRequired" />
3
- <json-editor
4
- ref="editorElement"
5
- component.ref="editorComponent"
6
- content.to-view="content"
7
- validator.bind="validator"
8
- read-only.bind="disabled"
9
- on-render-value.one-time="onRenderValue"
10
- on-render-menu.one-time="onRenderMenu"
11
- on-change.one-time="onChange"
12
- mode="text"
13
- ...$bindables="jsonEditorOptions"
14
- ></json-editor>
15
- </template>
@@ -1,17 +0,0 @@
1
- bs-json-input {
2
- display: block;
3
- position: relative;
4
-
5
- > input {
6
- position: absolute;
7
- top: 0;
8
- left: 0;
9
- width: 100%;
10
- height: 100%;
11
- z-index: -1;
12
- }
13
-
14
- > json-editor {
15
- height: 100%;
16
- }
17
- }
@@ -1,59 +0,0 @@
1
- // import { Meta, Story, StoryFnAureliaReturnType } from '@storybook/aurelia';
2
- //
3
- // import { BsJsonInput } from '.';
4
- //
5
- // const meta: Meta = {
6
- // title: 'Ekzo / Bootstrap Addons / Forms / Json input',
7
- // component: BsJsonInput,
8
- // };
9
- //
10
- // export default meta;
11
- //
12
- // const Overview: Story = (args): StoryFnAureliaReturnType => ({
13
- // props: args,
14
- // });
15
- //
16
- // Overview.args = {
17
- // jsonSchema: {
18
- // $schema: 'https://json-schema.org/draft/2020-12/schema',
19
- // type: 'object',
20
- // properties: {
21
- // enum: {
22
- // type: 'string',
23
- // enum: ['1', '2', '3'],
24
- // },
25
- // boolean: {
26
- // type: 'boolean',
27
- // },
28
- // email: {
29
- // type: 'string',
30
- // format: 'email',
31
- // },
32
- // number: {
33
- // type: 'number',
34
- // multipleOf: 0.0001,
35
- // },
36
- // },
37
- // required: ['enum'],
38
- // },
39
- // value: {
40
- // number: 1.11,
41
- // enum: '1',
42
- // },
43
- // };
44
- //
45
- // const JsonSchemaEditor: Story = (args): StoryFnAureliaReturnType => ({
46
- // props: args,
47
- // });
48
- //
49
- // JsonSchemaEditor.args = {
50
- // jsonSchema: true,
51
- // value: {
52
- // $schema: 'http://json-schema.org/draft-07/schema#',
53
- // type: 'object',
54
- // properties: {},
55
- // },
56
- // };
57
- //
58
- // // eslint-disable-next-line
59
- // export { Overview, JsonSchemaEditor };