Skip to content
Open

2.0 #11

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7e4ff96
update package.json
lenadax Jan 31, 2025
9a916a3
Update overlay button styles to bootstrap5
lenadax Jan 31, 2025
95112a9
Overlay rework
lenadax Feb 3, 2025
524e17e
remove redundant css
lenadax Feb 6, 2025
af2e916
WIP/test: prevent ts.ajax bound elements from persisting in cache as …
lenadax Feb 14, 2025
7491815
compiled js
lenadax Feb 14, 2025
3411ff9
WIP: memory
lenadax Feb 21, 2025
20d1bf8
update logic
lenadax Feb 21, 2025
14b0ccb
replace loading-spokes with bootstrap5 spinner
lenadax Feb 21, 2025
d3e32c4
destroy widgets within overlay using AjaxDestroy. Moving AjaxDestroy …
lenadax Feb 24, 2025
11d9e1c
create modal backdrop
lenadax Feb 24, 2025
dbb357f
better z-index logic for modals
lenadax Feb 24, 2025
867208f
Try to fix corepack issue in CI
rnixx Feb 24, 2025
39e67aa
minor adjustments
lenadax Feb 24, 2025
0b11d4d
remove redundant check
lenadax Feb 25, 2025
fc1b254
update deprecated unescape method. some cleanup
lenadax Mar 3, 2025
693d158
handle memory management for create_listener, Motion and Spinner
lenadax Mar 6, 2025
f620a3d
update changes
lenadax Mar 6, 2025
d77de24
attach changeListener to ajax (to properly destroy event listener)
lenadax Mar 13, 2025
bc59146
Provide pluggable AjaxDestroy callbacks
lenadax Mar 14, 2025
5afe7e5
WIP: move ajax related code to srr/ folder
lenadax Mar 17, 2025
ee815a5
update changelog
lenadax Mar 18, 2025
f25197d
Some cleanup
rnixx Mar 18, 2025
ceb9546
global callback registration for AjaxDestroy parser
lenadax Mar 18, 2025
b5335d6
remove obsolete destroy logic
lenadax Mar 18, 2025
39f69fd
change order of jquery methods
lenadax Mar 18, 2025
b0a964b
rename deregister to unregister
lenadax Mar 18, 2025
3184ce1
update changelog
lenadax Mar 18, 2025
4dd5aef
cleanup
lenadax Mar 18, 2025
ac0e632
fix test case
lenadax Mar 18, 2025
87d3861
Merge pull request #8 from conestack/memory
rnixx Mar 18, 2025
26ce8a4
allow passing options for selected_class and unselected_class for ts.…
lenadax Jun 10, 2025
c8cd0dc
descriptive error message for set_svg_attrs: failed without stack trace
lenadax Jun 20, 2025
7fbcec2
allow flavor and warning for overlay
lenadax Jul 2, 2025
d0b3132
Collapsible collapsed returns correct boolean with bs5.
lenadax Oct 30, 2025
1661bf6
Merge branch 'master' into bs5
rnixx Dec 1, 2025
6fc593d
merge
rnixx Dec 4, 2025
5c0324d
Merge branch 'master' into bs5
rnixx Dec 4, 2025
0af1f48
Merge branch 'master' into bs5
rnixx Dec 4, 2025
09f4fa2
err
rnixx Dec 4, 2025
89e68b7
version
rnixx Dec 4, 2025
80c07b2
upgrade to jquery 4.0.0
lenadax Jan 23, 2026
11fd708
fix js tests
lenadax Jan 23, 2026
7eb2e60
allow css on AjaxHandle
lenadax Jan 26, 2026
92049d6
allow title on AjaxHandle
lenadax Jan 27, 2026
da6f002
merge master
rnixx Feb 3, 2026
4451e4e
Test coverage
rnixx Feb 8, 2026
5b7e15a
API docs
rnixx Feb 8, 2026
fb0d0c7
Update docs
rnixx Feb 8, 2026
fba28b4
add biome linter
rnixx Feb 8, 2026
77da477
Add LLM context instructions for developing with treibstoff
rnixx Feb 9, 2026
f6d6352
Improve docs
rnixx Feb 9, 2026
91e1abb
Add DnD
rnixx Feb 9, 2026
967ccb1
Include dnd in bundle
rnixx Feb 9, 2026
a9891d0
linting
rnixx Feb 9, 2026
2a5fe67
explicit js import
lenadax Apr 20, 2026
0171f13
Import node payloads owned by a foreign document in ``AjaxHandle.upda…
rnixx Jul 22, 2026
10f1090
Update TODO
rnixx Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .ai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Treibstoff — LLM Instruction Files

This directory contains self-contained instruction files for LLM-assisted
development with treibstoff. Each file can be used as prompt input without
needing additional context.

## Usage

Copy the content of the relevant file(s) into your LLM prompt to provide
context for code generation, review, or explanation tasks.

For general understanding, start with `overview.md`. For specific tasks,
pick the topic file that matches your need.

## Files

