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 '