alchemy-form 0.3.0-alpha.4 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- ## 0.3.0-alpha.4
1
+ ## 0.3.0 (2026-01-21)
2
+
3
+ * Release as v0.3.0
4
+
5
+ ## 0.3.0-alpha.6
6
+
7
+ * Fix model and form resolution during client-side re-render when element is not yet in DOM
8
+ * Add fallback to default wrappers when field_type cannot be determined
9
+ * Implement `handleScrollEvent()` in `al-virtual-scroll` element - throttled culling of invisible elements during scroll
10
+ * Implement keyboard navigation for selected values in `al-select` - arrow keys now move between selected items when focus is on the component
11
+ * Fix potential deadlock in `al-field` when setting value during render - skip rerender if already rendering
12
+
13
+ ## 0.3.0-alpha.5 (2025-07-10)
14
+
15
+ * Add `al-datetime-input` to properly handle Date values with timezones
16
+
17
+ ## 0.3.0-alpha.4 (2025-07-10)
2
18
 
3
19
  * Always make `al-field`'s `getFieldType()` method return a string
4
20
  * Add the `mixed` field view
package/CLAUDE.md ADDED
@@ -0,0 +1,348 @@
1
+ # Alchemy Form Development Guide
2
+
3
+ ## Overview
4
+
5
+ Alchemy Form is a comprehensive form handling plugin for AlchemyMVC that provides custom HTML elements for building, validating, and rendering forms. It supports complex field types, relationships, arrays, schemas, and advanced features like query builders.
6
+
7
+ ## Commands
8
+ - Run tests: `npm test`
9
+
10
+ ## Dependencies
11
+ - alchemymvc (>=1.4.0)
12
+ - alchemy-media (>=0.9.0)
13
+ - alchemy-styleboost (>=0.5.0)
14
+
15
+ ## Directory Structure
16
+
17
+ ```
18
+ element/ # Custom form elements (~48 files)
19
+ ├── 00_form_base.js # Base class for all form elements
20
+ ├── 05_feedback_input.js # Abstract element with error/success feedback
21
+ ├── 10_dataprovider.js # Base for elements with remote data
22
+ ├── 40_stateful.js # State management for elements
23
+ ├── al_form.js # Main form element
24
+ ├── al_field.js # Base field element
25
+ ├── al_field_array.js # Array field wrapper
26
+ ├── al_field_schema.js # Schema/nested field support
27
+ ├── al_field_translatable.js # Multilingual field support
28
+ ├── al_select.js # Dropdown selector
29
+ ├── al_table.js # Data table element
30
+ ├── al_button.js # Interactive button with states
31
+ ├── al_query_builder*.js # Query builder UI (5 files)
32
+ └── ... # Other input types
33
+
34
+ helper/
35
+ ├── query_builder_ns.js # Query builder namespace & criteria
36
+ ├── field_recompute_handler.js # Computed field handler
37
+ ├── form_actions/ # Form action handlers
38
+ └── query_builder_variable_definition/ # Variable type definitions
39
+
40
+ controller/
41
+ └── form_api_controller.js # Backend API controller
42
+
43
+ config/
44
+ └── routes.js # API routes
45
+ ```
46
+
47
+ ## Core Element Hierarchy
48
+
49
+ ```
50
+ Alchemy.Element.Form.Base
51
+ ├── al-form # Main form container
52
+ ├── al-field # Base field element
53
+ │ ├── al-field-array # Array fields
54
+ │ ├── al-field-schema # Nested schema fields
55
+ │ └── al-field-translatable # Multilingual fields
56
+ ├── FeedbackInput (abstract)
57
+ │ ├── al-string-input
58
+ │ ├── al-number-input
59
+ │ └── al-password-input
60
+ ├── WithDataprovider (abstract)
61
+ │ ├── al-select
62
+ │ ├── al-table
63
+ │ └── al-query-builder
64
+ └── Stateful (abstract)
65
+ └── al-button
66
+ ```
67
+
68
+ ## Key Attributes
69
+
70
+ ### Form Element (`<al-form>`)
71
+ - `action` - URL/route for submission
72
+ - `method` - POST/GET
73
+ - `model` - Associated data model name
74
+ - `readonly` - Read-only form flag
75
+ - `serialize-entire-document` - Include all document properties
76
+
77
+ ### Field Element (`<al-field>`)
78
+ - `field-name` - Name in record
79
+ - `field-type` - Type of field (text, number, select, schema, array, etc.)
80
+ - `field-view` - Override template name
81
+ - `wrapper-view` - Override wrapper template
82
+ - `data-src` - Data source URL for dynamic options
83
+ - `min-entry-count`, `max-entry-count` - Array bounds
84
+ - `readonly` - Read-only field
85
+
86
+ ### Shared Attributes
87
+ - `purpose` - "edit" or "view" mode
88
+ - `mode` - "inline", "standalone", etc.
89
+ - `zone` - "admin", "frontend", "chimera", etc.
90
+
91
+ ## Form Usage
92
+
93
+ ### Basic Form
94
+ ```html
95
+ <al-form action="/api/save" model="User">
96
+ <al-field field-name="username"></al-field>
97
+ <al-field field-name="email"></al-field>
98
+ <al-button behaviour="submit">Save</al-button>
99
+ </al-form>
100
+ ```
101
+
102
+ ### Programmatic Form Control
103
+ ```javascript
104
+ const form = document.querySelector('al-form');
105
+
106
+ // Set document data
107
+ form.document = userDoc;
108
+
109
+ // Get form values
110
+ const values = form.getMainValue();
111
+
112
+ // Validate
113
+ const valid = await form.validate();
114
+
115
+ // Submit
116
+ await form.submit();
117
+
118
+ // Show validation errors
119
+ form.showViolations(validationError);
120
+
121
+ // Find specific field
122
+ const field = form.findFieldByPath('address.city');
123
+ ```
124
+
125
+ ## Field Types
126
+
127
+ ### Basic Types
128
+ - `text`, `string` - Text input
129
+ - `number` - Numeric input
130
+ - `password` - Password input
131
+ - `boolean` - Toggle switch
132
+ - `date`, `datetime` - Date picker
133
+
134
+ ### Complex Types
135
+ - `enum` - Dropdown selection
136
+ - `schema` - Nested object fields
137
+ - `array` - Repeatable field groups
138
+ - `belongsto` - Foreign key relationship
139
+ - `hasmany` - Multiple relationships
140
+
141
+ ### Array Fields
142
+ ```html
143
+ <al-field field-name="tags" field-type="array"
144
+ min-entry-count="1" max-entry-count="10">
145
+ </al-field>
146
+ ```
147
+
148
+ ### Schema Fields
149
+ ```html
150
+ <al-field field-name="address" field-type="schema">
151
+ <!-- Nested fields rendered automatically -->
152
+ </al-field>
153
+ ```
154
+
155
+ ## Select Element
156
+
157
+ ```html
158
+ <al-select data-src="/api/form/data/related"
159
+ method="POST"
160
+ page-size="20">
161
+ </al-select>
162
+ ```
163
+
164
+ Properties:
165
+ - `recordsource` - Set data source
166
+ - `dataprovider` - Data loading instance
167
+ - `page`, `page_size` - Pagination
168
+
169
+ ## Query Builder
170
+
171
+ ```html
172
+ <al-query-builder>
173
+ <al-query-builder-group condition="and">
174
+ <al-query-builder-entry type="qb_entry">
175
+ <!-- Field, operator, value inputs -->
176
+ </al-query-builder-entry>
177
+ </al-query-builder-group>
178
+ </al-query-builder>
179
+ ```
180
+
181
+ Apply to criteria:
182
+ ```javascript
183
+ const Qb = Blast.Classes.Alchemy.QueryBuilder;
184
+ Qb.applyToCriteria(queryBuilderValue, criteria, inverted);
185
+ ```
186
+
187
+ Supported operators: `equals`, `ne`, `starts_with`, `ends_with`, `is_empty`, `is_null`, `>`, `<`, `>=`, `<=`
188
+
189
+ ## Button States
190
+
191
+ ```html
192
+ <al-button behaviour="submit">
193
+ <al-state name="default">Save</al-state>
194
+ <al-state name="busy">Saving...</al-state>
195
+ <al-state name="done">Saved!</al-state>
196
+ <al-state name="error">Error</al-state>
197
+ </al-button>
198
+ ```
199
+
200
+ ```javascript
201
+ button.setState('busy', 3000, 'default'); // Revert after 3s
202
+ ```
203
+
204
+ ## Table Element
205
+
206
+ ```html
207
+ <al-table data-src="/api/records" method="POST">
208
+ <!-- Columns configured via schema or attributes -->
209
+ </al-table>
210
+
211
+ <al-pager></al-pager>
212
+ ```
213
+
214
+ ## API Routes
215
+
216
+ | Route | Method | Description |
217
+ |-------|--------|-------------|
218
+ | `/api/form/data/related` | POST | Fetch related records for select/autocomplete |
219
+ | `/api/form/data/qbdata` | POST | Load query builder variable data |
220
+ | `/api/form/data/recompute/{model}/{field}` | POST | Recompute computed field |
221
+ | `/api/form/data/enum_info/{model}/{id}` | GET | Get enum display info |
222
+
223
+ ## Computed Fields
224
+
225
+ Fields can be automatically recomputed when dependencies change:
226
+
227
+ ```javascript
228
+ // Server-side: define computed field
229
+ MyModel.addField('full_name', 'String', {
230
+ computed: {
231
+ fields: ['first_name', 'last_name'],
232
+ compute: doc => `${doc.first_name} ${doc.last_name}`
233
+ }
234
+ });
235
+ ```
236
+
237
+ Client-side automatically calls recompute API when dependencies change.
238
+
239
+ ## Validation
240
+
241
+ Validation is schema-based and returns violations:
242
+
243
+ ```javascript
244
+ // Form validates against model schema
245
+ const valid = await form.validate();
246
+
247
+ // Show errors inline
248
+ form.showViolations(err);
249
+
250
+ // Access specific field errors
251
+ field.showError(violation);
252
+ ```
253
+
254
+ ## Template Customization
255
+
256
+ Field templates follow a fallback chain:
257
+ 1. `field-view` attribute value
258
+ 2. `{purpose}_{mode}` template
259
+ 3. `{purpose}` template
260
+ 4. Default template
261
+
262
+ ```html
263
+ <al-field field-name="bio" field-view="custom_textarea"></al-field>
264
+ ```
265
+
266
+ ## Event Handling
267
+
268
+ ```javascript
269
+ // Form submit
270
+ form.addEventListener('submit', (e) => {
271
+ e.preventDefault();
272
+ // Custom handling
273
+ });
274
+
275
+ // Field change
276
+ field.addEventListener('change', (e) => {
277
+ console.log('New value:', e.target.value);
278
+ });
279
+
280
+ // Button activation
281
+ button.addEventListener('activate', (e) => {
282
+ // Button was clicked
283
+ });
284
+ ```
285
+
286
+ ## Field Value Internals
287
+
288
+ ### Value Storage
289
+
290
+ `al-field` uses several internal mechanisms to manage values:
291
+
292
+ - **`LAST_SET_VALUE` (Symbol)** - Stores the most recently set value, even before render completes
293
+ - **`value_to_render`** - Property used in templates, returns `LAST_SET_VALUE` or falls back to `original_value`
294
+ - **`value_element`** - The actual input element inside the field (may not exist until rendered)
295
+ - **`original_value`** - The initial value, used for change detection
296
+
297
+ ### Value Flow
298
+
299
+ ```
300
+ setValue(value)
301
+
302
+ Store in LAST_SET_VALUE
303
+
304
+ If value_element exists and has setter → set directly
305
+
306
+ Otherwise → rerender() to update template
307
+
308
+ Template uses value_to_render → renders value_element
309
+ ```
310
+
311
+ ### Render Safety
312
+
313
+ When setting values during a render (before `value_element` exists):
314
+ - Check `this.hasAttribute('data-he-rerendering')` before calling `rerender()`
315
+ - The value is already in `LAST_SET_VALUE` and will be picked up via `value_to_render`
316
+ - Calling `rerender()` during a render can cause deadlocks
317
+
318
+ ### Key Methods
319
+
320
+ | Method | Description |
321
+ |--------|-------------|
322
+ | `valueElementHasValuePropertySetter()` | Checks if value_element can accept values directly |
323
+ | `value_to_render` (property) | Returns the value for template rendering |
324
+ | `prepareRenderVariables()` | Hawkejs lifecycle method, sets up template variables |
325
+
326
+ ## Gotchas
327
+
328
+ 1. **Field paths:** Use dot notation for nested fields (`address.city`)
329
+
330
+ 2. **Array indices:** Array fields include index in path (`items.0.name`)
331
+
332
+ 3. **Schema supplier:** Schema fields need parent field reference via `getSchemaSupplierField()`
333
+
334
+ 4. **Dataprovider base:** Elements using remote data extend `WithDataprovider`, not `Base`
335
+
336
+ 5. **Purpose inheritance:** Child elements inherit `purpose` from parent form
337
+
338
+ 6. **Value serialization:** `form.getMainValue()` returns unwrapped object, `form.value` wraps with model name
339
+
340
+ 7. **Readonly mode:** Set `purpose="view"` for read-only display
341
+
342
+ 8. **Zone-based templates:** Use `zone` attribute for context-specific rendering (admin vs frontend)
343
+
344
+ 9. **Rerender during render:** Never call `rerender()` when `data-he-rerendering` attribute is present - can cause deadlock
345
+
346
+ 10. **Value before render:** Values set before first render are stored in `LAST_SET_VALUE` symbol and accessed via `value_to_render`
347
+
348
+ 11. **value vs value_to_render:** In templates, use `value_to_render` which handles pending values correctly
@@ -0,0 +1,6 @@
1
+ al-datetime-input {
2
+ > * {
3
+ width: 100%;
4
+ height: 100%;
5
+ }
6
+ }
@@ -17,3 +17,4 @@
17
17
  @use "_virtual_scroll.scss";
