Files
rspade_system/docs/skills/modals/SKILL.md
root 1b46c5270c Add skills documentation and misc updates
Add form value persistence across cache revalidation re-renders

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 04:38:06 +00:00

255 lines
7.3 KiB
Markdown
Executable File

---
name: modals
description: Creating modal dialogs in RSX including alerts, confirms, prompts, selects, and form modals. Use when implementing Modal.alert, Modal.confirm, Modal.prompt, Modal.form, creating modal classes extending Modal_Abstract, or building dialog-based user interactions.
---
# RSX Modal System
## Built-in Dialog Types
All modal methods are async and return appropriate values:
| Method | Returns | Description |
|--------|---------|-------------|
| `Modal.alert(body)` | `void` | Simple notification |
| `Modal.alert(title, body, buttonLabel?)` | `void` | Alert with title |
| `Modal.confirm(body)` | `boolean` | Yes/no confirmation |
| `Modal.confirm(title, body, confirmLabel?, cancelLabel?)` | `boolean` | Confirmation with labels |
| `Modal.prompt(body)` | `string\|false` | Text input |
| `Modal.prompt(title, body, default?, multiline?)` | `string\|false` | Prompt with options |
| `Modal.select(body, options)` | `string\|false` | Dropdown selection |
| `Modal.select(title, body, options, default?, placeholder?)` | `string\|false` | Select with options |
| `Modal.error(error, title?)` | `void` | Error with red styling |
| `Modal.unclosable(title, body)` | `void` | Modal user cannot close |
## Basic Usage Examples
```javascript
// Simple alert
await Modal.alert("File saved successfully");
// Alert with title
await Modal.alert("Success", "Your changes have been saved.");
// Confirmation
if (await Modal.confirm("Are you sure you want to delete this item?")) {
await Controller.delete(id);
}
// Confirmation with custom labels
const confirmed = await Modal.confirm(
"Delete Project",
"This will permanently delete the project.\n\nThis action cannot be undone.",
"Delete", // confirm button label
"Keep Project" // cancel button label
);
// Text prompt
const name = await Modal.prompt("Enter your name:");
if (name) {
// User entered something
}
// Multiline prompt
const notes = await Modal.prompt("Notes", "Enter description:", "", true);
// Selection dropdown
const choice = await Modal.select("Choose an option:", [
{value: 'a', label: 'Option A'},
{value: 'b', label: 'Option B'}
]);
// Unclosable modal (for critical operations)
Modal.unclosable("Processing", "Please wait...");
await long_operation();
await Modal.close(); // Must close programmatically
```
**Text formatting**: Use `\n\n` for paragraph breaks in modal body text.
---
## Form Modals
For complex data entry, use `Modal.form()`:
```javascript
const result = await Modal.form({
title: "Edit User",
component: "User_Form", // Component name (must implement vals())
component_args: {data: user}, // Args passed to component
max_width: 800, // Width in pixels (default: 800)
on_submit: async (form) => {
const response = await User_Controller.save(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false; // Keep modal open
}
return response.data; // Close modal and return data
}
});
if (result) {
// Modal closed with data
console.log(result.id);
}
```
### Form Component Requirements
The component used in `Modal.form()` must:
1. Implement `vals()` method (get/set form values)
2. Include `<div $sid="error_container"></div>` for validation errors
```javascript
class User_Form extends Component {
vals(values) {
if (values) {
this.$sid('name').val(values.name || '');
this.$sid('email').val(values.email || '');
return null;
} else {
return {
name: this.$sid('name').val(),
email: this.$sid('email').val()
};
}
}
}
```
---
## Modal Classes (Reusable Modals)
For complex or frequently-used modals, create dedicated classes:
```javascript
class Add_User_Modal extends Modal_Abstract {
static async show(initial_data = {}) {
return await Modal.form({
title: 'Add User',
component: 'User_Form',
component_args: {data: initial_data},
on_submit: async (form) => {
const response = await User_Controller.create(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
}) || false;
}
}
class Edit_User_Modal extends Modal_Abstract {
static async show(user_id) {
// Load data first
const user = await User_Model.fetch(user_id);
return await Modal.form({
title: 'Edit User',
component: 'User_Form',
component_args: {data: user},
on_submit: async (form) => {
const response = await User_Controller.update(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
}) || false;
}
}
```
### Usage Pattern
```javascript
// Create new user
const new_user = await Add_User_Modal.show();
if (new_user) {
grid.reload();
}
// Edit existing user
const updated_user = await Edit_User_Modal.show(user_id);
if (updated_user) {
// Refresh display
}
// Chain modals (page JS orchestrates, not modals)
const user = await Add_User_Modal.show();
if (user) {
await Assign_Role_Modal.show(user.id);
}
```
**Pattern**: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
---
## Modal Options
Options for `Modal.form()`:
```javascript
await Modal.form({
title: "Form Title",
component: "Form_Component",
component_args: {},
max_width: 800, // Width in pixels (default: 800)
closable: true, // Allow ESC/backdrop/X to close (default: true)
submit_label: "Save", // Submit button text
cancel_label: "Cancel", // Cancel button text
on_submit: async (form) => { /* ... */ }
});
```
Options for `Modal.show()` (custom modals):
```javascript
await Modal.show({
title: "Choose Action",
body: "What would you like to do?", // String, HTML, or jQuery element
max_width: 500, // Width in pixels
closable: true,
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Continue", value: true, class: "btn-primary", default: true}
]
});
```
---
## Modal Queuing
Multiple simultaneous modal requests are queued and shown sequentially:
```javascript
// All three modals queued and shown one after another
const p1 = Modal.alert("First");
const p2 = Modal.alert("Second");
const p3 = Modal.alert("Third");
await Promise.all([p1, p2, p3]);
```
Backdrop persists across queued modals with 500ms delay between.
---
## Best Practices
1. **Use appropriate type**: `alert()` for info, `confirm()` for decisions, `form()` for complex input
2. **Handle cancellations**: Always check for `false` return value
3. **Modal classes don't chain**: Page JS orchestrates sequences, not modal classes
4. **No UI updates in modals**: Page JS handles post-modal UI updates
5. **Loading states**: Use `Modal.unclosable()` + `Modal.close()` for long operations
## More Information
Details: `php artisan rsx:man modals`