Patching Components
The patch system allows modules to extend or modify components defined in other modules without changing their source code. This is essential for addon modules that need to enhance existing functionality.
How Patching Works
Section titled “How Patching Works”When you patch a component:
- Framework stores the patch with its options (priority, conditions, dependencies)
- Patches are sorted by priority and dependency order
- Each patch wraps the previous result, starting from the original
- The final patched component is stored in the registry
- All instances are re-mounted with the patched version
Original Component Patch A (priority: 10) Patch B (priority: 20)┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ ProductCard │ → │ + Discount │ → │ + Loyalty ││ │ │ Badge │ │ Points │└─────────────────┘ └─────────────────┘ └─────────────────┘Basic Patching
Section titled “Basic Patching”Wrapping a Component
Section titled “Wrapping a Component”Add content around an existing component:
const { patch, html } = window.fullfinity;
patch('ProductCard', (Original) => function(props) { return html` <div class="product-card-wrapper"> <${Original} ...${props} /> <div class="loyalty-badge"> +10 points with purchase </div> </div> `;});With Options
Section titled “With Options”patch('ProductCard', (Original) => function(props) { return html` <${Original} ...${props} /> <div class="loyalty-badge">+10 pts</div> `;}, { id: 'loyalty-badge', // Unique identifier (required for unpatch/after) priority: 20, // Lower runs first (default: 10)});Priority Ordering
Section titled “Priority Ordering”Patches are applied in priority order (lower number = earlier):
// This runs second (priority 20)patch('Cart', addLoyaltyDisplay, { id: 'loyalty', priority: 20 });
// This runs first (priority 10)patch('Cart', addDiscountBadge, { id: 'discount', priority: 10 });
// Result: Original → Discount → LoyaltyWhen priorities are equal, patches apply in registration order (module load order).
Patch Dependencies
Section titled “Patch Dependencies”Use after to ensure one patch runs after another:
// Patch A - no dependenciespatch('Checkout', addCouponField, { id: 'coupon-field' });
// Patch B - must run after coupon-fieldpatch('Checkout', addCouponValidation, { id: 'coupon-validation', after: 'coupon-field' // Runs after coupon-field regardless of priority});
// Multiple dependenciespatch('Checkout', addCouponSummary, { id: 'coupon-summary', after: ['coupon-field', 'coupon-validation']});Conditional Patches
Section titled “Conditional Patches”Apply patches only when conditions are met:
patch('CheckoutButton', addSubscriptionUpsell, { id: 'subscription-upsell', when: () => !window.currentUser?.hasSubscription});
// Patch is evaluated each time component rebuildspatch('PriceDisplay', addMemberDiscount, { id: 'member-discount', when: () => window.currentUser?.isMember});The when function is called during component rebuild. If it returns false, the patch is skipped.
Removing Patches
Section titled “Removing Patches”Use unpatch() to remove a patch at runtime:
const { patch, unpatch } = window.fullfinity;
// Add a patchpatch('Header', addPromoBanner, { id: 'promo-banner' });
// Later, remove itunpatch('Header', 'promo-banner');This rebuilds the component without the removed patch.
Listing Patches
Section titled “Listing Patches”See all patches applied to a component:
const { listPatches } = window.fullfinity;
console.log(listPatches('ProductCard'));// ['discount-badge', 'loyalty-badge', 'inventory-status']Method-Level Patching
Section titled “Method-Level Patching”For class-based or object components, patch specific methods with _super support.
Supports deferred patching (can patch before component exists) and multiple patches with priority.
const { patchMethod } = window.fullfinity;
// Patch a method with access to original via _superpatchMethod('CartWidget', 'computeTotal', function(_super, items) { const total = _super(items); // Call original return total * 0.9; // Apply 10% discount}, { id: 'discount', priority: 10 });
// Multiple patches stack - this runs after discount (higher priority = later)patchMethod('CartWidget', 'computeTotal', function(_super, items) { const total = _super(items); // Gets discounted total return total + 5; // Add shipping}, { id: 'shipping', priority: 20 });
// Result: original() → discount patch → shipping patch// So: (100 * 0.9) + 5 = 95Deferred Method Patching
Section titled “Deferred Method Patching”You can patch methods before the component exists:
// Works even if CartWidget isn't registered yetpatchMethod('CartWidget', 'computeTotal', applyDiscount, { id: 'discount' });
// When CartWidget registers, the patch is applied automaticallyRemoving Method Patches
Section titled “Removing Method Patches”const { unpatchMethod } = window.fullfinity;
unpatchMethod('CartWidget', 'computeTotal', 'discount');Listing Method Patches
Section titled “Listing Method Patches”const { listMethodPatches } = window.fullfinity;
console.log(listMethodPatches('CartWidget', 'computeTotal'));// ['discount', 'shipping']Extension Slots
Section titled “Extension Slots”Slots let components define extension points where addons can inject UI:
Defining Slots (in the component)
Section titled “Defining Slots (in the component)”Use useSlot to get slot contents and automatically re-render when addons register:
const { registry, html, useSlot } = window.fullfinity;
registry.component('ProductCard', function(props) { // useSlot re-renders when slot contents change const afterPrice = useSlot('ProductCard:afterPrice'); const beforeTitle = useSlot('ProductCard:beforeTitle');
return html` <div class="product-card"> ${beforeTitle.map(C => html`<${C} ...${props} />`)} <h3>${props.name}</h3> <span class="price">${props.price}</span> ${afterPrice.map(C => html`<${C} ...${props} />`)} </div> `;});Note: Use useSlot() instead of registry.getSlot() to ensure the component re-renders when addons register into the slot after initial mount.
Registering into Slots (from addons)
Section titled “Registering into Slots (from addons)”const { registry, html } = window.fullfinity;
// Simple slot registrationfunction LoyaltyPoints({ price }) { const points = Math.floor(price / 10); return html`<span class="loyalty-points">+${points} pts</span>`;}
registry.slot('ProductCard:afterPrice', LoyaltyPoints);
// With optionsregistry.slot('ProductCard:afterPrice', DiscountBadge, { priority: 5, // Lower runs first id: 'discount-badge', // For removal when: () => window.discountsEnabled});Removing from Slots
Section titled “Removing from Slots”registry.removeSlot('ProductCard:afterPrice', 'discount-badge');Listing Slots
Section titled “Listing Slots”// All slots for a componentregistry.listSlots('ProductCard');// ['ProductCard:beforeTitle', 'ProductCard:afterPrice']
// All slotsregistry.listSlots();Slots vs Patches
Section titled “Slots vs Patches”| Use Case | Use Slots | Use Patches |
|---|---|---|
| Add UI to predefined points | ✓ | |
| Modify existing behavior | ✓ | |
| Multiple addons add to same spot | ✓ | |
| Wrap entire component | ✓ | |
| Intercept/modify props | ✓ | |
| Component defines extension points | ✓ |
Deferred Patching
Section titled “Deferred Patching”If you patch a component that hasn’t been registered yet, the patch is automatically deferred:
// This works even if 'FutureComponent' doesn't exist yetpatch('FutureComponent', (Original) => function(props) { // When FutureComponent is eventually registered, // this patch will be applied automatically return html` <div class="enhanced"> <${Original} ...${props} /> </div> `;}, { id: 'enhancement' });Multiple Patches
Section titled “Multiple Patches”Multiple modules can patch the same component. Order is determined by:
- Priority (lower first)
- Dependencies (
afterconstraints) - Registration order (module load order)
// Module A (priority 10)patch('Cart', addDiscountField, { id: 'discount', priority: 10 });
// Module B (priority 20, after discount)patch('Cart', addDiscountValidation, { id: 'discount-validation', priority: 20, after: 'discount'});
// Module C (priority 15, but no dependency)patch('Cart', addLoyaltyDisplay, { id: 'loyalty', priority: 15 });
// Execution order: discount → loyalty → discount-validationCommon Patterns
Section titled “Common Patterns”Add a Tab to Existing Component
Section titled “Add a Tab to Existing Component”patch('ProductTabs', (Original) => function(props) { const { html } = window.fullfinity;
const extendedTabs = [ ...(props.tabs || []), { id: 'reviews', label: 'Reviews', content: html`<${ReviewsPanel} productId=${props.productId} />` } ];
return html`<${Original} ...${props} tabs=${extendedTabs} />`;}, { id: 'reviews-tab' });Intercept Form Submission
Section titled “Intercept Form Submission”patch('ContactForm', (Original) => function(props) { const { html } = window.fullfinity;
function handleSubmit(data) { // Add tracking analytics.track('contact_form_submit', data); // Call original handler props.onSubmit?.(data); }
return html`<${Original} ...${props} onSubmit=${handleSubmit} />`;}, { id: 'form-tracking' });Conditionally Modify Behavior
Section titled “Conditionally Modify Behavior”patch('CheckoutButton', (Original) => function(props) { const { useState, useEffect, html } = window.fullfinity; const [hasSubscription, setHasSubscription] = useState(false);
useEffect(() => { fetch('/api/user/subscription') .then(r => r.json()) .then(data => setHasSubscription(data.active)); }, []);
if (!hasSubscription) { return html` <div class="checkout-upsell"> <${Original} ...${props} /> <a href="/subscribe" class="upgrade-link"> Subscribe for free shipping! </a> </div> `; }
return html`<${Original} ...${props} />`;}, { id: 'subscription-upsell' });Best Practices
Section titled “Best Practices”1. Always Use IDs
Section titled “1. Always Use IDs”// Good: Has ID for debugging and removalpatch('Button', patcher, { id: 'my-module-button-patch' });
// Bad: No ID - can't unpatch or reference in 'after'patch('Button', patcher);2. Always Pass Props Through
Section titled “2. Always Pass Props Through”// Good: Spread props to preserve all original functionalitypatch('Button', (Original) => function(props) { return html`<${Original} ...${props} />`;}, { id: 'button-wrapper' });
// Bad: Only passing some props - onClick, className, etc. are lost!patch('Button', (Original) => function({ label }) { return html`<${Original} label=${label} />`;}, { id: 'broken-button' });3. Use Slots for Multiple Extensions
Section titled “3. Use Slots for Multiple Extensions”If you expect multiple addons to add UI to the same spot, use slots instead of patches:
// Bad: Multiple patches fighting for same spotpatch('Header', addPromo, { id: 'promo' });patch('Header', addAnnouncement, { id: 'announcement' });
// Good: Component defines slot, addons register into itregistry.slot('Header:announcements', PromoBanner);registry.slot('Header:announcements', AnnouncementBar);4. Document Your Patches
Section titled “4. Document Your Patches”/** * Patch: ProductCard + Loyalty Points * * Module: ecommerce_loyalty * Patches: ecommerce.ProductCard * * Adds loyalty points badge and tracking to product cards. * Points are calculated based on product price. */patch('ProductCard', (Original) => function(props) { // Implementation...}, { id: 'loyalty-points', priority: 20 });Debugging
Section titled “Debugging”Check Patches on a Component
Section titled “Check Patches on a Component”// In browser consoleconst { listPatches, registry } = window.fullfinity;
// See all patch IDsconsole.log(listPatches('ProductCard'));
// Get original unpatched componentconst Original = registry.getOriginal('ProductCard');
// Get current patched componentconst Patched = registry.component('ProductCard');Log Patch Application
Section titled “Log Patch Application”patch('DebugComponent', (Original) => { console.log('Patching DebugComponent'); console.log('Received Original:', Original);
return function(props) { console.log('Rendering patched DebugComponent with props:', props); return html`<${Original} ...${props} />`; };}, { id: 'debug-patch' });API Reference
Section titled “API Reference”patch(name, patcher, options?)
Section titled “patch(name, patcher, options?)”| Parameter | Type | Description |
|---|---|---|
name | string | Component name to patch |
patcher | function | (Original) => PatchedComponent |
options.id | string | Unique identifier |
options.priority | number | Lower runs first (default: 10) |
options.after | string | string[] | Run after these patch IDs |
options.when | function | Condition function () => boolean |
unpatch(name, id)
Section titled “unpatch(name, id)”Remove a patch by ID. Component is rebuilt without it.
patchMethod(name, methodName, handler, options?)
Section titled “patchMethod(name, methodName, handler, options?)”| Parameter | Type | Description |
|---|---|---|
name | string | Component name |
methodName | string | Method to patch |
handler | function | (_super, ...args) => result |
options.priority | number | Lower runs first (default: 10) |
options.id | string | Unique identifier for removal |
unpatchMethod(name, methodName, id)
Section titled “unpatchMethod(name, methodName, id)”Remove a method patch by ID.
listPatches(name)
Section titled “listPatches(name)”Returns array of patch IDs for the component.
listMethodPatches(name, methodName)
Section titled “listMethodPatches(name, methodName)”Returns array of method patch IDs.
registry.slot(slotName, component, options?)
Section titled “registry.slot(slotName, component, options?)”| Parameter | Type | Description |
|---|---|---|
slotName | string | Slot name (e.g., ‘ProductCard:afterPrice’) |
component | function | Component to inject |
options.priority | number | Lower runs first (default: 10) |
options.id | string | Unique identifier for removal |
options.when | function | Condition function |
registry.getSlot(slotName)
Section titled “registry.getSlot(slotName)”Returns array of components registered in the slot (filtered by conditions).
registry.removeSlot(slotName, id)
Section titled “registry.removeSlot(slotName, id)”Remove a component from a slot by ID.
registry.listSlots(componentName?)
Section titled “registry.listSlots(componentName?)”List slot names. If componentName provided, filters to slots starting with that prefix.
registry.getOriginal(name)
Section titled “registry.getOriginal(name)”Get the original unpatched component.