| File | Description |
|------|-------------|
| [overview.md](overview.md) | Architecture overview — module map, inheritance hierarchy, all public API members |
| [create-widget.md](create-widget.md) | How to create custom widgets — Widget, HTMLWidget, SVGContext, lifecycle |
| [property-binding.md](property-binding.md) | Reactive property system — all 9 property types, auto-handlers, cascading |
| [event-handling.md](event-handling.md) | Events, listeners, keyboard state — on/off/trigger, create_listener, KeyState |
| [drag-and-drop.md](drag-and-drop.md) | Native HTML5 DnD — DnD class, cross-instance coordination, evt.source |
| [motion-tracking.md](motion-tracking.md) | Drag, resize, selection — Motion class, scope variants, down/move/up |
| [template-parsing.md](template-parsing.md) | Template compilation — compile_template, t-elem, t-prop, t-val, t-type |
| [ssr-integration.md](ssr-integration.md) | SSR via HTML attributes — all ajax:* attributes, 7 patterns |
| [ssr-programmatic.md](ssr-programmatic.md) | Programmatic Ajax API — ajax.action, ajax.trigger, ajax.overlay, ajax.register |
| [build-forms.md](build-forms.md) | Form building — Form, FormInput, FormField, FormSelect, validation |
| [overlays-dialogs.md](overlays-dialogs.md) | Overlays and dialogs — Overlay, Dialog, Message, show_dialog, show_error |
| [svg-graphics.md](svg-graphics.md) | SVG graphics — SVGContext, svg_elem, svg_attrs, two-layer pattern |
| [http-requests.md](http-requests.md) | HTTP requests — http_request, HTTPRequest, spinner, error handling |
| [websocket-realtime.md](websocket-realtime.md) | WebSocket — Websocket class, events, JSON messaging, heartbeat |
| [testing.md](testing.md) | Writing QUnit tests — test patterns, DOM fixtures, mocks, assertions |
351 changes: 351 additions & 0 deletions .ai/build-forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
# Building Forms

This guide explains how to build forms with treibstoff's form system, including
validation, remote data fetching, and Ajax submission.

## Context

Treibstoff provides a form abstraction layer on top of DOM form elements. The
`Form` class manages form-level operations, while `FormInput`, `FormField`,
`FormSelect`, `FormCheckbox`, and `FormRemoteSelect` wrap individual elements
with reactive getters/setters and event handling.

## Key API

| Class/Function | Purpose |
|----------------|---------|
| `ts.Form` | Form container — initialize, lookup, field visibility |
| `ts.FormInput` | Input wrapper — value, disabled state |
| `ts.FormField` | Field wrapper — visibility, error state, reset |
| `ts.FormCheckbox` | Checkbox wrapper — checked state |
| `ts.FormSelect` | Select wrapper — options, clear |
| `ts.FormRemoteSelect` | Select with server-side option fetching |
| `ts.lookup_form_elem` | Find form element by naming convention |

## Naming Convention

Form elements are found by ID following this pattern:

- Form: `#form-{name}`
- Input: `#input-{form.name}-{field.name}`
- Field: `#field-{form.name}-{field.name}`

```html
<div id="form-user">
<div id="field-user-email" class="form-group">
<label>Email</label>
<input id="input-user-email" type="email">
</div>
<div id="field-user-role" class="form-group">
<label>Role</label>
<select id="input-user-role">
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
</div>
```

## Pattern 1: Basic Form with Fields

```javascript
import ts from 'treibstoff';

class UserForm extends ts.Form {
constructor(opts) {
super(opts);
this.email = new ts.FormField({
form: this,
name: 'email',
input: ts.FormInput
});
this.role = new ts.FormField({
form: this,
name: 'role',
input: ts.FormSelect
});
}
}

// Initialize from DOM context (e.g. in an ajax.register callback)
ts.Form.initialize($('#content'), UserForm, 'user');

// Later, look up the form instance
let form = ts.Form.instance('user');
form.email.input.value = 'user@example.com';
```

## Pattern 2: Checkbox Fields

```html
<div id="form-settings">
<div id="field-settings-notifications">
<input id="input-settings-notifications" type="checkbox">
<label>Enable notifications</label>
</div>
</div>
```

```javascript
class SettingsForm extends ts.Form {
constructor(opts) {
super(opts);
this.notifications = new ts.FormField({
form: this,
name: 'notifications',
input: ts.FormCheckbox
});
}
}

// Read/set checkbox state
let form = ts.Form.instance('settings');
form.notifications.input.checked = true;
let isChecked = form.notifications.input.checked;
```

## Pattern 3: Remote Select (Server-Fetched Options)

```javascript
class ProjectForm extends ts.Form {
constructor(opts) {
super(opts);
this.category = new ts.FormField({
form: this,
name: 'category',
input: new ts.FormRemoteSelect({
form: this,
name: 'category',
vocab: '/api/categories.json'
})
});
}

load_categories(filter) {
// Fetches JSON from /api/categories.json?q=filter
// Server must return array of [value, label] pairs
this.category.input.fetch({q: filter});
}
}
```