18
18
  @use "_settings_editor.scss";
19
19
  @use "_enum_badge.scss";
20
+ @use "_datetime.scss";
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The al-datetime-input:
3
+ * Populate an input on the browser-side with correct timezone info
4
+ *
5
+ * @author Jelle De Loecker <jelle@elevenways.be>
6
+ * @since 0.3.0
7
+ * @version 0.3.0
8
+ */
9
+ const DatetimeInput = Function.inherits('Alchemy.Element.Form.Base', 'DatetimeInput');
10
+
11
+ /**
12
+ * Get/set the mode
13
+ *
14
+ * @author Jelle De Loecker <jelle@elevenways.be>
15
+ * @since 0.3.0
16
+ * @version 0.3.0
17
+ */
18
+ DatetimeInput.setAttribute('datemode', null, function setMode(datemode) {
19
+ this.populateInputWithValue(this.value, datemode);
20
+ return datemode;
21
+ });
22
+
23
+ /**
24
+ * Get/set the value
25
+ *
26
+ * @author Jelle De Loecker <jelle@elevenways.be>
27
+ * @since 0.3.0
28
+ * @version 0.3.0
29
+ */
30
+ DatetimeInput.setAttribute('value', null, function setValue(value) {
31
+ return this.populateInputWithValue(value, this.datemode);
32
+ });
33
+
34
+ /**
35
+ * Set the value with a function call
36
+ *
37
+ * @author Jelle De Loecker <jelle@elevenways.be>
38
+ * @since 0.3.0
39
+ * @version 0.3.0
40
+ */
41
+ DatetimeInput.setMethod(function setValue(value) {
42
+ this.value = value;
43
+ });
44
+
45
+ /**
46
+ * Revalidate the value
47
+ *
48
+ * @author Jelle De Loecker <jelle@elevenways.be>
49
+ * @since 0.3.0
50
+ * @version 0.3.0
51
+ *
52
+ * @return {Object[]}
53
+ */
54
+ DatetimeInput.setMethod(function populateInput() {
55
+ return this.populateInputWithValue(this.value, this.datemode);
56
+ });
57
+
58
+ /**
59
+ * Revalidate the value
60
+ *
61
+ * @author Jelle De Loecker <jelle@elevenways.be>
62
+ * @since 0.3.0
63
+ * @version 0.3.0
64
+ *
65
+ * @return {Object[]}
66
+ */
67
+ DatetimeInput.setMethod(function populateInputWithValue(value, datemode) {
68
+
69
+ let iso_date,
70
+ date;
71
+
72
+ if (value) {
73
+ date = Date.create(value);
74
+ iso_date = date.toISOString();
75
+ } else {
76
+ iso_date = '';
77
+ }
78
+
79
+ value = iso_date;
80
+
81
+ if (Blast.isServer) {
82
+ return value;
83
+ }
84
+
85
+ if (arguments.length == 1) {
86
+ datemode = this.datemode;
87
+ }
88
+
89
+ let input = this.querySelector('input');
90
+
91
+ if (!datemode) {
92
+ datemode = 'datetime';
93
+ }
94
+
95
+ if (!input) {
96
+ input = this.createElement('input');
97
+ input.setAttribute('type', datemode + '-local');
98
+ }
99
+
100
+ if (!value) {
101
+ input.value = '';
102
+ return '';
103
+ }
104
+
105
+ let formatted;
106
+
107
+ if (datemode == 'date') {
108
+ formatted = date.format('Y-m-d');
109
+ } else {
110
+ formatted = date.format('Y-m-d\\TH:i:s');
111
+ }
112
+
113
+ input.value = formatted;
114
+
115
+ return iso_date;
116
+ });
117
+
118
+ /**
119
+ * Added to the DOM
120
+ *
121
+ * @author Jelle De Loecker <jelle@elevenways.be>
122
+ * @since 0.3.0
123
+ * @version 0.3.0
124
+ */
125
+ DatetimeInput.setMethod(function introduced() {
126
+ this.populateInput();
127
+ });
@@ -139,7 +139,7 @@ Field.setAttribute('placeholder');
139
139
  *
