Search code examples
extjsextjs6-classic

Extjs uses child-viewmodel data when parent-viewmodel is desired


Consider such snippet (which can be run at https://fiddle.sencha.com/, and at top right combobox choosing classic instead of modern):

Ext.define('ReusableComponent', {
    xtype: 'reusable',
    extend: 'Ext.Container',
    
    config: {
        foo: 'Foo'
    },
    
    updateFoo: function(foo) {
        this.getViewModel().set('foo', foo);  
    },
    
    viewModel: {
        data: {
            foo: 'Foo'
        }
    },
    
    items: [{
        bind: {
            html: 'foo = {foo}'
        }
    }]
});

Ext.define('ConcreteComponent', {
    extend: 'Ext.panel.Panel',
    
    viewModel: {
        data: {
            foo: 'Bar'
        }
    },
    
    layout: 'fit',
    items: [{
        xtype: 'reusable',
        bind: {
            foo: '{foo}'
        }
    }]
});

Ext.application({
    name : 'Fiddle',

    launch : function() {
        Ext.create('ConcreteComponent', {
            renderTo: Ext.getBody(),
            title: 'ConcreteComponent',
            width: 200,
            height: 200
        });
    }
});

The dream is to have a reusable component, which would have a defined external interface, and knowing it should be enough. Those who use that component should not have to know its internals. In this example, external interface is common config it inherits from Ext.Container (like width/height/etc), and foo config.

So say I then try to use it in ConcreteComponent. I know the reusable component has config foo, and thus I should be able to bind it to my own viewmodel, and that's what I do. However this doesn't work, and it shows foo = Foo, instead of (expected) foo = Bar. It seems clear why -- I unknowingly used name already present in the child's viewmodel, and extjs picks that instead of what I defined in ConcreteComponent. It's also clear how to bandaid-fix this (in ConcreteComponent rename viewmodel data property from foo to foo2 for example). But that forces to know internals of that reusable component, not just its public interface. Is there anyway to solve this? Or should children viewmodels always be considered part of their public interface no matter what?


Solution

  • Looks like the solution was to simply create a viewmodel manually under a private property (which breaks up viewmodels' child-parent chain extjs creates between component's viewmodel and its container's viewmodel https://docs.sencha.com/extjs/6.2.0/classic/Ext.Component.html#cfg-viewModel), and pass it to reusable component's children explicitly via viewModel config. Using defaults seems to work fine. I saw the solution when stumbling upon color picker's source code https://docs.sencha.com/extjs/6.2.0/classic/src/Selector.js.html . Here is question's fixed code

    Ext.define('ReusableComponent', {
        xtype: 'reusable',
        extend: 'Ext.Container',
        
        config: {
            foo: 'Foo'
        },
        
        updateFoo: function(foo) {
            this.childViewModel.set('foo', foo);  
        },
        
        constructor: function() {
            this.childViewModel = Ext.create('Ext.app.ViewModel', {
                data: {
                    foo: 'Foo'
                }
            });
            this.defaults = {
                viewModel: this.childViewModel
            };
            this.callParent(arguments);
        },
        
        items: [{
            bind: {
                html: 'foo = {foo}'
            }
        }]
    });
    
    Ext.define('ConcreteComponent', {
        extend: 'Ext.panel.Panel',
        
        viewModel: {
            data: {
                foo: 'Bar'
            }
        },
        
        layout: 'fit',
        items: [{
            xtype: 'reusable',
            bind: {
                foo: '{foo}'
            }
        }]
    });
    
    Ext.application({
        name : 'Fiddle',
    
        launch : function() {
            Ext.create('ConcreteComponent', {
                renderTo: Ext.getBody(),
                title: 'ConcreteComponent',
                width: 200,
                height: 200
            });
        }
    });