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:
@@ -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()) {
|
||||
|
||||
582
app/RSpade/man/forms_and_widgets.txt
Executable file
582
app/RSpade/man/forms_and_widgets.txt
Executable 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=" ">
|
||||
<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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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
724
app/RSpade/man/modals.txt
Executable 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
|
||||
|
||||
================================================================================
|
||||
@@ -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
212
app/RSpade/man/tasks.txt
Executable 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
|
||||
Reference in New Issue
Block a user