1. #1
    Sencha User
    Join Date
    Feb 2012
    Posts
    14
    Vote Rating
    1
    dmfr is on a distinguished road

      1  

    Default Ext.ux.ComponentRowExpander, component in grid row expander

    Hi,

    I am sharing a plugin extending Ext.ux.RowExpander to insert any component into the row expander.
    Do not hesitate to give feedback or share improvements if you find it useful.

    So far it has been (not so extensively) tested with ExtJS 4.1.x only.

    It features :
    - Rendering component inside RowExpander's rowbody
    - Monitoring view refresh to re-append existing component to new rowbodies
    - Monitoring column resize to trigger component layout
    - Garbage collection

    It lacks (for now):
    - smart monitoring of store / records to refresh components if needed (ie. when records are edited locally)


    How to use :
    Just override the createComponent method to configure and create the desired component.


    Example :

    Code:
    Ext.create('Ext.grid.Panel',{
      ....
      plugins:[{
        ptype:'cmprowexpander',
        expandOnDblClick: true,
        createComponent: function(view,record,htmlnode,index) {
          return Ext.create('Any.Component',{...}) ;
        }
      }]
    }) ;

    Source :
    Code:
    /*
     * Inspired by : http://www.rahulsingla.com/blog/2010/04/extjs-preserving-rowexpander-markup-across-view-refreshes
     * Reappend element to DOM : http://stackoverflow.com/questions/20143082/does-extjs-automatically-garbage-collect-components
     */
    
    Ext.define('Ext.ux.ComponentRowExpander', {
        extend: 'Ext.ux.RowExpander',
    
        alias: 'plugin.cmprowexpander',
    
        rowBodyTpl : ['<div></div>'],
        
        obj_recordId_componentId: {},
         
        init: function(grid) {
            this.callParent(arguments) ;
            
            var view = grid.getView() ;
            view.on('refresh', this.onRefresh, this);
            view.on('expandbody', this.onExpand, this);
            
            grid.on('destroy', this.onDestroyGrid, this) ;
            grid.headerCt.on('columnresize', this.onColumnResize, this) ;
            
            this.obj_recordId_componentId = {} ;
        },
        
        getRecordKey: function(record) {
            return (record.internalId);
        },
        
        createComponent: function(view, record, rowNode, rowIndex) {
            return Ext.create('Ext.Component') ;
        },
        
        onExpand: function(rowNode, record, expandRow) {
            var recordId = this.getRecordKey(record) ;
            if( Ext.isEmpty( this.obj_recordId_componentId[recordId] ) ) {
                var view = this.grid.getView(),
                    newComponent = this.createComponent(view, record, rowNode, view.indexOf(rowNode)),
                    targetRowbody = Ext.DomQuery.selectNode('div.x-grid-rowbody', expandRow) ;
                
                while (targetRowbody.hasChildNodes()) {
                    targetRowbody.removeChild(targetRowbody.lastChild);
                }
                newComponent.render( targetRowbody ) ;
                
                this.obj_recordId_componentId[recordId] = newComponent.getId() ;
            }
        },
        
        onRefresh: function(view) {
            var reusedCmpIds = [] ;
            Ext.Array.each( view.getNodes(), function(node) {
                var record = view.getRecord(node),
                    recordId = this.getRecordKey(record) ;
                    
                if( !Ext.isEmpty(this.obj_recordId_componentId[recordId]) ) {
                    var cmpId = this.obj_recordId_componentId[recordId] ;
                    
                    reusedCmpIds.push(cmpId) ;
                    var reusedComponent = Ext.getCmp(this.obj_recordId_componentId[recordId]),
                        targetRowbody = Ext.DomQuery.selectNode('div.x-grid-rowbody', node);
                    while (targetRowbody.hasChildNodes()) {
                        targetRowbody.removeChild(targetRowbody.lastChild);
                    }
                    
                    targetRowbody.appendChild( reusedComponent.getEl().dom );
                    reusedComponent.doComponentLayout() ;
                }
            },this) ;
            
            
            // Do Garbage collection
            // Method 1 ( http://skirtlesden.com/static/ux/download/component-column/1.1/Component.js )
            var keysToDelete = [] ;
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, testCmpId ) {
                comp = Ext.getCmp(testCmpId);
                el = comp && comp.getEl();
    
                if (!el || (true && (!el.dom || Ext.getDom(Ext.id(el)) !== el.dom))) {
                    // The component is no longer in the DOM
                    if (comp && !comp.isDestroyed) {
                        comp.destroy();
                        keysToDelete.push(recordId) ;
                    }
                }
            }) ;
            
            // Method 2
            /*
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, testCmpId ) {
                if( !Ext.Array.contains( reusedCmpIds, testCmpId ) ) {
                    comp = Ext.getCmp(testCmpId);
                    comp.destroy();
                    keysToDelete.push(recordId) ;
                }
            }) ;
            */
            
            // Clean map
            Ext.Array.each( keysToDelete, function(mkey) {
                delete this.obj_recordId_componentId[mkey] ;
            },this);
        },
        
        onColumnResize: function() {
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, cmpId ) {
                Ext.getCmp(cmpId).doComponentLayout();
            }) ;
        },
         
        onDestroyGrid: function() {
            Ext.Object.each( this.obj_recordId_componentId, function(recordId, cmpId) {
                Ext.getCmp(cmpId).destroy() ;
            }) ;
         }
    });

  2. #2
    Sencha - Support Team
    Join Date
    Feb 2013
    Location
    California
    Posts
    6,591
    Vote Rating
    168
    Gary Schlosberg is a splendid one to behold Gary Schlosberg is a splendid one to behold Gary Schlosberg is a splendid one to behold Gary Schlosberg is a splendid one to behold Gary Schlosberg is a splendid one to behold Gary Schlosberg is a splendid one to behold

      0  

    Default

    Very cool. Thanks for sharing this with the rest of us!
    Are you a Sencha products veteran who has wondered what it might be like to work at Sencha? If so, please reach out to our human resources manager: fabienne.bell@sencha.com

  3. #3
    Sencha User
    Join Date
    Oct 2010
    Posts
    45
    Vote Rating
    2
    undeclared is on a distinguished road

      0  

    Default

    Actually works on Ext 4.2.1. Doesn't remember components though, working on that, and I will most likely post the solution here once I figure it out.

  4. #4
    Sencha User
    Join Date
    Oct 2010
    Posts
    45
    Vote Rating
    2
    undeclared is on a distinguished road

      1  

    Default

    Code:
        
    onExpand: function(rowNode, record, expandRow) {        var recordId = this.getRecordKey(record) ;
            var targetRowbody = Ext.DomQuery.selectNode('div.x-grid-rowbody', expandRow);
            if( Ext.isEmpty( this.obj_recordId_componentId[recordId] ) ) {
                var view = this.grid.getView(),
                newComponent = this.createComponent(view, record, rowNode, view.indexOf(rowNode));
    
    
                while (targetRowbody.hasChildNodes()) {
                    targetRowbody.removeChild(targetRowbody.lastChild);
                }
                newComponent.render( targetRowbody ) ;
    
    
                this.obj_recordId_componentId[recordId] = newComponent.getId() ;
            }
            else
            {
                var cmpId = this.obj_recordId_componentId[recordId] ;
    
    
                var reusedComponent = Ext.getCmp(this.obj_recordId_componentId[recordId]);
    
    
                while (targetRowbody.hasChildNodes()) {
                    targetRowbody.removeChild(targetRowbody.lastChild);
                }
    
    
                targetRowbody.appendChild( reusedComponent.getEl().dom );
                reusedComponent.doComponentLayout() ;
            }
        },
    This fixed it. Now re-uses components.

  5. #5
    Sencha User
    Join Date
    Aug 2014
    Posts
    27
    Vote Rating
    0
    Richardmansfield is on a distinguished road

      0  

    Default

    Thanks for sharing the code undeclared as i have been searching for this these days.

  6. #6
    Sencha User
    Join Date
    Feb 2012
    Posts
    14
    Vote Rating
    1
    dmfr is on a distinguished road

      0  

    Default

    Hi,
    Can you explain what you mean by "remember the component" ?

    Collapsing a row body (with a component's dom inside) just hides the node, it doesn't destroy anything.
    On re-expand, previously rendered component shows and remains fully functional.

    May be I'm missing something ? Do you have a test case ?


    Btw, i've been adding a view resize listener (to sync components layout).
    + extending Ext.grid.plugin.RowExpander instead of obsolete Ext.ux.RowExpander

    Code:
    /*
     * Inspired by : http://www.rahulsingla.com/blog/2010/04/extjs-preserving-rowexpander-markup-across-view-refreshes
     * Reappend element to DOM : http://stackoverflow.com/questions/20143082/does-extjs-automatically-garbage-collect-components
     */
    
    Ext.define('Ext.ux.ComponentRowExpander', {
        extend: 'Ext.grid.plugin.RowExpander',
    
        alias: 'plugin.cmprowexpander',
    
        rowBodyTpl : ['<div></div>'],
        
        obj_recordId_componentId: {},
         
        init: function(grid) {
            this.callParent(arguments) ;
            
            var view = grid.getView() ;
            view.on('resize', this.onResize, this) ;
            view.on('refresh', this.onRefresh, this);
            view.on('expandbody', this.onExpand, this);
            
            grid.on('destroy', this.onDestroyGrid, this) ;
            grid.headerCt.on('columnresize', this.onResize, this) ;
            
            this.obj_recordId_componentId = {} ;
        },
        
        getRecordKey: function(record) {
            return (record.internalId);
        },
        
        createComponent: function(view, record, rowNode, rowIndex) {
            return Ext.create('Ext.Component') ;
        },
        
        onExpand: function(rowNode, record, expandRow) {
            var recordId = this.getRecordKey(record) ;
            if( Ext.isEmpty( this.obj_recordId_componentId[recordId] ) ) {
                var view = this.grid.getView(),
                    newComponent = this.createComponent(view, record, rowNode, view.indexOf(rowNode)),
                    targetRowbody = Ext.DomQuery.selectNode('div.x-grid-rowbody', expandRow) ;
                
                while (targetRowbody.hasChildNodes()) {
                    targetRowbody.removeChild(targetRowbody.lastChild);
                }
                newComponent.render( targetRowbody ) ;
                
                this.obj_recordId_componentId[recordId] = newComponent.getId() ;
            }
            this.onResize() ;
        },
        
        onRefresh: function(view) {
            var reusedCmpIds = [] ;
            Ext.Array.each( view.getNodes(), function(node) {
                var record = view.getRecord(node),
                    recordId = this.getRecordKey(record) ;
                    
                if( !Ext.isEmpty(this.obj_recordId_componentId[recordId]) ) {
                    var cmpId = this.obj_recordId_componentId[recordId] ;
                    
                    reusedCmpIds.push(cmpId) ;
                    var reusedComponent = Ext.getCmp(this.obj_recordId_componentId[recordId]),
                        targetRowbody = Ext.DomQuery.selectNode('div.x-grid-rowbody', node);
                    while (targetRowbody.hasChildNodes()) {
                        targetRowbody.removeChild(targetRowbody.lastChild);
                    }
                    
                    // http://stackoverflow.com/questions/20143082/does-extjs-automatically-garbage-collect-components
                    targetRowbody.appendChild( reusedComponent.getEl().dom );
                    reusedComponent.doComponentLayout() ;
                }
            },this) ;
            
            
            // Do Garbage collection
            // Method 1 ( http://skirtlesden.com/static/ux/download/component-column/1.1/Component.js )
            var keysToDelete = [] ;
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, testCmpId ) {
                comp = Ext.getCmp(testCmpId);
                el = comp && comp.getEl();
    
                if (!el || (true && (!el.dom || Ext.getDom(Ext.id(el)) !== el.dom))) {
                    // The component is no longer in the DOM
                    if (comp && !comp.isDestroyed) {
                        comp.destroy();
                        keysToDelete.push(recordId) ;
                    }
                }
            }) ;
            
            // Method 2
            /*
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, testCmpId ) {
                if( !Ext.Array.contains( reusedCmpIds, testCmpId ) ) {
                    comp = Ext.getCmp(testCmpId);
                    comp.destroy();
                    keysToDelete.push(recordId) ;
                }
            }) ;
            */
            
            // Clean map
            Ext.Array.each( keysToDelete, function(mkey) {
                delete this.obj_recordId_componentId[mkey] ;
            },this);
        },
        
        onResize: function() {
            Ext.Object.each( this.obj_recordId_componentId, function( recordId, cmpId ) {
                Ext.getCmp(cmpId).doComponentLayout();
            }) ;
        },
         
        onDestroyGrid: function() {
            Ext.Object.each( this.obj_recordId_componentId, function(recordId, cmpId) {
                Ext.getCmp(cmpId).destroy() ;
            }) ;
         }
    });

  7. #7
    Sencha User
    Join Date
    Oct 2010
    Posts
    45
    Vote Rating
    2
    undeclared is on a distinguished road

      0  

    Default

    At least in my version (ExtJS 4.2.1) it would seemingly re-create every time, at least that's what it seems like

  8. #8
    Sencha User
    Join Date
    Oct 2010
    Posts
    45
    Vote Rating
    2
    undeclared is on a distinguished road

      1  

    Default

    Hey, I found an issue with onExpand if you use an accordion layout on a nested panel. It's looking for an active rowbody I believe, but there is none, I presume because accordion layout works differently.

    I'm going to try to fix this myself via some research, but just thought that should be here.

    I also have to give some real credit again to the maker, because I am using this on literally like 15 different GUIs, and it's been great. You can even double nest and so on with this.

    Additional edit: this works in ExtJS 5.1.0 btw

  9. #9
    Sencha User
    Join Date
    Apr 2008
    Posts
    39
    Vote Rating
    0
    fafche is on a distinguished road

      0  

    Default

    This does not work if one of the store records will change.

    The reason is that the template will get overridden with nothing...

    Any one have a workaround for this?