Files
rspade_system/app/RSpade/man/forms_and_widgets.txt
root 21a7149486 Switch Ajax transport to JSON with proper Content-Type
Migrate $name from Form_Field to input components
Refactor form inputs: $name moves from Form_Field to input components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-29 09:43:23 +00:00

620 lines
20 KiB
Plaintext
Executable File

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 $label="Email Address" $required=true>
<Text_Input $name="email" $type="email" $placeholder="user@example.com" />
</Form_Field>
<Form_Field $label="Biography">
<Text_Input $name="bio" $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 $label="First Name">
<Text_Input $name="first_name" />
</Form_Field>
<Form_Field $label="Last Name">
<Text_Input $name="last_name" />
</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.
Architecture:
Form_Field extends Form_Field_Abstract, which provides all core
functionality (widget discovery, data-name attributes, error handling).
Form_Field adds visual formatting (labels, spacing, error display).
- Form_Field_Abstract: Base class with field functionality, no formatting
- Form_Field: Concrete implementation with Bootstrap formatting
- Form_Hidden_Field: Specialized single-tag hidden input
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
- Read data-name from child widget (set by Form_Input_Abstract)
- Display validation errors returned from server
- Provide consistent spacing and styling
Example - Basic Field:
<Form_Field $label="Email Address">
<Text_Input $name="email" $type="email" />
</Form_Field>
Example - Required Field with Help Text:
<Form_Field $label="Password"
$required=true
$help="Must be at least 8 characters">
<Text_Input $name="password" $type="password" />
</Form_Field>
Example - Field with HTML in Label:
<Form_Field $label="<i class='bi bi-twitter'></i> Twitter">
<Text_Input $name="twitter" $prefix="@" />
</Form_Field>
Form_Field_Abstract:
Use Form_Field_Abstract directly when you need field functionality
without visual formatting (e.g., for custom layouts).
Example - Unformatted Field:
<Form_Field_Abstract>
<Custom_Widget $name="custom_field" />
</Form_Field_Abstract>
Form_Hidden_Field:
Specialized single-tag component for hidden inputs. Unlike other
Form_Field components, this IS both the field wrapper AND the hidden
input itself (uses tag="input" type="hidden").
Example - Hidden Field:
<Form_Hidden_Field $name="id" />
Form_Hidden_Field extends Form_Field_Abstract and implements val()
to get/set the hidden input value. No child widget needed - the
component itself is the input.
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.$sid('input').val();
}
// Setter - update value
else {
this.data.value = value || '';
this.$sid('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.
IMPORTANT: Unlike standard HTML disabled elements (which don't submit
their values in traditional HTML forms), RSX disabled fields DO return
values when vals() is called and ARE included in Ajax form submissions.
This makes disabled fields useful for displaying read-only data that
should still be included in form submissions.
Behavior:
- Widget displays grayed-out appearance
- User cannot interact with widget
- val() getter still returns current value
- Value included in form submission via Ajax (unlike HTML forms)
- HTML disabled attribute prevents traditional browser form submission
Example - Disable Individual Fields:
<Form_Field $label="Email (Cannot Edit)">
<Text_Input $name="email" $type="email" $disabled=true />
</Form_Field>
Example - Conditional Disable:
<Form_Field $label="Status">
<Select_Input $name="status" $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 $label="First Name">
<Text_Input $name="first_name" />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $label="Last Name">
<Text_Input $name="last_name" />
</Form_Field>
</div>
</div>
<div class="row">
<div class="col-md-4">
<Form_Field $label="City">
<Text_Input $name="city" />
</Form_Field>
</div>
<div class="col-md-2">
<Form_Field $label="State">
<Text_Input $name="state" $maxlength=2 />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $label="ZIP Code">
<Text_Input $name="zip" />
</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 $sid="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 $label="Rate Your Experience">
<Rating_Input $name="satisfaction" />
</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))
<Form_Hidden_Field $name="id" />
@endif
<Form_Field $label="Company Name" $required=true>
<Text_Input $name="name" $seeder="company_name" />
</Form_Field>
<div class="row">
<div class="col-md-6">
<Form_Field $label="Email">
<Text_Input $name="email" $type="email" $seeder="email" />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $label="Phone">
<Text_Input $name="phone" $type="tel" $seeder="phone" />
</Form_Field>
</div>
</div>
<Form_Field $label="Industry">
<Select_Input $name="industry" $options="{{ json_encode($industries) }}"
$placeholder="Select Industry..." />
</Form_Field>
<Form_Field $label="Notes">
<Text_Input $name="notes" $type="textarea" $rows=5 />
</Form_Field>
<Form_Field $label="&nbsp;">
<Checkbox_Input $name="active" $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)