Add skills documentation and misc updates
Add form value persistence across cache revalidation re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -74,11 +74,19 @@ class Rsx_Breadcrumb_Resolver {
|
||||
return generation === Rsx_Breadcrumb_Resolver._generation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key including build_key for invalidation on deploy
|
||||
*/
|
||||
static _cache_key(url) {
|
||||
const build_key = window.rsxapp?.build_key || '';
|
||||
return Rsx_Breadcrumb_Resolver.CACHE_PREFIX + build_key + ':' + url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached chain for a URL
|
||||
*/
|
||||
static _get_cached_chain(url) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
|
||||
return Rsx_Storage.session_get(cache_key);
|
||||
}
|
||||
|
||||
@@ -86,7 +94,7 @@ class Rsx_Breadcrumb_Resolver {
|
||||
* Cache a resolved chain
|
||||
*/
|
||||
static _cache_chain(url, chain) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
|
||||
// Store simplified chain data (url, label, is_active)
|
||||
const cache_data = chain.map(item => ({
|
||||
url: item.url,
|
||||
@@ -250,7 +258,7 @@ class Rsx_Breadcrumb_Resolver {
|
||||
*/
|
||||
static clear_cache(url) {
|
||||
if (url) {
|
||||
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
|
||||
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
|
||||
Rsx_Storage.session_remove(cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\RSpade\CodeQuality\Rules\JavaScript;
|
||||
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
|
||||
use App\RSpade\Core\Cache\RsxCache;
|
||||
use App\RSpade\Core\Manifest\Manifest;
|
||||
|
||||
/**
|
||||
* Check Component implementations for common AI agent mistakes
|
||||
@@ -47,8 +48,26 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not a Component subclass
|
||||
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Component') {
|
||||
// Skip if no class defined
|
||||
if (!isset($metadata['class'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a Component subclass (directly or indirectly)
|
||||
$is_component_subclass = false;
|
||||
if (isset($metadata['extends']) && $metadata['extends'] === 'Component') {
|
||||
$is_component_subclass = true;
|
||||
} elseif (isset($metadata['extends'])) {
|
||||
// Check inheritance chain via manifest
|
||||
try {
|
||||
$is_component_subclass = Manifest::js_is_subclass_of($metadata['class'], 'Component');
|
||||
} catch (\Exception $e) {
|
||||
// Manifest not ready or class not found - skip
|
||||
$is_component_subclass = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_component_subclass) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,6 +78,16 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for on_destroy method using manifest metadata (the correct method is on_stop)
|
||||
if (!empty($metadata['public_instance_methods'])) {
|
||||
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
|
||||
if ($method_name === 'on_destroy') {
|
||||
$line = $method_info['line'] ?? 1;
|
||||
$this->throw_on_destroy_error($file_path, $line, $metadata['class']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$lines = explode("\n", $contents);
|
||||
|
||||
// Check for render() method and incorrect lifecycle methods
|
||||
@@ -151,11 +180,48 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
|
||||
$error_message .= "- on_create(): Called when component is created\n";
|
||||
$error_message .= "- on_load(): Called to load async data\n";
|
||||
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
|
||||
$error_message .= "- on_destroy(): Called when component is destroyed\n\n";
|
||||
$error_message .= "- on_stop(): Called when component is destroyed\n\n";
|
||||
$error_message .= "FIX:\n";
|
||||
$error_message .= "Rename '{$method_name}()' to 'on_{$method_name}()'\n";
|
||||
$error_message .= "==========================================";
|
||||
|
||||
throw new YoureDoingItWrongException($error_message);
|
||||
}
|
||||
|
||||
private function throw_on_destroy_error(string $file_path, int $line_number, string $class_name): void
|
||||
{
|
||||
$error_message = "==========================================\n";
|
||||
$error_message .= "FATAL: Jqhtml component uses incorrect lifecycle method name\n";
|
||||
$error_message .= "==========================================\n\n";
|
||||
$error_message .= "File: {$file_path}\n";
|
||||
$error_message .= "Line: {$line_number}\n";
|
||||
$error_message .= "Class: {$class_name}\n\n";
|
||||
$error_message .= "The method 'on_destroy()' does not exist in jqhtml components.\n\n";
|
||||
$error_message .= "PROBLEM:\n";
|
||||
$error_message .= "The correct lifecycle method for cleanup is 'on_stop()', not 'on_destroy()'.\n";
|
||||
$error_message .= "This is a common mistake from other frameworks.\n\n";
|
||||
$error_message .= "INCORRECT:\n";
|
||||
$error_message .= " class {$class_name} extends Component {\n";
|
||||
$error_message .= " on_destroy() {\n";
|
||||
$error_message .= " // cleanup code\n";
|
||||
$error_message .= " }\n";
|
||||
$error_message .= " }\n\n";
|
||||
$error_message .= "CORRECT:\n";
|
||||
$error_message .= " class {$class_name} extends Component {\n";
|
||||
$error_message .= " on_stop() {\n";
|
||||
$error_message .= " // cleanup code\n";
|
||||
$error_message .= " }\n";
|
||||
$error_message .= " }\n\n";
|
||||
$error_message .= "LIFECYCLE METHODS:\n";
|
||||
$error_message .= "- on_create(): Called when component is created (sync)\n";
|
||||
$error_message .= "- on_render(): Called after template renders (sync)\n";
|
||||
$error_message .= "- on_load(): Called to load async data\n";
|
||||
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
|
||||
$error_message .= "- on_stop(): Called when component is destroyed (sync)\n\n";
|
||||
$error_message .= "FIX:\n";
|
||||
$error_message .= "Rename 'on_destroy()' to 'on_stop()'\n";
|
||||
$error_message .= "==========================================";
|
||||
|
||||
throw new YoureDoingItWrongException($error_message);
|
||||
}
|
||||
}
|
||||
129
app/RSpade/upstream_changes/form_value_persistence_12_29.txt
Executable file
129
app/RSpade/upstream_changes/form_value_persistence_12_29.txt
Executable file
@@ -0,0 +1,129 @@
|
||||
FORM VALUE PERSISTENCE - MIGRATION GUIDE
|
||||
Date: 2025-12-29
|
||||
|
||||
SUMMARY
|
||||
|
||||
When using stale-while-revalidate caching, forms may render from cache and then
|
||||
re-render with fresh data after cache revalidation. If a user modifies an input
|
||||
between these two renders, their changes would be lost. This update adds automatic
|
||||
tracking and restoration of user-modified form values across re-renders within
|
||||
the same parent action context.
|
||||
|
||||
The system works by storing changed values on the parent component (or the form
|
||||
itself if no parent exists). When the form re-renders, cached values are merged
|
||||
back into the form state before widgets are populated.
|
||||
|
||||
AFFECTED FILES
|
||||
|
||||
Theme form component:
|
||||
- rsx/theme/components/forms/rsx_form.js
|
||||
|
||||
Widget components (contract requirement):
|
||||
- Any custom Widget that doesn't fire 'input' events won't participate in caching
|
||||
|
||||
CHANGES REQUIRED
|
||||
|
||||
1. Add on_render() Method to Rsx_Form
|
||||
|
||||
Add this method after on_create():
|
||||
|
||||
on_render() {
|
||||
// Form value persistence across cache revalidation re-renders.
|
||||
// When a form renders from cache, the user may change inputs before
|
||||
// the cache revalidates and the form re-renders with fresh data.
|
||||
// This system caches user changes and re-applies them after re-render.
|
||||
|
||||
// Determine cache storage location and key
|
||||
const cache_location = this.parent() || this;
|
||||
const cache_key = this.parent() ? `__formvals_${this._cid}` : '__this_formvals';
|
||||
|
||||
// Initialize cache if it doesn't exist
|
||||
if (!cache_location[cache_key]) {
|
||||
cache_location[cache_key] = {};
|
||||
}
|
||||
const cache = cache_location[cache_key];
|
||||
|
||||
// If cache has values from prior render, merge into state.values
|
||||
// These will be applied when on_ready calls vals()
|
||||
if (Object.keys(cache).length > 0) {
|
||||
Object.assign(this.state.values, cache);
|
||||
}
|
||||
|
||||
// Register input listeners on all widgets to track user changes
|
||||
const that = this;
|
||||
this.$.shallowFind('.Widget').each(function () {
|
||||
const $widget = $(this);
|
||||
const component = $widget.component();
|
||||
if (component && 'on' in component) {
|
||||
const widget_name = $widget.data('name');
|
||||
if (widget_name) {
|
||||
component.on('input', function (comp, value) {
|
||||
cache[widget_name] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
2. Widget Contract Requirements
|
||||
|
||||
For form value persistence to work, Widget components must:
|
||||
|
||||
a) Have a data-name attribute (set via $name on Form_Field)
|
||||
b) Implement component.on('input', callback) event registration
|
||||
c) Fire the 'input' event when the user changes the value
|
||||
d) NOT fire 'input' when val() is called programmatically
|
||||
|
||||
Standard framework widgets (Text_Input, Select_Input, Checkbox_Input, etc.)
|
||||
already meet these requirements. Custom widgets should be verified.
|
||||
|
||||
HOW IT WORKS
|
||||
|
||||
1. First Render (from cache):
|
||||
- on_create: parses $data into this.state.values
|
||||
- on_render: initializes cache on parent, registers input listeners
|
||||
- on_ready: loads values into widgets via vals()
|
||||
- User modifies a checkbox
|
||||
- Input listener fires, stores { checkbox_name: new_value } in cache
|
||||
|
||||
2. Second Render (cache revalidated):
|
||||
- on_create: parses fresh $data into this.state.values
|
||||
- on_render: finds existing cache, merges cached values into this.state.values
|
||||
- on_ready: loads merged values (server data + user changes) into widgets
|
||||
- User's checkbox change is preserved
|
||||
|
||||
Cache Storage:
|
||||
- If form has a parent: stored at parent.__formvals_{form_cid}
|
||||
- If form has no parent: stored at form.__this_formvals
|
||||
|
||||
Cache Lifecycle:
|
||||
- Created when form first renders
|
||||
- Persists as long as parent component exists
|
||||
- Automatically cleared when parent is destroyed (navigating away)
|
||||
|
||||
CONFIGURATION
|
||||
|
||||
No configuration required. The feature is automatic.
|
||||
|
||||
VERIFICATION
|
||||
|
||||
1. Open a page with a form that uses stale-while-revalidate caching
|
||||
|
||||
2. Quickly modify a form input (checkbox, text field, etc.) before
|
||||
the cache revalidates
|
||||
|
||||
3. Wait for the form to re-render with fresh data
|
||||
|
||||
4. Verify that your change is preserved after re-render
|
||||
|
||||
5. In browser console, you can inspect the cache:
|
||||
// If form has a parent:
|
||||
Spa.action.__formvals_form
|
||||
|
||||
// Or check what's stored:
|
||||
console.log(Object.keys(Spa.action).filter(k => k.startsWith('__formvals')));
|
||||
|
||||
REFERENCE
|
||||
|
||||
php artisan rsx:man form_conventions
|
||||
rsx/theme/components/forms/rsx_form.js
|
||||
Reference in New Issue
Block a user