Add Text_Input $max_length requirement and Model.field_length() API

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-29 10:48:10 +00:00
parent 21a7149486
commit 12b742fbdf
6 changed files with 211 additions and 2 deletions

View File

@@ -546,6 +546,24 @@ class Database_BundleIntegration extends BundleIntegration_Abstract
$content .= " }\n\n";
}
// Generate field_length() method for varchar max lengths
$varchar_lengths = [];
foreach ($columns as $col_name => $col_data) {
if (isset($col_data['max_length']) && $col_data['max_length'] !== null) {
$varchar_lengths[$col_name] = $col_data['max_length'];
}
}
$content .= " /**\n";
$content .= " * Get max length for a varchar/char column.\n";
$content .= " * @param {string} column - Column name\n";
$content .= " * @returns {number|null} Max length for varchar/char columns, null for other types\n";
$content .= " */\n";
$content .= " static field_length(column) {\n";
$content .= " const lengths = " . json_encode($varchar_lengths, JSON_FORCE_OBJECT) . ";\n";
$content .= " return lengths[column] ?? null;\n";
$content .= " }\n\n";
$content .= "}\n";
return $content;

View File

@@ -75,6 +75,7 @@ class Model_ManifestSupport extends ManifestSupport_Abstract
foreach ($column_results as $column) {
$columns[$column->Field] = [
'type' => static::__parse_column_type($column->Type),
'max_length' => static::__parse_varchar_length($column->Type),
'nullable' => ($column->Null === 'YES'),
'key' => $column->Key,
'default' => $column->Default,
@@ -158,6 +159,24 @@ class Model_ManifestSupport extends ManifestSupport_Abstract
return $type;
}
/**
* Extract varchar length from MySQL column type
*
* Returns the max length for varchar/char fields, null for all other types.
*
* @param string $type MySQL column type (e.g., "varchar(255)", "char(10)", "text")
* @return int|null Max length for varchar/char, null otherwise
*/
protected static function __parse_varchar_length(string $type): ?int
{
// Match varchar(N) or char(N)
if (preg_match('/^(?:varchar|char)\((\d+)\)/i', $type, $matches)) {
return (int) $matches[1];
}
return null;
}
/**
* Get the name of this support module
*

View File

@@ -220,8 +220,18 @@ BUILT-IN INPUTS
Text_Input
Text, email, url, tel, number, textarea inputs.
<Text_Input $type="email" $placeholder="user@example.com" />
<Text_Input $type="textarea" $rows="5" />
REQUIRES $max_length argument for character limits.
<Text_Input $name="email" $max_length=User_Model.field_length('email') />
<Text_Input $name="notes" $type="textarea" $max_length=-1 $rows="5" />
$max_length values:
- Positive number: Sets HTML maxlength attribute
- -1: Unlimited (no maxlength applied)
- Undefined: Console error with guidance
Use Model.field_length('column') for database-driven limits.
Subclasses (Phone_Text_Input, Currency_Input) are exempt.
Select_Input
Dropdown with static options.

View File

@@ -79,12 +79,34 @@ Remove any implementations of:
- `_transform_value(value)` - no longer in base class
- `seed()` - removed from Form_Input_Abstract contract
### 6. Update _mark_ready() implementation
If you've copied `Form_Input_Abstract` or have custom input classes, update `_mark_ready()`:
```javascript
_mark_ready() {
this._is_ready = true;
if (this._pending_value !== null) {
this._set_value(this._pending_value);
this.trigger('val', this._pending_value);
this._pending_value = null;
} else {
this.trigger('val', this._get_value());
}
}
```
The fix: `_mark_ready()` now always triggers the 'val' event on initialization.
This ensures listeners registered via `this.sid('input').on('val', ...)` receive
the initial value even when no value was buffered.
## How It Works Now
1. Input component receives `$name="email"` as argument
2. `Form_Input_Abstract.on_create()` sets `data-name="email"` on component root
3. `Form_Field` finds child `.Form_Input_Abstract` and reads its `data-name`
4. `Rsx_Form.vals()` finds all `[data-name]` elements and calls `val()` on each
5. `_mark_ready()` triggers 'val' event with initial value on component ready
## Validation

View File

@@ -0,0 +1,139 @@
# Text Input $max_length Requirement
**Date:** 2025-12-29
**Affects:** All Text_Input and textarea components
## Breaking Change
`Text_Input` now **requires** the `$max_length` argument. This ensures all text fields have explicit character limits tied to database schema.
### Before (Old Pattern)
```html
<Text_Input $name="email" $placeholder="user@example.com" />
<Text_Input $name="notes" $type="textarea" $rows=5 />
```
### After (New Pattern)
```html
<Text_Input $name="email" $max_length=User_Model.field_length('email') $placeholder="user@example.com" />
<Text_Input $name="notes" $type="textarea" $max_length=-1 $rows=5 />
```
## Why This Change
Database VARCHAR columns have length limits. When forms don't enforce these limits:
- Users can enter text that gets truncated on save
- Data loss occurs silently
- No feedback to user about constraints
By requiring `$max_length`, we ensure:
- Character limits are enforced in the UI
- Limits come from a single source of truth (database schema)
- Changes to database column sizes automatically propagate to forms
## The field_length() API
Models now expose a `field_length(column)` static method that returns the VARCHAR max length:
```javascript
// Returns max length for varchar/char columns, null for others
User_Model.field_length('email') // 255
User_Model.field_length('username') // 50
User_Model.field_length('id') // null (not a varchar)
```
This is generated automatically from database schema metadata.
## $max_length Values
| Value | Behavior |
|-------|----------|
| Positive number | Sets HTML `maxlength` attribute to that value |
| `-1` | Unlimited - no maxlength attribute applied |
| `undefined` | **Error** - console.error with guidance |
## Migration Checklist
### 1. Find all Text_Input components
```bash
grep -rn '<Text_Input' rsx/app --include="*.jqhtml" --include="*.blade.php"
```
### 2. Add $max_length to each
For database-backed fields (recommended):
```html
<Text_Input $name="email" $max_length=User_Model.field_length('email') />
```
For custom limits:
```html
<Text_Input $name="search" $max_length=100 />
```
For truly unlimited fields (use sparingly):
```html
<Text_Input $name="notes" $type="textarea" $max_length=-1 />
```
### 3. Verify no console errors
Load each form and check browser console. Any Text_Input missing `$max_length` will log:
```
Text_Input with $name="fieldname" requires $max_length. Use $max_length=Model_Name.field_length('column_name') for database-driven limits, a numeric value for custom limits, or -1 for unlimited.
```
## Subclasses Are Exempt
Components extending `Text_Input` (like `Phone_Text_Input`, `Currency_Input`) are NOT affected. The validation only triggers when `Text_Input` is used directly:
```javascript
// In Text_Input.on_create():
if (this.constructor === Text_Input && this.args.max_length === undefined) {
console.error(...);
}
```
This allows specialized inputs to define their own intrinsic limits.
## Custom Text Input Components
If you have custom components extending `Text_Input`, they automatically bypass the $max_length requirement. If you want your custom component to also require $max_length, add the same check:
```javascript
class My_Text_Input extends Text_Input {
on_create() {
super.on_create();
// Optionally enforce $max_length on this component too
if (this.constructor === My_Text_Input && this.args.max_length === undefined) {
console.error(`My_Text_Input with $name="${this.args.name}" requires $max_length.`);
}
}
}
```
## Renamed Argument
The old `$maxlength` argument has been renamed to `$max_length` (with underscore) for consistency with RSX naming conventions.
```html
<!-- OLD (no longer works) -->
<Text_Input $maxlength="255" />
<!-- NEW -->
<Text_Input $max_length=255 />
```
## Validation
After migration, verify no console errors by loading forms and checking browser console:
```bash
# Find all forms in the app
find rsx/app -name "*.jqhtml" -exec grep -l '<Text_Input' {} \;
```
Load each form and confirm no $max_length errors in console.

View File

@@ -698,6 +698,7 @@ Pattern recognition:
- `Form_Utils.apply_form_errors()` for validation
- Action loads data, Controller saves it
- `$disabled=true` still returns values (unlike HTML)
- **Text_Input requires `$max_length`**: Use `Model.field_length('column')` for database-driven limits, numeric value for custom, or `-1` for unlimited
**Detailed guidance in `forms` skill** - auto-activates when building forms.