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 +17 -1
- package/CLAUDE.md +348 -0
- package/assets/stylesheets/form/elements/_datetime.scss +6 -0
- package/assets/stylesheets/form/elements/index.scss +1 -0
- package/element/al_datetime_input.js +127 -0
- package/element/al_field.js +55 -7
- package/element/al_select.js +39 -2
- package/element/al_virtual_scroll.js +14 -2
- package/package.json +1 -1
- package/view/form/inputs/edit/date.hwk +13 -13
- package/view/form/inputs/edit/datetime.hwk +13 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
## 0.3.0-
|
|
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,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
|
+
});
|
package/element/al_field.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
700
|
-
|
|
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,
|
package/element/al_select.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
382
|
+
* @version 0.3.0
|
|
383
383
|
*/
|
|
384
384
|
VirtualScroll.setMethod(function handleScrollEvent() {
|
|
385
|
-
|
|
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,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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>
|