The server must return a JSON array of `[value, label]` pairs:
```json
[["cat1", "Category 1"], ["cat2", "Category 2"]]
```

## Pattern 4: Select with Programmatic Options

```javascript
let form = ts.Form.instance('project');
// Set options (array of [value, label] pairs or Option objects)
form.category.input.options = [
['opt1', 'Option 1'],
['opt2', 'Option 2']
];

// Clear all options
form.category.input.clear();

// Read current value
let selected = form.category.input.value;
```

## Pattern 5: Field Visibility and Error State

```javascript
let form = ts.Form.instance('user');

// Hide a field
form.email.visible = false;
form.email.hidden = true; // equivalent

// Show a field
form.email.visible = true;

// Bulk visibility
form.set_field_visibility([form.email, form.role], false);

// Error state
form.email.has_error = true; // adds 'has-error' class
form.email.has_error = false; // removes it

// Reset field (clear value, remove error, remove help text)
form.email.reset();
form.email.reset('default@example.com'); // reset with default value
```

## Pattern 6: Change Events

`FormSelect` and `FormCheckbox` use the `changeListener` mixin, which triggers
`on_change` when the user interacts with the element.

```javascript
class FilterForm extends ts.Form {
constructor(opts) {
super(opts);
this.status = new ts.FormField({
form: this,
name: 'status',
input: ts.FormSelect
});
// Listen for selection changes
this.status.input.on('on_change', function(inst, evt) {
console.log('Status changed to:', inst.value);
});
}
}
```

## Pattern 7: Template-Based Form Inputs

For forms built with `compile_template`, use `t-prop` attributes to create
`InputProperty` bindings:

```javascript
class InlineForm extends ts.Widget {
constructor(opts) {
super({parent: opts.parent});
ts.compile_template(this, `
<div class="inline-form">
<input t-elem="name_input" t-prop="name" t-val="Default">
<input t-elem="count_input" t-prop="count"
t-type="number" t-val="0">
</div>
`, opts.container);
// this.name is now an InputProperty
// this.count is an InputProperty with number extraction
}

on_name(val) {
console.log('Name changed to:', val);
}

on_count(val) {
console.log('Count changed to:', val); // val is a number
}
}
```

## Pattern 8: Ajax Form Submission

Forms marked with `class="ajax"` or `ajax:form="true"` are submitted via a
hidden iframe. The server response calls `ts.ajax.form()` to update the DOM.

```html
<form id="form-user" class="ajax"
action="/users/save" method="POST">
<div id="field-user-name">
<input id="input-user-name" name="name">
</div>
<button type="submit">Save</button>
</form>
```

The server-side handler processes the form and returns an HTML page in the
iframe that calls:

```html
<script>
parent.ts.ajax.form({
payload: '...new form HTML...',
selector: '#form-user',
mode: 'replace',
next: [
{type: 'message', payload: 'Saved!', flavor: 'info'}
],
error: false
});
</script>
```

## Complete Example

```html
<div id="form-task">
<div id="field-task-title" class="form-group">
<label>Title</label>
<input id="input-task-title" type="text">
</div>
<div id="field-task-priority" class="form-group">
<label>Priority</label>
<select id="input-task-priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div id="field-task-done" class="form-group">
<input id="input-task-done" type="checkbox">
<label>Done</label>
</div>
</div>
```

```javascript
class TaskForm extends ts.Form {
constructor(opts) {
super(opts);
this.title = new ts.FormField({
form: this, name: 'title', input: ts.FormInput
});
this.priority = new ts.FormField({
form: this, name: 'priority', input: ts.FormSelect
});
this.done = new ts.FormField({
form: this, name: 'done', input: ts.FormCheckbox
});

this.priority.input.on('on_change', this.on_priority_change.bind(this));
}

on_priority_change(inst, evt) {
if (inst.value === 'high') {
this.title.elem.addClass('text-danger');
} else {
this.title.elem.removeClass('text-danger');
}
}

validate() {
let valid = true;
if (!this.title.input.value.trim()) {
this.title.has_error = true;
valid = false;
}
return valid;
}

reset_all() {
this.title.reset();
this.priority.input.value = 'medium';
this.done.input.checked = false;
}
}

$(function() {
ts.ajax.register(function(context) {
ts.Form.initialize(context, TaskForm, 'task');
}, true);
});
```

## Pitfalls

1. **Element IDs must follow the naming convention** (`#form-{name}`,
`#input-{form}-{field}`, `#field-{form}-{field}`). If elements aren't found,
pass them explicitly via `opts.elem`.

2. **`FormField` wraps both the field container and the input.** Access the
input via `field.input`. The field itself provides visibility and error state.

3. **`FormField` accepts an input class or instance.** Pass `ts.FormInput` (class)
and it creates the instance. Pass `new ts.FormRemoteSelect(...)` (instance)
for pre-configured inputs.

4. **`FormRemoteSelect.fetch()` is async.** The select options are populated
when the HTTP request completes.

5. **`Form.initialize()` silently returns if the form element is not found**
in the given context. This is by design — forms may not be present on
every page.
Loading
Loading