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>
620 lines
20 KiB
Plaintext
Executable File
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=" ">
|
|
<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)
|