140
140
  * @author Jelle De Loecker <jelle@elevenways.be>
141
141
  * @since 0.1.0
142
- * @version 0.2.0
142
+ * @version 0.3.0
143
143
  */
144
144
  Field.enforceProperty(function alchemy_form(new_value) {
145
145
 
@@ -153,8 +153,22 @@ Field.enforceProperty(function alchemy_form(new_value) {
153
153
  if (!new_value && this.alchemy_field_schema && this.alchemy_field_schema.alchemy_field) {
154
154
  new_value = this.alchemy_field_schema.alchemy_field.alchemy_form;
155
155
  }
156
+
157
+ // Fallback: try to find al-form through the hawkejs renderer's ancestor chain
158
+ // This is needed during client-side re-render when the element isn't in the DOM yet
159
+ if (!new_value && this.hawkejs_renderer) {
160
+ let ancestor = this.hawkejs_renderer.current_variables?.$ancestor_element;
161
+
162
+ if (ancestor) {
163
+ ancestor = ancestor.queryUp('al-form');
164
+
165
+ if (ancestor) {
166
+ new_value = ancestor;
167
+ }
168
+ }
169
+ }
156
170
  }
157
-
171
+
158
172
  return new_value;
159
173
  });
160
174
 
@@ -401,7 +415,7 @@ Field.setMethod(function getPathEntryName() {
401
415
  *
402
416
  * @author Jelle De Loecker <jelle@elevenways.be>
403
417
  * @since 0.1.0
404
- * @version 0.1.0
418
+ * @version 0.3.0
405
419
  */
406
420
  Field.setProperty(function model() {
407
421
 
@@ -414,6 +428,27 @@ Field.setProperty(function model() {
414
428
  if (form) {
415
429
  return form.model;
416
430
  }
431
+
432
+ // Fallback: check if we have a stored resolved model from a previous render
433
+ // This is especially useful during client-side re-render when queryUp fails
434
+ if (this.assigned_data?._resolved_model) {
435
+ return this.assigned_data._resolved_model;
436
+ }
437
+
438
+ // Fallback: try to get the model name from the field_context
439
+ // Note: this only works if field_context is a different element (not this)
440
+ if (this.field_context && this.field_context !== this && this.field_context.model) {
441
+ return this.field_context.model;
442
+ }
443
+
444
+ // Fallback: try to get it from the alchemy_field_schema
445
+ if (this.alchemy_field_schema) {
446
+ let parent_field = this.alchemy_field_schema.alchemy_field;
447
+
448
+ if (parent_field) {
449
+ return parent_field.model;
450
+ }
451
+ }
417
452
  }, function setModel(model) {
418
453
  return this.setAttribute('model', model);
419
454
  });
@@ -520,7 +555,7 @@ Field.setProperty(function view_files() {
520
555
  *
521
556
  * @author Jelle De Loecker <jelle@elevenways.be>
522
557
  * @since 0.1.0
523
- * @version 0.1.11
558
+ * @version 0.3.0
524
559
  */
525
560
  Field.setProperty(function wrapper_files() {
526
561
 
@@ -554,8 +589,11 @@ Field.setProperty(function wrapper_files() {
554
589
  result.push(view);
555
590
  }
556
591
 
592
+ // If no wrappers were found (e.g., field_type couldn't be determined),
593
+ // add a fallback to the default wrapper so slots can still render
557
594
  if (result.length == 0) {
558
- return false;
595
+ result.push(this.generateTemplatePath('wrappers', view_type, 'default'));
596
+ result.push(this.generateTemplatePath('wrappers', 'default', 'default'));
559
597
  }
560
598
 
561
599
  return result;
@@ -696,8 +734,10 @@ Field.setProperty(function value() {
696
734
  this[LAST_SET_VALUE] = value;
697
735
 
698
736
  if (!this.valueElementHasValuePropertySetter()) {
699
- // @TODO: Rerendering during a render causes a deadlock
700
- if (has_changed) {
737
+ // Only rerender if we're not already rendering.
738
+ // If we are rendering, the value is stored in LAST_SET_VALUE
739
+ // and will be picked up via value_to_render.
740
+ if (has_changed && !this.hasAttribute('data-he-rerendering')) {
701
741
  this.rerender();
702
742
  }
703
743
  } else {
@@ -796,6 +836,14 @@ Field.setMethod(function prepareRenderVariables() {
796
836
  let value = this.value_to_render,
797
837
  value_is_empty = value == null || value === '' || (Array.isArray(value) ? value.length == 0 : false);
798
838
 
839
+ // Resolve the model name now while we might still have DOM access
840
+ // Store it in assigned_data so it survives re-renders
841
+ let resolved_model = this.model;
842
+
843
+ if (resolved_model && !this.assigned_data._resolved_model) {
844
+ this.assigned_data._resolved_model = resolved_model;
845
+ }
846
+
799
847
  let result = {
800
848
  alchemy_field : this,
801
849
  field_context : this,
@@ -1567,7 +1567,7 @@ AlchemySelect.setMethod(function _getSelection(input) {
1567
1567
  *
1568
1568
  * @author Jelle De Loecker <jelle@develry.be>
1569
1569
  * @since 0.1.0
1570
- * @version 0.1.0
1570
+ * @version 0.3.0
1571
1571
  */
1572
1572
  AlchemySelect.setMethod(function advanceSelection(direction) {
1573
1573
 
@@ -1606,7 +1606,44 @@ AlchemySelect.setMethod(function advanceSelection(direction) {
1606
1606
  }
1607
1607
  }
1608
1608
  } else {
1609
- console.log('@TODO: Advance selection');
1609
+ // Focus is on the al-select element itself, not the type_area.
1610
+ // Move focus/highlight to the appropriate value item.
1611
+ let active_item = this.wrapper?.querySelector('al-select-item.active');
1612
+ let value_items = this.wrapper?.querySelectorAll('al-select-item.value');
1613
+
1614
+ if (!value_items || value_items.length === 0) {
1615
+ // No value items to navigate, focus the input
1616
+ this.type_area.focus();
1617
+ return;
1618
+ }
1619
+
1620
+ let items_array = Array.from(value_items);
1621
+ let current_index = active_item ? items_array.indexOf(active_item) : -1;
1622
+ let new_index;
1623
+
1624
+ if (current_index === -1) {
1625
+ // No active item yet, select first or last based on direction
1626
+ new_index = direction > 0 ? 0 : items_array.length - 1;
1627
+ } else {
1628
+ new_index = current_index + direction;
1629
+ }
1630
+
1631
+ // Remove active class from current item
1632
+ if (active_item) {
1633
+ active_item.classList.remove('active');
1634
+ }
1635
+
1636
+ // Check bounds
1637
+ if (new_index < 0 || new_index >= items_array.length) {
1638
+ // Moved past the edge, focus the input
1639
+ this.type_area.focus();
1640
+ return;
1641
+ }
1642
+
1643
+ // Set new active item
1644
+ let new_active = items_array[new_index];
1645
+ new_active.classList.add('active');
1646
+ new_active.focus();
1610
1647
  }
1611
1648
 
1612
1649
  });
@@ -379,10 +379,22 @@ VirtualScroll.setMethod(function getDomKeyRange() {
379
379
  *
380
380
  * @author Jelle De Loecker <jelle@elevenways.be>
381
381
  * @since 0.2.10
382
- * @version 0.2.10
382
+ * @version 0.3.0
383
383
  */
384
384
  VirtualScroll.setMethod(function handleScrollEvent() {
385
- // @TODO
385
+
386
+ // Throttle the culling operation to avoid performance issues
387
+ // during rapid scrolling
388
+ if (this._cull_scheduled) {
389
+ return;
390
+ }
391
+
392
+ this._cull_scheduled = true;
393
+
394
+ requestAnimationFrame(() => {
395
+ this._cull_scheduled = false;
396
+ this.cullInvisibleElements();
397
+ });
386
398
  });
387
399
 
388
400
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "alchemy-form",
3
3
  "description": "Form plugin for Alchemy",
4
- "version": "0.3.0-alpha.4",
4
+ "version": "0.3.0",
5
5
  "repository": {
6
6
  "type" : "git",
7
7
  "url" : "https://github.com/11ways/alchemy-form.git"
@@ -1,13 +1,13 @@
1
- <%
2
- if (value) {
3
- value = value.format('Y-m-d');
4
- }
5
- %>
6
- <input
7
- value=<% value %>
8
- class="alchemy-field-value"
9
- type="date"
10
- form=<% form_id %>
11
- name=<% path %>
12
- pattern="\d{4}-\d{2}-\d{2}"
13
- >
1
+ <al-datetime-input
2
+ datemode="date"
3
+ >
4
+ <% $0.setValue(value) %>
5
+ <input
6
+ value=<% value %>
7
+ class="alchemy-field-value"
8
+ type="date"
9
+ form=<% form_id %>
10
+ name=<% path %>
11
+ pattern="\d{4}-\d{2}-\d{2}"
12
+ >
13
+ </al-datetime-input>
@@ -1,20 +1,13 @@
1
- <%
2
-
3
- if (value) {
4
-
5
- // Make sure it's a date
6
- value = Date.create(value);
7
-
8
- // According to MDN `toISOString()` should work,
9
- // but neither Chrome or Firefox allow that format (it still contains timezone info)
10
- value = value.format('Y-m-d\\TH:i:s');
11
- }
12
- %>
13
- <input
14
- value=<% value %>
15
- class="alchemy-field-value"
16
- type="datetime-local"
17
- form=<% form_id %>
18
- name=<% path %>
19
- pattern="\d{4}-\d{2}-\d{2} \d{1,2}:\d{1,2}(?::\d{1,2})?"
20
- >
1
+ <al-datetime-input
2
+ datemode="datetime"
3
+ >
4
+ <% $0.setValue(value) %>
5
+ <input
6
+ class="alchemy-field-value"
7
+ type="datetime-local"
8
+ form=<% form_id %>
9
+ name=<% path %>
10
+ pattern="\d{4}-\d{2}-\d{2} \d{1,2}:\d{1,2}(?::\d{1,2})?"
11
+ value=""
12
+ >
13
+ </al-datetime-input>