Enhance refactor commands with controller-aware Route() updates and fix code quality violations

Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 06:21:56 +00:00
parent e678b987c2
commit f6ac36c632
5683 changed files with 5854736 additions and 22329 deletions

View File

@@ -273,7 +273,7 @@ CALLING API METHODS
ROUTE RESOLUTION
PHP:
$url = Rsx::Route('User_Controller', 'show')->url(['id' => 5]);
$url = Rsx::Route('User_Controller', 'show', ['id' => 5]);
// Returns: "/users/5"
if (Rsx::Route('User_Controller')->is_current()) {

View File

@@ -0,0 +1,582 @@
FORMS_AND_WIDGETS(3) RSX Framework Manual FORMS_AND_WIDGETS(3)
NAME
Forms and Widgets - RSX form system with reusable widget components
SYNOPSIS
// Blade form markup
<Rsx_Form $data="{{ json_encode($form_data) }}"
$action="{{ Rsx::Route('Controller', 'save') }}">
<Form_Field $name="email" $label="Email Address" $required=true>
<Text_Input $type="email" $placeholder="user@example.com" />
</Form_Field>
<Form_Field $name="bio" $label="Biography">
<Text_Input $type="textarea" $rows=5 />
</Form_Field>
<button type="button" id="save-btn">Save</button>
</Rsx_Form>
// JavaScript - wire save button to form
$('#save-btn').on('click', function() {
const $form = $('.Rsx_Form').first();
$form.component().submit();
});
DESCRIPTION
The RSX form system provides a clean separation between form structure
(Rsx_Form), field layout (Form_Field), and input widgets (Text_Input,
Select_Input, etc). This architecture enables:
- Reusable widgets across all forms
- Consistent validation error display
- Automatic value collection and population
- Test data generation via seeders
- Read-only/disabled states
- Custom field layouts without modifying widget code
Key Components:
- Rsx_Form: Container managing form submission and validation
- Form_Field: Layout wrapper providing labels, help text, error display
- Widgets: Reusable input components (Text_Input, Select_Input, etc)
RSX_FORM COMPONENT
The Rsx_Form component manages form data flow, submission, and validation.
Required Attributes:
$action - Controller method reference for form submission
Example: Frontend_Clients_Controller.save
Optional Attributes:
$data - JSON-encoded object with initial form values
Used for edit mode to populate fields
Example: $data="{{ json_encode($client_data) }}"
Methods:
vals() - Get all form values as object
vals(values) - Set all form values from object
submit() - Submit form to $action endpoint
seed() - Fill all fields with test data (debug mode only)
Form Discovery:
Rsx_Form automatically discovers all widgets using shallowFind('.Widget')
and collects values based on their data-name attributes. No registration
or manual wiring required.
Example - Basic Form:
<Rsx_Form $action="{{ Rsx::Route('Users_Controller', 'save') }}">
<Form_Field $name="first_name" $label="First Name">
<Text_Input />
</Form_Field>
<Form_Field $name="last_name" $label="Last Name">
<Text_Input />
</Form_Field>
<button type="button" id="save-btn">Save</button>
</Rsx_Form>
Example - Edit Mode with Initial Data:
@php
$form_data = [
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
];
@endphp
<Rsx_Form $data="{{ json_encode($form_data) }}"
$action="{{ Rsx::Route('Users_Controller', 'save') }}">
<!-- Fields automatically populated from $data -->
</Rsx_Form>
FORM_FIELD WRAPPER
Form_Field provides consistent layout for labels, help text, and error
display. It wraps a single widget and connects it to the form.
Required Attributes:
$name - Field name for form serialization and error display
Optional Attributes:
$label - Label text displayed above field
$required - Boolean, adds red asterisk to label
$help - Help text displayed below field
Responsibilities:
- Display label with optional required indicator
- Set data-name attribute on child widget
- Display validation errors returned from server
- Provide consistent spacing and styling
Example - Basic Field:
<Form_Field $name="email" $label="Email Address">
<Text_Input $type="email" />
</Form_Field>
Example - Required Field with Help Text:
<Form_Field $name="password"
$label="Password"
$required=true
$help="Must be at least 8 characters">
<Text_Input $type="password" />
</Form_Field>
Example - Field with HTML in Label:
<Form_Field $name="twitter" $label="<i class='bi bi-twitter'></i> Twitter">
<Text_Input $prefix="@" />
</Form_Field>
Custom Layout:
Form_Field can be extended or replaced with custom jqhtml to change
field layout. The only requirement is that the child widget must
have the data-name attribute set to the field name.
Example - Horizontal Layout:
<Define:Form_Field_Horizontal extends="Form_Field">
<div class="row mb-3">
<label class="col-md-3 col-form-label">
<%!= this.args.label %>
</label>
<div class="col-md-9">
<%= content() %>
<% if (this.has_error()) { %>
<div class="invalid-feedback d-block">
<%= this.get_error() %>
</div>
<% } %>
</div>
</div>
</Define:Form_Field_Horizontal>
WIDGET INTERFACE
All form widgets must implement the standard widget interface:
Required:
- CSS class "Widget" on root element
- val() method for getting current value
- val(value) method for setting value
Optional:
- seed() method for generating test data
- Support for $disabled attribute
Widget Responsibilities:
1. Value Management
Widgets must implement getter/setter via val() method:
val() {
// Getter - return current value
if (arguments.length === 0) {
return this.$id('input').val();
}
// Setter - update value
else {
this.data.value = value || '';
this.$id('input').val(this.data.value);
}
}
2. Disabled State
Widgets should respect $disabled attribute:
- Render with disabled HTML attribute
- Display grayed-out appearance
- Still return value via val() getter
- Do not submit in HTML form (handled by browser)
3. Test Data (Optional)
Widgets may implement seed() for debug mode:
async seed() {
if (this.args.seeder) {
// Generate test data
this.val('Test Value');
}
}
BUILT-IN WIDGETS
Text_Input
Basic text input supporting multiple types and textarea.
Attributes:
$type - Input type (text, email, url, tel, number, textarea)
$rows - Number of rows for textarea (default: 3)
$placeholder - Placeholder text
$prefix - Text to prepend (creates input-group)
$suffix - Text to append (creates input-group)
$min - Minimum value for number inputs
$max - Maximum value for number inputs
$maxlength - Maximum length for text inputs
$disabled - Disable input (grayed out, still returns value)
$seeder - Seeder function name for test data
Examples:
<Text_Input $type="email" $placeholder="user@example.com" />
<Text_Input $type="textarea" $rows=5 $placeholder="Enter bio..." />
<Text_Input $type="number" $min=0 $max=100 />
<Text_Input $prefix="@" $placeholder="username" />
<Text_Input $type="url" $disabled=true />
Select_Input
Dropdown select with options.
Attributes:
$options - Array of options (see below)
$placeholder - Placeholder option text
$disabled - Disable select (grayed out, still returns value)
$seeder - Seeder function name for test data
Options Format:
Simple array: ['Option 1', 'Option 2', 'Option 3']
Object array: [
{value: 'opt1', label: 'Option 1'},
{value: 'opt2', label: 'Option 2'}
]
From Blade: $options="{{ json_encode($options_array) }}"
Examples:
@php
$industries = ['Technology', 'Finance', 'Healthcare'];
@endphp
<Select_Input $options="{{ json_encode($industries) }}"
$placeholder="Select Industry..." />
@php
$sizes = [
['value' => 'sm', 'label' => 'Small (1-10)'],
['value' => 'md', 'label' => 'Medium (11-50)'],
['value' => 'lg', 'label' => 'Large (50+)'],
];
@endphp
<Select_Input $options="{{ json_encode($sizes) }}" />
Checkbox_Input
Checkbox with optional label.
Attributes:
$label - Label text displayed next to checkbox
$checked_value - Value when checked (default: "1")
$unchecked_value - Value when unchecked (default: "0")
$disabled - Disable checkbox (grayed out, still returns value)
Examples:
<Checkbox_Input $label="Subscribe to newsletter" />
<Checkbox_Input $label="I agree to terms"
$checked_value="yes"
$unchecked_value="no" />
Wysiwyg_Input
Rich text editor using Quill.
Attributes:
$placeholder - Placeholder text
$disabled - Disable editor (not yet implemented)
$seeder - Seeder function name for test data
Example:
<Wysiwyg_Input $placeholder="Enter description..." />
SEEDING TEST DATA
The seed system generates realistic test data for forms during development.
Enabled only when window.rsxapp.debug is true.
Seed Button:
Rsx_Form automatically displays a "Fill Test Data" button in debug mode.
Clicking this button calls seed() on all widgets.
Widget Seeding:
Widgets implement seed() to generate appropriate test data:
async seed() {
if (this.args.seeder) {
// TODO: Implement Rsx_Random_Values endpoint
let value = 'Test ' + (this.args.seeder || 'Value');
this.val(value);
}
}
Seeder Names:
Specify seeder via $seeder attribute:
<Text_Input $seeder="company_name" />
<Text_Input $seeder="email" />
<Text_Input $seeder="phone" />
Future Implementation:
Planned Rsx_Random_Values endpoint will provide:
- company_name() - Random company names
- email() - Random email addresses
- phone() - Random phone numbers
- first_name() - Random first names
- last_name() - Random last names
- address() - Random street addresses
- city() - Random city names
DISABLED STATE
Disabled widgets display as read-only but still participate in form
value collection.
Behavior:
- Widget displays grayed-out appearance
- User cannot interact with widget
- val() getter still returns current value
- Value included in form submission via Ajax
- HTML disabled attribute prevents browser form submission
Example - Disable Individual Fields:
<Form_Field $name="email" $label="Email (Cannot Edit)">
<Text_Input $type="email" $disabled=true />
</Form_Field>
Example - Conditional Disable:
<Form_Field $name="status" $label="Status">
<Select_Input $options="{{ json_encode($statuses) }}"
$disabled="{{ !$can_edit_status }}" />
</Form_Field>
Use Cases:
- Display data that cannot be edited
- Show calculated or system-managed values
- Enforce permissions (some users see but cannot edit)
- Multi-step forms (disable completed steps)
FORM SUBMISSION
Form submission uses Ajax to send data to controller methods.
JavaScript Submission:
$('#save-btn').on('click', function() {
const $form = $('.Rsx_Form').first();
const form_component = $form.component();
form_component.submit();
});
What Happens:
1. Form calls vals() to collect all widget values
2. Sends values to this.args.action via Ajax.call()
3. Handles response:
- Success: Redirect if response.redirect provided
- Validation errors: Display errors on fields
- General errors: Log to console
Controller Method:
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
// Validation
$validated = $request->validate([
'email' => 'required|email',
'name' => 'required|string|max:255',
]);
// Save data
$user = User::create($validated);
// Return response
return [
'success' => true,
'redirect' => Rsx::Route('Users_Controller', 'view', $user->id),
];
}
Validation Errors:
When validation fails, return errors in format:
return [
'success' => false,
'errors' => [
'email' => 'The email field is required.',
'name' => 'The name field is required.',
],
];
Form automatically displays errors below each field.
MULTI-COLUMN LAYOUTS
Use Bootstrap grid for multi-column field layouts:
<div class="row">
<div class="col-md-6">
<Form_Field $name="first_name" $label="First Name">
<Text_Input />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $name="last_name" $label="Last Name">
<Text_Input />
</Form_Field>
</div>
</div>
<div class="row">
<div class="col-md-4">
<Form_Field $name="city" $label="City">
<Text_Input />
</Form_Field>
</div>
<div class="col-md-2">
<Form_Field $name="state" $label="State">
<Text_Input $maxlength=2 />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $name="zip" $label="ZIP Code">
<Text_Input />
</Form_Field>
</div>
</div>
CREATING CUSTOM WIDGETS
Create custom widgets by implementing the widget interface.
Example - Rating Widget:
File: rating_input.jqhtml
<Define:Rating_Input class="Widget">
<div class="rating">
<% for (let i = 1; i <= 5; i++) { %>
<i $id="star_<%= i %>"
class="bi bi-star<%= this.data.value >= i ? '-fill' : '' %>"
data-rating="<%= i %>"></i>
<% } %>
</div>
</Define:Rating_Input>
File: rating_input.js
class Rating_Input extends Form_Input_Abstract {
on_create() {
this.data.value = 0;
}
on_ready() {
const that = this;
this.$.find('[data-rating]').on('click', function() {
that.val($(this).data('rating'));
});
}
val(value) {
if (arguments.length === 0) {
return this.data.value;
} else {
this.data.value = value || 0;
// Update star display
this.$.find('[data-rating]').each(function() {
const rating = $(this).data('rating');
$(this).toggleClass('bi-star-fill', rating <= value);
$(this).toggleClass('bi-star', rating > value);
});
}
}
async seed() {
this.val(Math.floor(Math.random() * 5) + 1);
}
}
Usage:
<Form_Field $name="satisfaction" $label="Rate Your Experience">
<Rating_Input />
</Form_Field>
EXAMPLES
Complete Form Example:
@php
$form_data = isset($client) ? [
'name' => $client->name,
'email' => $client->email,
'industry' => $client->industry,
'active' => $client->active,
] : [];
$industries = ['Technology', 'Finance', 'Healthcare'];
@endphp
<Rsx_Form $data="{{ json_encode($form_data) }}"
$action="{{ Rsx::Route('Clients_Controller', 'save') }}">
@if (isset($client))
<input type="hidden" name="id" value="{{ $client->id }}">
@endif
<Form_Field $name="name" $label="Company Name" $required=true>
<Text_Input $seeder="company_name" />
</Form_Field>
<div class="row">
<div class="col-md-6">
<Form_Field $name="email" $label="Email">
<Text_Input $type="email" $seeder="email" />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $name="phone" $label="Phone">
<Text_Input $type="tel" $seeder="phone" />
</Form_Field>
</div>
</div>
<Form_Field $name="industry" $label="Industry">
<Select_Input $options="{{ json_encode($industries) }}"
$placeholder="Select Industry..." />
</Form_Field>
<Form_Field $name="notes" $label="Notes">
<Text_Input $type="textarea" $rows=5 />
</Form_Field>
<Form_Field $name="active" $label="&nbsp;">
<Checkbox_Input $label="Active Client" />
</Form_Field>
<button type="button" class="btn btn-primary" id="save-btn">
Save Client
</button>
</Rsx_Form>
<script>
$('#save-btn').on('click', function() {
$('.Rsx_Form').first().component().submit();
});
</script>
SEE ALSO
jqhtml(3), ajax(3), validation(3)
RSX Framework October 2025 FORMS_AND_WIDGETS(3)

