Skip to Content
  • Home
  • Book
  • Blog
  • About us
  • Help
  • Contact us
  •  [email protected]
  • Sign in
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contact Us
Simplify It S.R.L.
      • Home
      • Book
      • Blog
      • About us
      • Help
      • Contact us
    •  [email protected]
    • English (US) Español (BO)
    • Sign in
    • Contact Us

    Chapter 17 Part 2: Advanced Bridge Patterns and Migration Strategies

  • All Blogs
  • OWL Book
  • Chapter 17 Part 2: Advanced Bridge Patterns and Migration Strategies
  • August 25, 2026 by
    Chapter 17 Part 2: Advanced Bridge Patterns and Migration Strategies
    Grover Menacho

    Why this chapter? Because bridging in only one direction isn't enough on a real project — you'll just as often need to reuse a battle-tested legacy widget (a signature pad, a barcode scanner integration) from inside a brand-new OWL screen, and you'll need a plan for eventually retiring the bridge altogether. What is Odoo trying to solve with this? Beyond basic interoperability, Odoo needs patterns robust enough for two-way communication and testing between old and new code, plus a realistic, incremental path off the legacy widget system rather than an indefinite dependency on it. Real-world application: Migration timelines like the one in this chapter are what I actually hand to clients — not "rewrite everything," but a phased plan that keeps their system running while legacy widgets get replaced module by module.

    In Part 1, we explored how to embed modern OWL components within legacy widgets. Now we'll tackle the reverse scenario, advanced communication patterns, and strategic approaches to complete modernization.


    Scenario 2: Using Legacy Widgets in OWL Components

    Sometimes you need to include complex legacy functionality in new OWL applications. This might be a specialized field widget, a complex third-party component, or legacy business logic that would take months to rewrite.

    Example: Including a Legacy Field Widget in an OWL Form

    Let's say you're building a modern OWL customer management interface, but you need to include a complex legacy signature widget that handles electronic signatures.

    1. The Modern OWL Customer Form

    /** @odoo-module **/
    
    import { Component, useState } from "@odoo/owl";
    import { useService } from "@web/core/utils/hooks";
    import { LegacyComponent } from "@web/legacy/legacy_component";
    
    export class CustomerForm extends Component {
        static template = "customer_management.CustomerForm";
        static components = { LegacyComponent };
    
        setup() {
            this.orm = useService("orm");
            this.notification = useService("notification");
    
            this.state = useState({
                customer: {
                    name: "",
                    email: "",
                    phone: "",
                    signature: null,
                },
                loading: false,
                signatureRequired: false,
            });
    
            // Configuration for legacy signature widget
            this.legacySignatureConfig = {
                Widget: "web_digital_sign.DigitalSignWidget", // Legacy widget class name
                widgetOptions: {
                    signatureMode: "draw",
                    required: true,
                    width: 400,
                    height: 200,
                    onSignatureChange: this.onSignatureChange.bind(this),
                    onSignatureClear: this.onSignatureClear.bind(this),
                }
            };
    
            // Configuration for legacy address widget (another example)
            this.legacyAddressConfig = {
                Widget: "base_address.AddressWidget",
                widgetOptions: {
                    countryField: "country_id",
                    stateField: "state_id", 
                    onAddressValidated: this.onAddressValidated.bind(this),
                }
            };
        }
    

    With the configuration in place, the component loads the customer record through the orm service, exactly like any regular OWL component — the legacy widgets don't change how you fetch data, only how part of the form is rendered:

        async loadCustomer(customerId) {
            this.state.loading = true;
            try {
                const customer = await this.orm.read("res.partner", [customerId], [
                    "name", "email", "phone", "signature", "street", "city", "country_id", "state_id"
                ]);
    
                if (customer.length > 0) {
                    this.state.customer = customer[0];
                    // Update legacy widget data if needed
                    this._updateLegacyWidgets();
                }
            } catch (error) {
                this.notification.add("Failed to load customer data", { type: "danger" });
            } finally {
                this.state.loading = false;
            }
        }
    
        async saveCustomer() {
            if (!this._validateForm()) {
                return;
            }
    
            this.state.loading = true;
            try {
                // Get data from legacy widgets before saving
                const signatureData = this._getLegacyWidgetData('signature');
                const addressData = this._getLegacyWidgetData('address');
    
                const customerData = {
                    ...this.state.customer,
                    signature: signatureData,
                    ...addressData,
                };
    
                if (customerData.id) {
                    await this.orm.write("res.partner", [customerData.id], customerData);
                } else {
                    customerData.id = await this.orm.create("res.partner", customerData);
                    this.state.customer.id = customerData.id;
                }
    
                this.notification.add("Customer saved successfully", { type: "success" });
    
            } catch (error) {
                this.notification.add("Failed to save customer", { type: "danger" });
            } finally {
                this.state.loading = false;
            }
        }
    
        // Callbacks for legacy widget interactions
        onSignatureChange(signatureData) {
            this.state.customer.signature = signatureData;
            this.state.signatureRequired = false;
            console.log("Signature updated:", signatureData);
        }
    
        onSignatureClear() {
            this.state.customer.signature = null;
            this.state.signatureRequired = true;
            console.log("Signature cleared");
        }
    
        onAddressValidated(addressData) {
            // Update customer data with validated address
            Object.assign(this.state.customer, addressData);
            console.log("Address validated:", addressData);
        }
    
        // Form validation including legacy widget validation
        _validateForm() {
            if (!this.state.customer.name.trim()) {
                this.notification.add("Customer name is required", { type: "warning" });
                return false;
            }
    
            if (!this.state.customer.email.trim()) {
                this.notification.add("Email is required", { type: "warning" });
                return false;
            }
    
            // Validate legacy widgets
            if (!this._validateLegacyWidget('signature')) {
                this.notification.add("Valid signature is required", { type: "warning" });
                return false;
            }
    
            if (!this._validateLegacyWidget('address')) {
                this.notification.add("Valid address is required", { type: "warning" });
                return false;
            }
    
            return true;
        }
    
        // Helper methods for legacy widget interaction
        _updateLegacyWidgets() {
            // Trigger updates in legacy widgets when OWL state changes
            // This would depend on the specific legacy widget APIs
        }
    
        _getLegacyWidgetData(widgetType) {
            // Extract data from legacy widgets
            // Implementation depends on how legacy widgets expose their data
            return {};
        }
    
        _validateLegacyWidget(widgetType) {
            // Validate legacy widget data
            // Implementation depends on specific widget validation APIs
            return true;
        }
    
        // Event handlers for OWL form elements
        onNameChange(event) {
            this.state.customer.name = event.target.value;
        }
    
        onEmailChange(event) {
            this.state.customer.email = event.target.value;
        }
    
        onPhoneChange(event) {
            this.state.customer.phone = event.target.value;
        }
    
        clearSignature() {
            // Programmatically clear legacy signature widget
            // This would call methods on the legacy widget instance
        }
    }
    

    Notice that saveCustomer pulls data out of the legacy widgets (via _getLegacyWidgetData) right before saving, rather than keeping that data in OWL state at all times — the legacy signature and address widgets are the source of truth for their own fields until the moment you need to persist them.

    2. The OWL Template

    <t t-name="customer_management.CustomerForm" owl="1">
        <div class="customer-form-container">
            <div class="form-header">
                <h2>Customer Information</h2>
                <div class="form-actions">
                    <button class="btn btn-primary" 
                            t-on-click="saveCustomer"
                            t-att-disabled="state.loading">
                        <t t-if="state.loading">Saving...</t>
                        <t t-else="">Save Customer</t>
                    </button>
                </div>
            </div>
    
            <!-- Modern OWL form fields -->
            <div class="row">
                <div class="col-md-6">
                    <div class="card modern-form-section">
                        <div class="card-header">
                            <h5>Basic Information (Modern OWL)</h5>
                        </div>
                        <div class="card-body">
                            <div class="form-group">
                                <label for="customer-name">Customer Name *</label>
                                <input type="text" 
                                       id="customer-name"
                                       class="form-control"
                                       t-att-value="state.customer.name"
                                       t-on-input="onNameChange"
                                       placeholder="Enter customer name"/>
                            </div>
    
                            <div class="form-group">
                                <label for="customer-email">Email *</label>
                                <input type="email" 
                                       id="customer-email"
                                       class="form-control"
                                       t-att-value="state.customer.email"
                                       t-on-input="onEmailChange"
                                       placeholder="[email protected]"/>
                            </div>
    
                            <div class="form-group">
                                <label for="customer-phone">Phone</label>
                                <input type="tel" 
                                       id="customer-phone"
                                       class="form-control"
                                       t-att-value="state.customer.phone"
                                       t-on-input="onPhoneChange"
                                       placeholder="+1 (555) 123-4567"/>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div class="col-md-6">
                    <!-- Legacy address widget -->
                    <div class="card legacy-widget-section">
                        <div class="card-header">
                            <h5>Address Information (Legacy Widget)</h5>
                        </div>
                        <div class="card-body">
                            <LegacyComponent
                                widget="legacyAddressConfig.Widget"
                                widgetOptions="legacyAddressConfig.widgetOptions"
                                record="state.customer"
                            />
                        </div>
                    </div>
                </div>
            </div>
    
            <!-- Legacy signature widget -->
            <div class="row">
                <div class="col-12">
                    <div class="card legacy-widget-section">
                        <div class="card-header">
                            <h5>Digital Signature (Legacy Widget)</h5>
                            <div class="card-actions">
                                <button class="btn btn-sm btn-secondary" 
                                        t-on-click="clearSignature">
                                    Clear Signature
                                </button>
                            </div>
                        </div>
                        <div class="card-body">
                            <div t-if="state.signatureRequired" class="alert alert-warning">
                                Please provide a digital signature
                            </div>
    
                            <!-- Legacy signature widget mounted here -->
                            <LegacyComponent
                                widget="legacySignatureConfig.Widget"
                                widgetOptions="legacySignatureConfig.widgetOptions"
                                record="state.customer"
                            />
                        </div>
                    </div>
                </div>
            </div>
    
            <!-- Loading overlay -->
            <div t-if="state.loading" class="loading-overlay">
                <div class="spinner-border" role="status">
                    <span class="sr-only">Loading...</span>
                </div>
            </div>
        </div>
    </t>
    

    3. Supporting CSS for Mixed Architecture

    /* Customer form specific styles */
    .customer-form-container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
    }
    
    .form-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 30px;
        padding-bottom: 15px;
        border-bottom: 2px solid #e9ecef;
    }
    
    /* Differentiate modern vs legacy sections */
    .modern-form-section {
        border-left: 4px solid #007bff;
    }
    
    .modern-form-section .card-header {
        background-color: #f8f9fa;
        color: #007bff;
    }
    
    .legacy-widget-section {
        border-left: 4px solid #ffc107;
    }
    
    .legacy-widget-section .card-header {
        background-color: #fff8e1;
        color: #856404;
    }
    
    .legacy-widget-section .card-header h5::after {
        content: " (Legacy)";
        font-size: 0.8em;
        opacity: 0.7;
    }
    
    /* Loading overlay */
    .loading-overlay {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(255, 255, 255, 0.8);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1000;
    }
    
    /* Form validation states */
    .form-group.has-error .form-control {
        border-color: #dc3545;
    }
    
    .form-group.has-success .form-control {
        border-color: #28a745;
    }
    

    Understanding the LegacyComponent Wrapper

    The LegacyComponent is a special OWL component provided by Odoo that acts as an adapter. Here's how it works internally:

    The LegacyComponent Implementation (Simplified)

    // This is a simplified version of how Odoo's LegacyComponent works
    import { Component, onMounted, onWillUnmount, useRef, xml } from "@odoo/owl";
    
    export class LegacyComponent extends Component {
        static template = xml`<div t-ref="legacyContainer"/>`;
    
        static props = {
            widget: String,
            widgetOptions: { type: Object, optional: true },
            record: { type: Object, optional: true },
        };
    
        setup() {
            this.legacyWidget = null;
            this.legacyContainerRef = useRef("legacyContainer");
    
            onMounted(() => {
                this.mountLegacyWidget();
            });
    
            onWillUnmount(() => {
                this.unmountLegacyWidget();
            });
        }
    
        async mountLegacyWidget() {
            try {
                // Get the legacy widget class by name
                const WidgetClass = this.env.services.legacy_widget_registry.get(this.props.widget);
    
                if (!WidgetClass) {
                    throw new Error(`Legacy widget '${this.props.widget}' not found`);
                }
    
                // Create instance of legacy widget
                this.legacyWidget = new WidgetClass(this, this.props.widgetOptions || {});
    
                // Set up data binding if record provided
                if (this.props.record) {
                    this.legacyWidget.set('record', this.props.record);
                }
    
                // Mount the legacy widget
                await this.legacyWidget.appendTo(this.legacyContainerRef.el);
    
                // Set up bidirectional communication
                this.setupLegacyEvents();
    
            } catch (error) {
                console.error('Failed to mount legacy widget:', error);
                this.showError(error.message);
            }
        }
    

    Once the widget is mounted, setupLegacyEvents wires up the two directions of communication: legacy events flow into OWL callbacks, and (if the caller provided one) an onValueChange prop is forwarded to the legacy widget's own event.

        setupLegacyEvents() {
            if (!this.legacyWidget) return;
    
            // Listen for legacy widget events
            this.legacyWidget.on('value_changed', this, this.onLegacyValueChange);
            this.legacyWidget.on('validation_error', this, this.onLegacyValidationError);
    
            // Forward OWL events to legacy widget
            if (this.props.widgetOptions.onValueChange) {
                this.legacyWidget.on('value_changed', this, (newValue) => {
                    this.props.widgetOptions.onValueChange(newValue);
                });
            }
        }
    
        onLegacyValueChange(newValue) {
            // Update the record if provided
            if (this.props.record && this.legacyWidget.field_name) {
                this.props.record[this.legacyWidget.field_name] = newValue;
            }
    
            // Trigger OWL re-render if needed
            this.render();
        }
    
        onLegacyValidationError(error) {
            console.warn('Legacy widget validation error:', error);
            // Could trigger OWL component to show error state
        }
    
        unmountLegacyWidget() {
            if (this.legacyWidget) {
                // Remove event listeners
                this.legacyWidget.off('value_changed', this);
                this.legacyWidget.off('validation_error', this);
    
                // Destroy the legacy widget
                this.legacyWidget.destroy();
                this.legacyWidget = null;
            }
        }
    
        showError(message) {
            // Render error state in OWL component
            this.legacyContainerRef.el.innerHTML = `
                <div class="alert alert-danger">
                    <strong>Widget Error:</strong> ${message}
                </div>
            `;
        }
    }
    

    Advanced Bridge Patterns

    1. Event Communication Between Legacy and OWL

    Often, you need sophisticated communication between legacy widgets and OWL components:

    // In the legacy widget
    const LegacyWidget = Widget.extend({
        start: function() {
            this._super(...arguments);
    
            // Listen for events from OWL components
            this.eventBus = core.bus;
            this.eventBus.on('owl_component_event', this, this._onOWLEvent);
        },
    
        _onOWLEvent: function(data) {
            console.log('Legacy widget received OWL event:', data);
            // React to OWL component events
            this._updateLegacyUI(data);
        },
    
        _triggerEventForOWL: function(data) {
            // Send events to OWL components
            this.eventBus.trigger('legacy_widget_event', data);
        },
    
        destroy: function() {
            this.eventBus.off('owl_component_event', this);
            this._super(...arguments);
        }
    });
    
    // In the OWL component
    export class ModernComponent extends Component {
        setup() {
            this.eventBus = useService("bus");
    
            // Listen for events from legacy widgets
            this.eventBus.addEventListener('legacy_widget_event', this.onLegacyEvent.bind(this));
        }
    
        onLegacyEvent(event) {
            console.log('OWL component received legacy event:', event.detail);
            // React to legacy widget events
        }
    
        sendEventToLegacy(data) {
            // Send events to legacy widgets
            this.eventBus.trigger('owl_component_event', data);
        }
    }
    

    2. Shared State Management

    For complex scenarios where legacy and OWL components need to share state:

    // Shared state service that both legacy and OWL can use
    odoo.define('shared_state.StateManager', function (require) {
        "use strict";
    
        const core = require('web.core');
    
        class SharedStateManager {
            constructor() {
                this.state = {};
                this.listeners = {};
            }
    
            setState(key, value) {
                const oldValue = this.state[key];
                this.state[key] = value;
    
                // Notify listeners
                if (this.listeners[key]) {
                    this.listeners[key].forEach(callback => {
                        callback(value, oldValue);
                    });
                }
    
                // Trigger global event
                core.bus.trigger('shared_state_changed', { key, value, oldValue });
            }
    
            getState(key) {
                return this.state[key];
            }
    
            subscribe(key, callback) {
                if (!this.listeners[key]) {
                    this.listeners[key] = [];
                }
                this.listeners[key].push(callback);
    
                // Return unsubscribe function
                return () => {
                    const index = this.listeners[key].indexOf(callback);
                    if (index > -1) {
                        this.listeners[key].splice(index, 1);
                    }
                };
            }
        }
    
        // Create singleton instance
        const sharedState = new SharedStateManager();
    
        return sharedState;
    });
    
    // Usage in legacy widget
    const LegacyWidget = Widget.extend({
        start: function() {
            this._super(...arguments);
            this.sharedState = require('shared_state.StateManager');
    
            // Subscribe to state changes
            this.unsubscribe = this.sharedState.subscribe('currentUser', (newUser) => {
                this._updateUserDisplay(newUser);
            });
        },
    
        _updateCurrentUser: function(userData) {
            this.sharedState.setState('currentUser', userData);
        },
    
        destroy: function() {
            if (this.unsubscribe) {
                this.unsubscribe();
            }
            this._super(...arguments);
        }
    });
    
    // Usage in OWL component
    export class ModernComponent extends Component {
        setup() {
            this.sharedState = useService("shared_state");
    
            this.state = useState({
                currentUser: this.sharedState.getState('currentUser')
            });
    
            // Subscribe to shared state changes
            this.unsubscribe = this.sharedState.subscribe('currentUser', (newUser) => {
                this.state.currentUser = newUser;
            });
        }
    
        updateUser(userData) {
            this.sharedState.setState('currentUser', userData);
        }
    
        willUnmount() {
            if (this.unsubscribe) {
                this.unsubscribe();
            }
        }
    }
    

    3. Progressive Migration Strategy

    Here's a systematic approach to migrating from legacy to OWL:

    // Migration wrapper that can switch between legacy and OWL implementations
    odoo.define('migration_wrapper.ComponentSwitcher', function (require) {
        "use strict";
    
        const Widget = require('web.Widget');
        const { mount } = require("@odoo/owl");
    
        const ComponentSwitcher = Widget.extend({
            init: function(parent, options) {
                this._super(...arguments);
                this.options = options;
                this.useOWL = this._shouldUseOWL();
            },
    
            _shouldUseOWL: function() {
                // Logic to determine whether to use OWL or legacy
                // Could be based on feature flags, user preferences, etc.
                return this.options.forceOWL || 
                       localStorage.getItem('use_owl_components') === 'true' ||
                       this.options.migrationPhase >= 2;
            },
    
            async start() {
                await this._super(...arguments);
    
                if (this.useOWL) {
                    await this._mountOWLComponent();
                } else {
                    await this._mountLegacyComponent();
                }
            },
    
            async _mountOWLComponent() {
                const { ModernComponent } = require('my_module.ModernComponent');
                this.owlComponent = await mount(ModernComponent, this.el, {
                    props: this.options.componentProps
                });
            },
    
            async _mountLegacyComponent() {
                const LegacyComponent = require('my_module.LegacyComponent');
                this.legacyComponent = new LegacyComponent(this, this.options.componentProps);
                await this.legacyComponent.appendTo(this.el);
            },
    
            destroy: function() {
                if (this.owlComponent) {
                    this.owlComponent.destroy();
                }
                if (this.legacyComponent) {
                    this.legacyComponent.destroy();
                }
                this._super(...arguments);
            }
        });
    
        return ComponentSwitcher;
    });
    

    Testing Bridge Components

    Testing components that use the legacy-OWL bridge requires special considerations:

    1. Testing OWL Components with Legacy Dependencies

    Odoo's modern test framework is @odoo/hoot (Chapter 16), not the legacy QUnit runner — the same conventions apply here: describe/test/expect instead of QUnit.module/QUnit.test/assert, and mountWithCleanup instead of a manual mount + destroy().

    import { describe, test, expect, beforeEach } from "@odoo/hoot";
    import { mountWithCleanup } from "@web/../tests/web_test_helpers";
    
    describe("Bridge Components", () => {
        let mockLegacyWidget;
        let widgetWasCreated;
        let dataWasFetched;
    
        beforeEach(() => {
            widgetWasCreated = false;
            dataWasFetched = false;
    
            // Plain mock object standing in for the legacy widget instance
            mockLegacyWidget = {
                start: () => Promise.resolve(),
                destroy: () => {},
                getData: () => {
                    dataWasFetched = true;
                    return { signature: "mock_signature" };
                },
                validate: () => true,
            };
    
            // Mock the legacy widget constructor referenced by name
            window.MockLegacyWidget = function () {
                widgetWasCreated = true;
                return mockLegacyWidget;
            };
        });
    
        test("OWL component integrates with legacy widget", async () => {
            const customerForm = await mountWithCleanup(CustomerForm, {
                props: {
                    legacyWidgetConfig: {
                        Widget: "MockLegacyWidget",
                        widgetOptions: { required: true },
                    },
                },
            });
    
            // The legacy widget should have been instantiated
            expect(widgetWasCreated).toBe(true);
    
            // Data exchange with the legacy widget should have happened
            await customerForm.saveCustomer();
            expect(dataWasFetched).toBe(true);
    
            // No manual destroy() needed — mountWithCleanup unmounts automatically
            // after the test.
        });
    });
    

    Migration Best Practices

    1. Start Small and Iterative

    // Phase 1: Add new OWL components to legacy screens
    // - Add modern charts to existing dashboards
    // - Introduce new interactive widgets
    // - Keep legacy functionality intact
    
    // Phase 2: Replace non-critical legacy components
    // - Migrate simple widgets first
    // - Replace form controls and basic UI elements
    // - Use bridge for complex interactions
    
    // Phase 3: Migrate core functionality
    // - Replace main form widgets
    // - Migrate list views and kanban views
    // - Maintain bridge only for edge cases
    
    // Phase 4: Full modernization
    // - Complete migration to OWL
    // - Remove bridge code
    // - Optimize performance
    

    2. Migration Timeline Example

    Quarter 1: Foundation - Set up bridge infrastructure - Migrate 2-3 simple components - Train team on bridge patterns - Establish testing procedures

    Quarter 2: Acceleration - Migrate 5-10 medium complexity components - Build reusable bridge utilities - Optimize performance bottlenecks - Document best practices

    Quarter 3: Major Features - Migrate core business logic components - Replace main form and list interfaces - Minimize bridge usage - Performance optimization

    Quarter 4: Completion - Migrate remaining edge cases - Remove bridge code - Full OWL implementation - Performance validation

    3. Success Metrics

    Track your migration progress:

    const MigrationMetrics = {
        totalComponents: 50,
        migratedToOWL: 35,
        usingBridge: 10,
        remainingLegacy: 5,
    
        get migrationProgress() {
            return (this.migratedToOWL / this.totalComponents) * 100;
        },
    
        get bridgeUsage() {
            return (this.usingBridge / this.totalComponents) * 100;
        },
    
        generateReport() {
            return {
                migrationProgress: `${this.migrationProgress.toFixed(1)}%`,
                bridgeUsage: `${this.bridgeUsage.toFixed(1)}%`,
                componentsRemaining: this.remainingLegacy,
                estimatedCompletionTime: `${Math.ceil(this.remainingLegacy / 5)} sprints`
            };
        }
    };
    
    console.table(MigrationMetrics.generateReport());
    

    Performance Considerations

    1. Bundle Size Management

    When bridging legacy and OWL, be mindful of JavaScript bundle size:

    // Lazy loading to avoid loading unnecessary code
    const loadLegacyWidget = async (widgetName) => {
        // Only load legacy widget when needed
        const { [widgetName]: Widget } = await import(`./legacy_widgets/${widgetName}`);
        return Widget;
    };
    
    const loadOWLComponent = async (componentName) => {
        // Only load OWL component when needed
        const { [componentName]: Component } = await import(`./owl_components/${componentName}`);
        return Component;
    };
    

    2. Memory Management

    Proper cleanup is crucial when mixing legacy and OWL:

    const BridgeManager = {
        components: new Map(),
    
        async mountOWLInLegacy(owlComponent, target, props) {
            const component = await mount(owlComponent, target, { props });
            this.components.set(target, { type: 'owl', instance: component });
            return component;
        },
    
        mountLegacyInOWL(legacyWidget, target, options) {
            const widget = new legacyWidget(null, options);
            widget.appendTo(target);
            this.components.set(target, { type: 'legacy', instance: widget });
            return widget;
        },
    
        cleanup(target) {
            const component = this.components.get(target);
            if (component) {
                if (component.type === 'owl') {
                    component.instance.destroy();
                } else if (component.type === 'legacy') {
                    component.instance.destroy();
                }
                this.components.delete(target);
            }
        },
    
        cleanupAll() {
            this.components.forEach((component, target) => {
                this.cleanup(target);
            });
        }
    };
    
    // Ensure cleanup on page unload
    window.addEventListener('beforeunload', () => {
        BridgeManager.cleanupAll();
    });
    

    Common Pitfalls

    Forgetting onWillUnmount on the LegacyComponent wrapper. If unmountLegacyWidget isn't called when the OWL component is destroyed, the legacy widget instance and its DOM event listeners stay alive forever — this is the single most common source of memory leaks in bridge code.

    Reading legacy widget state as if it were reactive. this.legacyWidget.field_name or similar values are plain properties on a legacy object; OWL has no way to know when they change. Any time the legacy widget's data needs to affect OWL rendering, it must flow through an explicit event (like value_changed) that updates this.state.

    Building a new "shared state" mechanism per bridge instead of reusing one. The SharedStateManager pattern shown above is meant to be a single registered service, not something you re-invent for every legacy/OWL pair — otherwise legacy widgets and OWL components end up talking to different, disconnected state objects.

    Skipping the migration plan. It's tempting to leave a bridge component in place indefinitely because "it works." Every bridge component should have a documented target date or trigger condition for full migration — otherwise the bridge layer itself becomes permanent technical debt.

    Exercises

    1. Extend the CustomerForm example so the legacy signature widget's onSignatureChange callback also calls a new _markFormDirty() method, and use it to disable the Save button until the user provides a signature.
    2. Using the LegacyComponent implementation as a reference, write (on paper or in code) the onWillUnmount hook you'd need if LegacyComponent didn't already provide one — what exactly must be cleaned up, and in what order?
    3. Look at the SharedStateManager example: rewrite subscribe so a component that forgets to call the returned unsubscribe function doesn't cause a memory leak (hint: consider what happens if subscribe is called every time a component mounts, but unsubscribe is only called manually).

    Conclusion: Bridging the Past and Future

    The legacy-OWL bridge represents more than just a technical solution—it's a strategic approach to software evolution. It acknowledges the reality that in professional software development, you rarely have the luxury of starting from scratch. Instead, you must build the future while maintaining the present.

    Throughout this book, you've learned:

    1. Modern JavaScript fundamentals that power contemporary web development
    2. OWL component architecture for building maintainable, scalable interfaces
    3. Advanced patterns like state management, composition, and testing
    4. Real-world integration techniques for working with existing systems

    The bridge patterns in this chapter complete your toolkit, giving you the skills to:

    • Modernize incrementally without breaking existing functionality
    • Integrate smoothly between different architectural paradigms
    • Manage complexity in mixed-technology environments
    • Plan migrations strategically with measurable progress

    Your Next Steps

    As you apply these concepts in your own projects:

    1. Start small: Choose low-risk components for your first bridge implementations
    2. Document everything: Clear documentation helps your team understand the migration strategy
    3. Test thoroughly: Bridge components require testing both sides of the integration
    4. Plan the future: Every bridge component should have a migration path to pure OWL
    5. Share knowledge: Help your team understand both legacy and modern patterns

    The Professional Developer's Mindset

    Professional Odoo developers understand that mastery isn't just about knowing the latest technology—it's about knowing when and how to apply the right tool for each situation. Sometimes that's cutting-edge OWL components. Sometimes it's reliable legacy widgets. Often, it's the bridge between them.

    The skills you've learned in this book will serve you well as Odoo continues to evolve. You now have the foundation to adapt to new frameworks, integrate with future technologies, and most importantly, deliver value to users while maintaining system stability.

    Final Challenge

    As you close this book, here's a challenge to cement your learning:

    Build a complete Odoo module that demonstrates everything you've learned: 1. Modern OWL components with hooks and state management 2. Integration with Odoo services and backend models 3. Comprehensive test suite 4. Bridge integration with at least one legacy component 5. Professional documentation and code organization

    This project will serve as your portfolio piece and proof of mastery. More importantly, it will give you the confidence to tackle any OWL development challenge that comes your way.

    Welcome to the ranks of professional Odoo developers. You're ready to build the future, one component at a time.

    Happy coding, and may your bridges be strong and your migrations smooth!

    TL;DR: Legacy widgets can also live inside OWL components (via LegacyComponent + onWillUnmount cleanup), two-way state sharing needs one reusable service rather than a bridge-specific hack, and every bridge component should carry an explicit migration deadline so it doesn't become permanent.

    Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch17_2_ex1. Installation instructions are in the repository README.

    What's Next?

    You now have OWL 2.0 covered end to end — the final chapter is a look at where the framework is heading next, so you're not caught off guard when OWL 3 eventually arrives.


    End of Chapter 17

    in OWL Book
    Chapter 18: OWL 3 — What's Next (in Alpha)
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Useful Links
    • Home
    • About us
    • Blog
    • The OWL 2.0 book
    • Help
    • Contact us
    About us

    Simplify It S.R.L. is an Odoo implementation and development firm based in La Paz, Bolivia, working with companies across Latin America and North America. We have been building on Odoo since version 6.1: custom modules, version migrations and Bolivian localization.

    We are also the authors of the OWL 2.0 book, published free chapter by chapter on our blog.

    Connect with us
    • Contact us
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Follow us
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Powered by Odoo - Create a free website