diff --git a/app/RSpade/Core/Database/Database_BundleIntegration.php b/app/RSpade/Core/Database/Database_BundleIntegration.php index 25d2356cc..d4f914415 100644 --- a/app/RSpade/Core/Database/Database_BundleIntegration.php +++ b/app/RSpade/Core/Database/Database_BundleIntegration.php @@ -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; diff --git a/app/RSpade/Core/Manifest/Modules/Model_ManifestSupport.php b/app/RSpade/Core/Manifest/Modules/Model_ManifestSupport.php index 87c73209c..5f202cc87 100644 --- a/app/RSpade/Core/Manifest/Modules/Model_ManifestSupport.php +++ b/app/RSpade/Core/Manifest/Modules/Model_ManifestSupport.php @@ -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 * diff --git a/app/RSpade/man/form_input.txt b/app/RSpade/man/form_input.txt index bbe24e051..cdd85fbf2 100755 --- a/app/RSpade/man/form_input.txt +++ b/app/RSpade/man/form_input.txt @@ -220,8 +220,18 @@ BUILT-IN INPUTS Text_Input Text, email, url, tel, number, textarea inputs. - - + REQUIRES $max_length argument for character limits. + + + + + $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. diff --git a/app/RSpade/upstream_changes/form_input_abstract_12_29_2.txt b/app/RSpade/upstream_changes/form_input_abstract_12_29_2.txt index 736da2fff..bb3558635 100755 --- a/app/RSpade/upstream_changes/form_input_abstract_12_29_2.txt +++ b/app/RSpade/upstream_changes/form_input_abstract_12_29_2.txt @@ -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 diff --git a/app/RSpade/upstream_changes/text_field_max_lengths_12_29.txt b/app/RSpade/upstream_changes/text_field_max_lengths_12_29.txt new file mode 100755 index 000000000..205cba56c --- /dev/null +++ b/app/RSpade/upstream_changes/text_field_max_lengths_12_29.txt @@ -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 + + +``` + +### After (New Pattern) +```html + + +``` + +## 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 ' +``` + +For custom limits: +```html + +``` + +For truly unlimited fields (use sparingly): +```html + +``` + +### 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 + + + + + +``` + +## 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 '