View File

@@ -92,7 +92,7 @@ TEMPLATE SYNTAX
Template expressions:
<%= expression %> - Escaped HTML output (safe, default)
<%== expression %> - Unescaped raw output (pre-sanitized content only)
<%!= expression %> - Unescaped raw output (pre-sanitized content only)
<% statement; %> - JavaScript statements (loops, conditionals)
Attributes:
@@ -103,6 +103,18 @@ TEMPLATE SYNTAX
data-attr="value" - HTML data attributes
class="my-class" - CSS classes (merged with component name)
Conditional Attributes (v2.2.162+):
Place if statements directly in attribute context to conditionally apply
attributes based on component arguments:
<input type="text"
<% if (this.args.required) { %>required="required"<% } %>
<% if (this.args.min !== undefined) { %>min="<%= this.args.min %>"<% } %> />
Works with static values, interpolated expressions, and multiple conditionals
per element. Compiles to Object.assign() with ternary operators at build time.
No nested conditionals or else clauses supported - use separate if statements.
DEFINE TAG CONFIGURATION
The <Define> tag supports three types of attributes:
@@ -350,32 +362,35 @@ CONTROL FLOW AND LOOPS
COMPONENT LIFECYCLE
Five-stage deterministic lifecycle:
render → on_render → on_create → on_load → on_ready
on_create → render → on_render → on_load → on_ready
1. render (automatic, top-down)
1. on_create() (synchronous, runs BEFORE first render)
- Setup default state BEFORE template executes
- Initialize this.data properties so template can reference them
- Must be synchronous (no async/await)
- Perfect for abstract component base classes
- Example: this.data.rows = this.data.rows || [];
2. render (automatic, top-down)
- Template executes, DOM created
- First render: this.data = {} (empty object)
- First render: can safely reference properties set in on_create()
- Parent completes before children
- Not overridable
2. on_render() (top-down)
3. on_render() (top-down)
- Fires immediately after render, BEFORE children ready
- Hide uninitialized elements
- Set initial visual state
- Prevents flash of uninitialized content
- Parent completes before children
3. on_create() (bottom-up)
- Quick synchronous setup
- Set instance properties
- Children complete before parent
4. on_load() (bottom-up, siblings in parallel)
4. on_load() (bottom-up, siblings in parallel, CAN be async)
- Load async data
- ONLY modify this.data - NEVER DOM
- NO child component access
- Siblings at same depth execute in parallel
- Children complete before parent
- If this.data changes, triggers automatic re-render
5. on_ready() (bottom-up)
- All children guaranteed ready
@@ -385,22 +400,67 @@ COMPONENT LIFECYCLE
- Children complete before parent
Depth-Ordered Execution:
- First: on_create runs before anything else (setup state)
- Top-down: render, on_render (parent before children)
- Bottom-up: on_create, on_load, on_ready (children before parent)
- Bottom-up: on_load, on_ready (children before parent)
- Parallel: Siblings at same depth during on_load()
Critical rules:
- Use on_create() to initialize default state before template runs
- Never modify DOM in on_load() - only update this.data
- on_load() runs in parallel for siblings (DOM unpredictable)
- Data changes during load trigger single re-render
- Data changes during load trigger automatic re-render
- on_create(), on_render(), on_destroy() must be synchronous
- on_load() and on_ready() can be async
ON_CREATE() USE CASES
The on_create() method runs BEFORE the first render, making it perfect
for initializing default state that templates will reference:
Example: Preventing "not iterable" errors
class DataGrid_Abstract extends Jqhtml_Component {
on_create() {
// Initialize defaults BEFORE template renders
this.data.rows = [];
this.data.loading = true;
this.data.is_empty = false;
}
async on_ready() {
// Later: load actual data
await this.load_page(1);
}
}
Template can now safely iterate:
<% for(let row of this.data.rows) { %>
<%= content('row', row); %>
<% } %>
Without on_create(), template would fail with "this.data.rows is not
iterable" because this.data starts as {} before on_load() runs.
Abstract Component Pattern:
Use on_create() in abstract base classes to ensure child templates
have required properties initialized:
class Form_Abstract extends Jqhtml_Component {
on_create() {
// Set defaults that all forms need
this.data.fields = this.data.fields || [];
this.data.errors = this.data.errors || {};
this.data.submitting = false;
}
}
DOUBLE-RENDER PATTERN
Components may render TWICE if on_load() modifies this.data:
1. First render: this.data = {} (empty)
2. on_load() populates this.data
3. Automatic re-render with populated data
4. on_ready() fires after second render (only once)
1. on_create() sets defaults: this.data.rows = []
2. First render: template uses empty rows array
3. on_load() populates this.data.rows with actual data
4. Automatic re-render with populated data
5. on_ready() fires after second render (only once)
Use for loading states:
<Define:Product_List>

View File

@@ -139,6 +139,76 @@ JQUERY HELPERS
$(this).attr('target', '_blank');
}
Component-Aware DOM Traversal
.shallowFind(selector)
Finds child elements matching the selector that don't have another
element of the same class as a parent between them and the component.
Useful for finding direct widget children in nested component trees
without accidentally selecting widgets from nested child components.
Example:
Component_A
└── div
└── Widget (found)
└── span
└── Widget (not found - has Widget parent)
$('.Component_A').shallowFind('.Widget')
// Returns only the first Widget
Use case - Finding form widgets without selecting nested widgets:
this.$.shallowFind('.Form_Field').each(function() {
// Only processes fields directly in this form,
// not fields in nested sub-forms
});
.closest_sibling(selector)
Searches for elements within progressively higher ancestor containers.
Similar to .closest() but searches within ancestors instead of
matching the ancestors themselves. Stops searching at <body> tag.
Useful for component-to-component communication when components need
to find related sibling or cousin components without knowing the
exact DOM structure.
Algorithm:
1. Get current element's parent
2. Search within parent using parent.find(selector)
3. If found, return results
4. If not found, move to parent's parent and repeat
5. Stop when reaching <body> (searches body but not beyond)
6. Return empty jQuery object if nothing found
Example DOM structure:
<body>
<div class="form-section">
<div class="row">
<div class="Country_Select_Input"></div>
</div>
<div class="another-row">
<div class="State_Select_Input"></div>
</div>
</div>
</body>
$('.Country_Select_Input').closest_sibling('.State_Select_Input')
// Finds State_Select_Input by searching up to form-section
Use case - Country selector updating state selector:
on_ready() {
if (this.tom_select) {
this.tom_select.on('change', () => {
const state_input = this.$el.closest_sibling('.State_Select_Input');
if (state_input.exists()) {
const widget = state_input.component();
widget.set_country_code(this.val());
}
});
}
}
Form Validation
.checkValidity()

724
app/RSpade/man/modals.txt Executable file
View File

@@ -0,0 +1,724 @@
================================================================================
MODAL SYSTEM
================================================================================
The Modal system provides a consistent, queue-managed interface for displaying
dialogs throughout the application. All modals are managed by the static Modal
class, which handles queuing, backdrop management, and user interactions.
================================================================================
BASIC DIALOGS
================================================================================
ALERT
-----
Show a simple notification message with an OK button.
await Modal.alert(message)
await Modal.alert(title, message)
await Modal.alert(title, message, button_label)
Examples:
await Modal.alert("File saved successfully");
await Modal.alert("Success", "Your changes have been saved");
await Modal.alert("Notice", "Operation complete", "Got it");
Parameters:
message - Message text (if only 1 arg) or jQuery element
title - Optional title (default: "Notice")
button_label - Optional button text (default: "OK")
Returns: Promise<void>
CONFIRM
-------
Show a confirmation dialog with Cancel and Confirm buttons.
let result = await Modal.confirm(message)
let result = await Modal.confirm(title, message)
let result = await Modal.confirm(title, message, confirm_label, cancel_label)
Examples:
if (await Modal.confirm("Delete this item?")) {
// User confirmed
}
if (await Modal.confirm("Delete Item", "This cannot be undone")) {
// User confirmed
}
Parameters:
message - Message text (if 1-2 args) or jQuery element
title - Optional title (default: "Confirm")
confirm_label - Confirm button text (default: "Confirm")
cancel_label - Cancel button text (default: "Cancel")
Returns: Promise<boolean> - true if confirmed, false if cancelled
PROMPT
------
Show an input dialog for text entry.
let value = await Modal.prompt(message)
let value = await Modal.prompt(title, message)
let value = await Modal.prompt(title, message, default_value)
let value = await Modal.prompt(title, message, default_value, multiline)
let value = await Modal.prompt(title, message, default_value, multiline, error)
Examples:
let name = await Modal.prompt("What is your name?");
if (name) {
console.log("Hello, " + name);
}
let email = await Modal.prompt("Email", "Enter your email:", "user@example.com");
let feedback = await Modal.prompt("Feedback", "Enter your feedback:", "", true);
Rich Content Example:
const $rich = $('<div>')
.append($('<h5 style="color: #2c3e50;">').text('Registration'))
.append($('<p>').html('Enter your <strong>full name</strong>'));
let name = await Modal.prompt($rich);
Validation Pattern (Lazy Re-prompting):
let email = '';
let error = null;
let valid = false;
while (!valid) {
email = await Modal.prompt('Email', 'Enter email:', email, false, error);
if (email === false) return; // Cancelled
// Validate
if (!email.includes('@')) {
error = 'Please enter a valid email address';
} else {
valid = true;
}
}
// email is now valid
Parameters:
message - Prompt message text or jQuery element
title - Optional title (default: "Input")
default_value - Default input value (default: "")
multiline - Show textarea instead of input (default: false)
error - Optional error message to display as validation feedback
Returns: Promise<string|false> - Input value or false if cancelled
Input Constraints:
Standard input: 245px minimum width
Textarea input: 315px minimum width
Spacing: 36px between message and input field
Error Display:
When error parameter is provided:
- Input field marked with .is-invalid class (red border)
- Error message displayed below input as .invalid-feedback
- Input retains previously entered value
- User can correct and resubmit
ERROR
-----
Show an error message dialog.
await Modal.error(error)
await Modal.error(error, title)
Examples:
await Modal.error("File not found");
await Modal.error(exception, "Upload Failed");
await Modal.error({message: "Invalid format"}, "Error");
Parameters:
error - String, error object, or {message: string}
title - Optional title (default: "Error")
Handles various error formats:
- String: "Error message"
- Object: {message: "Error"}
- Laravel response: {responseJSON: {message: "Error"}}
- Field errors: {field: "Error", field2: "Error2"}
Returns: Promise<void>
================================================================================
CUSTOM MODALS
================================================================================
SHOW
----
Display a custom modal with specified content and buttons.
let result = await Modal.show(options)
Options:
title - Modal title (default: "Modal")
body - String, HTML, or jQuery element
buttons - Array of button definitions (see below)
max_width - Maximum width in pixels (default: 800)
closable - Allow ESC/backdrop/X to close (default: true)
Button Definition:
{
label: "Button Text",
value: "return_value",
class: "btn-primary", // Bootstrap button class
default: true, // Make this the default button
callback: async function() {
// Optional: perform action and return result
return custom_value;
}
}
Examples:
// Two button modal
const result = await Modal.show({
title: "Choose Action",
body: "What would you like to do?",
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Continue", value: true, class: "btn-primary", default: true}
]
});
// Three button modal
const result = await Modal.show({
title: "Save Changes",
body: "How would you like to save?",
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Save Draft", value: "draft", class: "btn-info"},
{label: "Publish", value: "publish", class: "btn-success", default: true}
]
});
// jQuery content
const $content = $('<div>')
.append($('<p>').text('Custom content'))
.append($('<ul>').append($('<li>').text('Item 1')));
await Modal.show({
title: "Custom Content",
body: $content,
buttons: [{label: "Close", value: true, class: "btn-primary"}]
});
Returns: Promise<any> - Value from clicked button (false if cancelled)
================================================================================
FORM MODALS
================================================================================
FORM
----
Display a form component in a modal with validation support.
let result = await Modal.form(options)
Options:
component - Component class name (string)
component_args - Arguments to pass to component
title - Modal title (default: "Form")
max_width - Maximum width in pixels (default: 800)
closable - Allow ESC/backdrop to close (default: true)
submit_label - Submit button text (default: "Submit")
cancel_label - Cancel button text (default: "Cancel")
on_submit - Callback function (receives form component)
The on_submit callback pattern:
- Receives the form component instance
- Call form.vals() to get current values
- Perform validation/submission
- Return false to keep modal open (for errors)
- Return data to close modal and resolve promise
Simple Example:
const result = await Modal.form({
title: "New User",
component: "User_Form",
on_submit: async (form) => {
const values = form.vals();
if (!values.name) {
await Modal.alert("Name is required");
return false; // Keep modal open
}
await sleep(500); // Simulate save
return values; // Close modal with data
}
});
if (result) {
console.log("Saved:", result);
}
Validation Example:
const result = await Modal.form({
title: "Edit Profile",
component: "Profile_Form",
component_args: {data: user_data},
submit_label: "Update",
on_submit: async (form) => {
const values = form.vals();
// Server-side validation
const response = await User_Controller.update_profile(values);
if (response.errors) {
// Show errors and keep modal open
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
// Success - close and return
return response.data;
}
});
Creating Form Components:
Your form component must:
- Extend Jqhtml_Component
- Implement vals() method for getting/setting values
- Use standard form HTML with name attributes
- Include error container: <div $id="error_container"></div>
Example form component (my_form.jqhtml):
<Define:My_Form tag="div">
<div $id="error_container"></div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" $id="name_input" name="name">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" $id="email_input" name="email">
</div>
</Define:My_Form>
Example form component class (my_form.js):
class My_Form extends Jqhtml_Component {
on_create() {
this.data.values = this.args.data || {};
}
on_ready() {
if (this.data.values) {
this.vals(this.data.values);
}
}
vals(values) {
if (values) {
// Setter
this.$id('name_input').val(values.name || '');
this.$id('email_input').val(values.email || '');
return null;
} else {
// Getter
return {
name: this.$id('name_input').val(),
email: this.$id('email_input').val()
};
}
}
}
Validation Error Handling:
Form_Utils.apply_form_errors() automatically handles:
- Field-specific errors (matched by name attribute)
- General error messages
- Multiple error formats (string, array, object)
- Animated error display
- Bootstrap 5 validation classes
Error format examples:
// Field errors
{
name: "Name is required",
email: "Invalid email format"
}
// General errors
"An error occurred"
// Array of errors
["Error 1", "Error 2"]
// Laravel format
{
name: ["Name is required", "Name too short"],
email: ["Invalid format"]
}
Returns: Promise<Object|false> - Form data or false if cancelled
================================================================================
SPECIAL MODALS
================================================================================
UNCLOSABLE
----------
Display a modal that cannot be closed by user (no ESC, backdrop, or X button).
Must be closed programmatically.
Modal.unclosable(message)
Modal.unclosable(title, message)
Examples:
Modal.unclosable("Processing", "Please wait...");
setTimeout(() => {
Modal.close();
}, 3000);
Parameters:
message - Message text
title - Optional title (default: "Please Wait")
Returns: void (does not wait for close)
Note: Call Modal.close() to dismiss the modal programmatically.
================================================================================
MODAL STATE MANAGEMENT
================================================================================
IS_OPEN
-------
Check if a modal is currently displayed.
if (Modal.is_open()) {
console.log("Modal is open");
}
Returns: boolean
GET_CURRENT
-----------
Get the currently displayed modal instance.
const modal = Modal.get_current();
if (modal) {
console.log("Modal instance exists");
}
Returns: Rsx_Modal instance or null
CLOSE
-----
Programmatically close the current modal.
await Modal.close();
Typically used with unclosable modals or to force-close from external code.
Returns: Promise<void>
APPLY_ERRORS
------------
Apply validation errors to the current modal (if it contains a form).
Modal.apply_errors({
field1: "Error message",
field2: "Another error"
});
This is a convenience method that calls Form_Utils.apply_form_errors() on
the current modal's body element.
Parameters:
errors - Error object (field: message pairs)
Returns: void
================================================================================
MODAL QUEUING
================================================================================
The Modal system automatically queues multiple simultaneous modal requests
and displays them sequentially:
// All three modals are 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]);
Queuing Behavior:
- Single shared backdrop persists across queued modals
- 500ms delay between modals (backdrop stays visible)
- Backdrop fades in at start of queue
- Backdrop fades out when queue is empty
- Each modal appears instantly (no fade animation)
Current Limitations:
- All modals treated equally (no priority levels)
- No concept of "modal sessions" or grouped interactions
- FIFO queue order (first requested, first shown)
Future Considerations:
When implementing real-time notifications or background events, you may
need to distinguish between:
- User-initiated modal sequences (conversational flow)
- Background notifications (should wait for user flow to complete)
Planned features:
- Priority levels for different modal types
- Modal sessions to group related interactions
- External event blocking during active user sessions
================================================================================
MODAL SIZING
================================================================================
Responsive Sizing:
- Desktop: 60% viewport width preferred, max 80%
- Mobile: 90% viewport width
- Minimum width: 400px desktop, 280px mobile
- Minimum height: 260px
Maximum Width:
- Default: 800px
- Configurable via max_width option
- Examples: 500px (forms), 1200px (data tables)
Scrolling:
- Triggers when content exceeds 80% viewport height
- Modal body becomes scrollable
- Header and footer remain fixed
Manual Control:
Modal.show({
max_width: 1200, // Wide modal for tables
body: content
});
================================================================================
STYLING AND UX
================================================================================
Modal Appearance:
- Centered vertically and horizontally
- Gray header background (#f8f9fa)
- Smaller title font (1rem)
- Shorter header padding (0.75rem)
- Subtle drop shadow (0 4px 12px rgba(0,0,0,0.15))
- Buttons centered horizontally as a group
- Modal body text centered (for simple dialogs)
Animations:
- Modal appears instantly (no fade)
- Backdrop fades in/out over 250ms
- Validation errors fade in over 300ms
Body Scroll Lock:
- Page scrolling disabled when modal open
- Scrollbar width calculated and compensated via padding
- Prevents layout shift when scrollbar disappears
- Original body state restored when modal closes
- Managed at backdrop level (first modal locks, last unlocks)
Accessibility:
- ESC key closes modal (if closable)
- Backdrop click closes modal (if closable)
- Focus management (input fields auto-focus)
- Keyboard navigation support
================================================================================
BEST PRACTICES
================================================================================
1. Use Appropriate Dialog Type
- alert() - Notifications, information
- confirm() - Yes/no decisions
- prompt() - Simple text input
- form() - Complex forms with validation
- show() - Custom requirements
2. Handle Cancellations
Always check for false (cancelled) return values:
const result = await Modal.confirm("Delete?");
if (result === false) {
return; // User cancelled
}
3. Validation Feedback
Keep modal open for validation errors:
if (errors) {
Form_Utils.apply_form_errors(form.$, errors);
return false; // Keep open
}
4. Avoid Nested Modals
While technically possible, nested modals create poor UX.
Close the first modal before showing a second:
await Modal.alert("Step 1");
await Modal.alert("Step 2"); // Shows after first closes
5. Loading States
For long operations, use unclosable modals:
Modal.unclosable("Saving", "Please wait...");
await save_operation();
await Modal.close();
6. Rich Content
Use jQuery elements for formatted content:
const $content = $('<div>')
.append($('<h5>').text('Title'))
.append($('<p>').html('<strong>Bold</strong> text'));
await Modal.alert($content);
7. Form Component Design
- Keep vals() method simple and synchronous
- Put async logic in on_submit callback
- Use standard HTML form structure
- Include error_container div for validation
- Match field name attributes to error keys
================================================================================
COMMON PATTERNS
================================================================================
Delete Confirmation:
const confirmed = await Modal.confirm(
"Delete Item",
"This action cannot be undone. Are you sure?",
"Delete Forever",
"Cancel"
);
if (confirmed) {
await Item_Controller.delete(item_id);
await Modal.alert("Item deleted successfully");
}
Save with Validation:
const result = await Modal.form({
title: "Edit Profile",
component: "Profile_Form",
component_args: {data: user},
on_submit: async (form) => {
const values = form.vals();
const response = await User_Controller.save(values);
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
});
if (result) {
await Modal.alert("Profile updated successfully");
}
Multi-Step Process:
const name = await Modal.prompt("What is your name?");
if (!name) return;
const email = await Modal.prompt("Enter your email:");
if (!email) return;
const confirmed = await Modal.confirm(
"Confirm Registration",
`Register ${name} with ${email}?`
);
if (confirmed) {
await register({name, email});
}
Progressive Disclosure:
const result = await Modal.show({
title: "Choose Action",
body: "What would you like to do?",
buttons: [
{label: "View Details", value: "view"},
{label: "Edit", value: "edit"},
{label: "Delete", value: "delete", class: "btn-danger"}
]
});
if (result === "view") {
// Show details modal
} else if (result === "edit") {
// Show edit form
} else if (result === "delete") {
// Confirm and delete
}
================================================================================
TROUBLESHOOTING
================================================================================
Modal Won't Close
- Check if callback returns false (intentionally keeping open)
- Verify closable: true option is set
- Check for JavaScript errors in callback
- Use Modal.close() to force close
Validation Errors Not Showing
- Ensure form has <div $id="error_container"></div>
- Verify field name attributes match error keys
- Check that fields are wrapped in .form-group containers
- Use Form_Utils.apply_form_errors(form.$, errors)
Form Values Not Saving
- Verify vals() method returns correct object
- Check that on_submit returns data (not false)
- Ensure callback doesn't throw unhandled errors
- Test vals() method independently
Queue Not Working
- All modals automatically queue
- If backdrop flickers, check for multiple backdrop creation
- Verify using Modal.* static methods (not creating instances)
Component Not Found
- Ensure component class name is correct (case-sensitive)
- Check that component files are in manifest
- Verify component extends Jqhtml_Component
- Component must be in /rsx/ directory tree
================================================================================

View File

@@ -12,7 +12,7 @@ DESCRIPTION
Key differences from Laravel:
- Laravel: route('user.profile', $user) using named routes
- RSX: Rsx::Route('User_Controller', 'profile')->url(['id' => $user->id])
- RSX: Rsx::Route('User_Controller', 'profile', ['id' => $user->id])
Benefits:
- No route name management required
@@ -25,30 +25,24 @@ BASIC USAGE
PHP Syntax:
use App\RSpade\Core\Rsx;
// Create route proxy (action defaults to 'index')
$route = Rsx::Route('Demo_Index_Controller');
$route = Rsx::Route('Demo_Index_Controller', 'show');
// Generate URLs (returns string directly)
$url = Rsx::Route('Demo_Index_Controller'); // /demo
$url = Rsx::Route('Demo_Index_Controller', 'show'); // /demo/show
$url = Rsx::Route('Demo_Index_Controller', 'show', ['id' => 123]); // /demo/123
$url = Rsx::Route('Demo_Index_Controller', 'show', 123); // /demo/123 (shorthand)
// Generate URLs
$url = $route->url(); // /demo
$url = $route->url(['id' => 123]); // /demo/123 or /demo?id=123
$absolute = $route->absolute_url(); // https://site.com/demo
// Navigate (redirect)
$route->navigate(); // Sends Location header and exits
// Use in redirects
return redirect(Rsx::Route('Demo_Index_Controller'));
JavaScript Syntax:
// Create route proxy (action defaults to 'index')
const route = Rsx.Route('Demo_Index_Controller');
const route = Rsx.Route('Demo_Index_Controller', 'show');
// Generate URLs (returns string directly)
const url = Rsx.Route('Demo_Index_Controller'); // /demo
const url = Rsx.Route('Demo_Index_Controller', 'show'); // /demo/show
const url = Rsx.Route('Demo_Index_Controller', 'show', {id: 123}); // /demo/123
const url = Rsx.Route('Demo_Index_Controller', 'show', 123); // /demo/123 (shorthand)
// Generate URLs
const url = route.url(); // /demo
const url = route.url({id: 123}); // /demo/123 or /demo?id=123
const absolute = route.absolute_url(); // https://site.com/demo
// Navigate
route.navigate(); // Sets window.location.href
// Use in navigation
window.location.href = Rsx.Route('Demo_Index_Controller');
ROUTE PATTERNS
Route Definition:
@@ -149,15 +143,18 @@ ADVANCED PATTERNS
Complex Parameter Examples:
// Multiple parameters
#[Route('/api/v1/users/:company/:division/:id')]
$route->url(['company' => 'acme', 'division' => 'sales', 'id' => 123]);
$url = Rsx::Route('Api_V1_Users_Controller', 'show',
['company' => 'acme', 'division' => 'sales', 'id' => 123]);
// Result: /api/v1/users/acme/sales/123
// Query parameters for extra values
$route->url(['id' => 123, 'format' => 'json', 'include' => 'profile']);
$url = Rsx::Route('Demo_Controller', 'show',
['id' => 123, 'format' => 'json', 'include' => 'profile']);
// Result: /demo/123?format=json&include=profile
// Complex objects as parameters
$route->url(['filter' => ['status' => 'active', 'type' => 'user']]);
$url = Rsx::Route('Demo_Controller', 'index',
['filter' => ['status' => 'active', 'type' => 'user']]);
// Result: /demo?filter[status]=active&filter[type]=user
Route Groups and Prefixes:
@@ -194,8 +191,7 @@ JAVASCRIPT BUNDLE ROUTES
Runtime Route Access:
// Routes available after bundle loads
if (typeof Rsx !== 'undefined') {
const route = Rsx.Route('Demo_Index_Controller');
const url = route.url();
const url = Rsx.Route('Demo_Index_Controller');
}
ERROR HANDLING
@@ -218,7 +214,7 @@ DEBUGGING ROUTES
php artisan rsx:routes # List all discovered routes
Test Route Generation:
php artisan rsx:debug /demo --eval="Rsx.Route('Demo_Index_Controller').url()"
php artisan rsx:debug /demo --eval="Rsx.Route('Demo_Index_Controller')"
Route Information:
php artisan rsx:manifest:show # View route cache in manifest
@@ -229,21 +225,21 @@ COMMON PATTERNS
public static function handle_form(Request $request, array $params = [])
{
// Process form...
Rsx::Route('Dashboard_Index_Controller')->navigate();
return redirect(Rsx::Route('Dashboard_Index_Controller'));
}
AJAX URL Generation:
// Generate URLs for AJAX calls
const apiUrl = Rsx.Route('Api_User_Controller', 'update').url({id: userId});
const apiUrl = Rsx.Route('Api_User_Controller', 'update', {id: userId});
fetch(apiUrl, {method: 'POST', body: formData});
Form Action URLs:
// Generate form action URLs
<form action="<?= Rsx::Route('User_Profile_Controller', 'update')->url() ?>" method="POST">
<form action="<?= Rsx::Route('User_Profile_Controller', 'update') ?>" method="POST">
Link Generation:
// Generate navigation links
<a href="<?= Rsx::Route('Dashboard_Index_Controller')->url() ?>">Dashboard</a>
<a href="<?= Rsx::Route('Dashboard_Index_Controller') ?>">Dashboard</a>
TROUBLESHOOTING
Route Not Found:

212
app/RSpade/man/tasks.txt Executable file
View File

@@ -0,0 +1,212 @@
RSX TASK SYSTEM
================
The RSX Task system provides a structured way to define and execute background
tasks, similar to how Controllers handle HTTP requests but for CLI/background
execution.
## OVERVIEW
Tasks are static methods in Service classes that can be executed from:
- Command line (via `rsx:task:run`)
- Internal PHP code (via `Task::internal()`)
- Future: Queue systems, cron scheduling, progress tracking
## CREATING SERVICES
Services are classes that extend `Rsx_Service_Abstract` and live in:
/rsx/services/
Example service structure:
<?php
use App\RSpade\Core\Service\Rsx_Service_Abstract;
class My_Service extends Rsx_Service_Abstract
{
#[Task('Description of what this task does')]
public static function my_task(array $params = [])
{
// Task implementation
return [
'message' => 'Task completed',
'data' => 'result data'
];
}
}
## TASK METHODS
Tasks must:
- Be public static methods
- Have the #[Task('description')] attribute
- Accept array $params = [] parameter
- Return data (will be JSON-encoded for CLI output)
Task signature:
public static function task_name(array $params = [])
## PARAMETER HANDLING
Services inherit parameter helpers from Rsx_Service_Abstract:
protected static function __param($params, $key, $default = null)
protected static function __has_param($params, $key)
Example usage:
$name = static::__param($params, 'name', 'Guest');
if (static::__has_param($params, 'force')) {
// ...
}
## PRE-TASK HOOK
Override pre_task() to run validation/auth before any task executes:
public static function pre_task(array $params = [])
{
if (!RsxAuth::check()) {
throw new \Exception("Authentication required");
}
return null; // Continue to task
}
If pre_task() returns non-null, task execution halts and returns that value.
## LISTING TASKS
View all available tasks:
php artisan rsx:task:list
Output shows services and their tasks with descriptions:
Service_Test
hello_world - Test task with no arguments
greet - Test task with optional name parameter
calculate - Test task with multiple parameters
## RUNNING TASKS
Execute a task from command line:
php artisan rsx:task:run Service task_name
With parameters:
php artisan rsx:task:run Service task_name --param=value
php artisan rsx:task:run Service greet --name=John
Boolean flags:
php artisan rsx:task:run Service task_name --force
JSON values (auto-parsed):
php artisan rsx:task:run Service task_name --data='{"key":"value"}'
Debug mode (wrapped response):
php artisan rsx:task:run Service task_name --debug
## OUTPUT MODES
Default mode - Raw JSON response (just the return value):
{
"message": "Task completed",
"count": 42
}
Debug mode - Wrapped response with success indicator:
{
"success": true,
"result": {
"message": "Task completed",
"count": 42
}
}
## INTERNAL TASK CALLS
Call tasks from PHP code using Task::internal():
use App\RSpade\Core\Task\Task;
$result = Task::internal('Service_Name', 'task_name', [
'param1' => 'value1',
'param2' => 'value2'
]);
This is useful for:
- Composing complex tasks from simpler ones
- Calling tasks from controllers
- Background job processing
Example composition:
#[Task('Run all seeders')]
public static function seed_all(array $params = [])
{
$clients = Task::internal('Seeder_Service', 'seed_clients', $params);
$contacts = Task::internal('Seeder_Service', 'seed_contacts', $params);
return [
'clients' => $clients,
'contacts' => $contacts
];
}
## ERROR HANDLING
All errors return JSON (never throws to stderr):
{
"success": false,
"error": "Error message",
"error_type": "Exception",
"trace": "..."
}
Exit codes:
- 0: Success
- 1: Error
## ATTRIBUTE CONFLICTS
A method cannot have multiple execution type attributes. These conflict:
- #[Route] (HTTP routes)
- #[Ajax_Endpoint] (Ajax endpoints)
- #[Task] (CLI tasks)
The manifest build will fail if these are mixed on the same method.
## USE CASES
Tasks are ideal for:
- Database seeders
- Data migrations
- Report generation
- Batch processing
- Maintenance operations
- Background jobs
- Scheduled operations
Example services:
Seeder_Service - Database seeding
Report_Service - Generate reports
Cleanup_Service - Maintenance tasks
Import_Service - Data imports
Export_Service - Data exports
## FUTURE FEATURES (NOT YET IMPLEMENTED)
The Task system is designed to support future enhancements:
- Queue integration (dispatch tasks to Redis/database queue)
- Cron scheduling (#[Schedule] attribute)
- Progress tracking (long-running tasks report progress)
- Task history (log of all task executions)
- Task dependencies (ensure X runs before Y)
- Parallel execution (run multiple tasks concurrently)
## EXAMPLES
See example services:
/rsx/services/service_test.php - Basic task examples
/rsx/services/seeder_service.php - Database seeding examples
Test tasks:
php artisan rsx:task:list
php artisan rsx:task:run Service_Test hello_world
php artisan rsx:task:run Service_Test greet --name=Brian
php artisan rsx:task:run Service_Test calculate --a=10 --b=5 --op=multiply