1. #1
    Sencha User
    Join Date
    Mar 2010
    Posts
    25
    Vote Rating
    1
    BigSeanDawg is on a distinguished road

      0  

    Default HTML-focused DOM Layout for auto-height web applications (Ext 4.1)

    HTML-focused DOM Layout for auto-height web applications (Ext 4.1)


    Hey Ext community,

    We are making a new Ext application, and our team has Ext expertise, but our designers are providing a rich HTML/CSS experience that doesn't always fit nicely into the Ext Component hierarchy/layout. We need the browser scrolling (auto-height), and lots of custom HTML with heavy CSS.

    To address, I created what I'm calling a DOM layout, where a Container can provide the HTML around its children components. I'd love to get your thoughts or feedback!

    Dom.js

    Code:
    /**
     * @class jx.core.layout.Dom
     * @extends Ext.layout.container.Auto
     * 
     * Custom layout that reads a "domTpl" from the Container, and renders child "items" in the
     * template for the "section" attribute of children.
     */
    Ext.define('jx.core.layout.Dom', {
        alias: ['layout.dom', 'layout.domcontainer'],
        extend: 'Ext.layout.container.Auto',
        type: 'domcontainer',
        statics: {
            sectionRegexCache: {},
            catchAllSection: 'catch-all'
        },
        
        sectionTemplateDataPrefix: 'SECTION',
        
        renderTpl: [
            '{%this.renderBody(out,values)%}'
        ],
        
        /**
         * @property domTpl
         * Layout property that will render "items" into locations specified by the {@link #section}
         * attribute of child items.  This property can just be specified on the component instead
         * of in a "layoutConfig".
         */
        domTpl: null,
        
        /**
         * @property section
         * Read from each "item" to determine where it belongs in the {@link #domTpl}.
         */
        section: null,
    
        /**
         * Returns the section element for this layout.  If the sectionName is null or not found
         * the "catchAll" section is returned.
         */
        getSection: function(sectionName) {
            if (!sectionName || !this.sections[sectionName]) {
                sectionName = this.self.catchAllSection;
            }
            var section = this.sections[sectionName];
            if (section === this.self.catchAllSection) {
                section = this.owner.el;
            }
            else if (Ext.isString(section)) {
                //if we find a string, it is a section id, so we set it up to return the element next time
                var elId = this.owner.id + '-section-' + sectionName;
                this.sections[sectionName] = section = Ext.get(elId);
            }
            return section;
        },    
        
        /**
         * @Override
         * Check if the parent of the item has the correct "data-section" attribute
         */
        isValidParent : function(item, target, position) {
            var itemEl = item.el ? item.el.dom : Ext.getDom(item);
            var sectionName = this.getSectionName(item);
            
            var inSection = false;
            if (sectionName === this.self.catchAllSection) {
                inSection = this.owner.el.dom == itemEl.parentNode;
            }
            else {
                inSection =  (itemEl && Ext.fly(itemEl.parentNode).getAttribute('data-section') === sectionName);
            }
            return inSection;
        },
        
        /**
         * @Override
         * This is called when you add() an item to the layout.  We only need
         * to find the appropriate section node and render as a child.
         */
        renderItem: function(item) {
            var section = this.getSection(item.section);
            return this.callParent([item, section]);
        },
    
        /**
         * @Override
         * Here we override {@link Ext.layout.container.Container.doRenderItems} so that we can pass
         * in the "out" to {@link #getRenderTree}, since we are using that function to render all 
         * children in this one pass.
         */    
        doRenderItems: function (out, renderData) {
            renderData.$layout.getRenderTree(out);
        },
    
        /**
         * @Override
         * Overridden to take in the "out" variable, and then use the "renderCfgs" property to
         * render the HTML into the correct location in the {@link #domTpl}
         */
        getRenderTree: function(out) {
            // -- BEGIN SUPERCLASS IMPLEMENTATION --- //
            var result,
                items = this.owner.items,
                itemsGen,
                renderCfgs = {};
            
            do {
                itemsGen = items.generation;
                result = this.getItemsRenderTree(this.getLayoutItems(), renderCfgs);
            } while (items.generation !== itemsGen);
            //return result;
            // -- END SUPERCLASS IMPLEMENTATION --- //
            
            
            // -- BEGIN CUSTOM CODE --- //
            try {
                var cmp = this.owner,
                    domTplData = cmp.initRenderData(),
                    domTpl = cmp.domTpl || '';
                
                domTpl = this.parseDomTemplateSections(domTpl);
                this.setDomTemplateData(domTplData, renderCfgs);
                new Ext.XTemplate(domTpl).applyOut(domTplData, out);
            }
            catch (e) {
                this.logWarning(e);
            }
            // -- END CUSTOM CODE --- //
        },
        
        /**
         * @private
         * Reads the given template, looking for "data-section" attributes to set up as sections.
         * The new template is returned with the needed details to make adding/removing items
         * later possible.
         */
        parseDomTemplateSections: function(domTpl) {
            this.sections = {};
            var re = /(<(\S+)[^>]*\bdata-section=('|")(.*?)\3[^>]*>)(.*?)(<\/\2>)/g;
            var match;
            var newDomTpl = [];
            var lastStart = 0;
            
            /*
             * Here we setup the "catchAll" section, which is where we will always be able to render elements
             * who either don't have a section, or who point to a non-existant section.
             */
            domTpl += '{' + this.sectionTemplateDataPrefix + this.self.catchAllSection + '}';
            this.sections[this.self.catchAllSection] = this.owner.id;
            
            while (match = re.exec(domTpl)) {
                var fullMatch = match[0],
                    openTag = match[1],
                    tagName = match[2],
                    quoteUsed = match[3],
                    section = match[4],
                    tagContents = match[5],
                    closeTag = match[6];
                
                //throw an error if there are any ids declares on the tag, since dom layout needs to control the id
                if (/\bid=/i.exec(openTag)) {
                    throw "DOM Layout requires that no id be specified on section elements... " +
                        "Class: " + this.owner.self.getName() + ", Section: " + section + ", Tag: " + openTag;
                }
                
                var sectionId = "{id}-section-" + section;
                this.sections[section] = sectionId;
                var dataIndex = openTag.indexOf('data-section=');
                
                //pushing onto an array is much faster than replacing strings with "+="
                newDomTpl.push(domTpl.slice(lastStart, match.index));
                newDomTpl.push(openTag.slice(0, dataIndex))
                newDomTpl.push('id="' + sectionId);
                newDomTpl.push('" ');
                newDomTpl.push(openTag.slice(dataIndex));
                newDomTpl.push(tagContents);
                newDomTpl.push('{');
                newDomTpl.push(this.sectionTemplateDataPrefix);
                newDomTpl.push(section);
                newDomTpl.push('}');
                newDomTpl.push(closeTag);
                
                lastStart = match.index + fullMatch.length;
            }
            newDomTpl.push(domTpl.slice(lastStart));
            
            return newDomTpl.join('');
        },
        
        /**
         * @private
         * Sets up the domTplData so that child items get rendered into the proper sections
         */
        setDomTemplateData: function(domTplData, renderCfgs) {
            var layoutItems = this.getLayoutItems();
            for (var i = 0; i < layoutItems.length; i++) {
                var item = layoutItems[i];
                
                /*
                 * If the item has no section, we'll still render it in the "catchAll" section so that
                 * we don't get other errors about it not being rendered.
                 */
                var section = this.getSectionName(item, true);
                
                /*
                 * Insert an attribute in the template string: {SECTIONname}
                 */
                var tplAttr = this.sectionTemplateDataPrefix + section;
                domTplData[tplAttr] = (domTplData[tplAttr] || '') + Ext.DomHelper.markup(renderCfgs[item.id]);
            }
        },
        
        /**
         * @private
         */
        getSectionName: function(item, logWarning) {
            var sectionName = item.section;
            if (!sectionName) {
                sectionName = this.self.catchAllSection;
            }
            if (!this.sections[sectionName]) {
                if (logWarning) {
                    this.logWarning('No "' + sectionName + '" section found for class: ' + item.self.getName() + ' (id: ' + item.getId() + ')... The item will be rendered to the catchAll section!');
                }
                sectionName = this.self.catchAllSection;
            }
            return sectionName;
        },
        
        /**
         * @private
         */
        logWarning: function(err) {
            console && Ext.isFunction(console.warn) && console.warn(err);
        }
    
    });
    Here's the important parts:
    • Give your Ext.Container a domTpl attribute, which should be an XTemplate string
    • The template can have html elements with data-section attributes
    • Children of the container can have a section configuration attribute to say what section from the template they should be rendered into
    • If no section is provided (or the section doesn't exist) the component will be rendered at the bottom of the HTML segment generated from the component
    Example usage:

    Code:
    /**
     * Base class for all "App" pages, which sets up needed or common functionality.
     */
    Ext.define('jx.app.App', {
        extend: 'Ext.Container',
        requires: [
            'jx.core.layout.Dom',
            'jx.app.Header',
            'jx.app.Footer',
            'jx.artifact.ArtifactPage'
        ],
        
        initComponent: function() {
            Ext.apply(this, {
                layout: 'dom',
                domTpl: 
                    '<div data-section="header"></div>' +
                    
                    '<div class="container app-content">' +
                        '<div class="row">' +
                            '<div data-section="page" class="span12 page-content"></div>' +
                        '</div>' +
                        
                        '<div class="row">' +
                            '<div data-section="footer" class="span12"></div>' +
                        '</div>' +
                    '</div>',
                items: [
                    {
                        xtype: 'jx.app.Header',
                        section: 'header'
                    }, {
                        xtype: 'jx.app.Footer',
                        section: 'footer'
                    }, {
                        xtype: 'jx.artifact.ArtifactPage',
                        section: 'page'
                    }
                ]
            })
            this.callParent(arguments);
        }
        
    });
    In this example, I'm actually using the Bootstrap classes that produce really nice responsive layouts with minimal HTML. This markup would normally require 6 additional components.

    Does anyone think this is useful? Does Ext already support something similar? Any feedback, suggestions, or comments are welcome. Thanks!

  2. #2
    Sencha User
    Join Date
    Apr 2010
    Posts
    100
    Vote Rating
    0
    Dipish is an unknown quantity at this point

      0  

    Default


    It's difficult to get Ext's layout mechanism completely out of the way but I found it helpful to use Ext's auto layout in similar cases, possibly overriding some layout-related methods on components.

  3. #3
    Sencha User
    Join Date
    Mar 2010
    Posts
    25
    Vote Rating
    1
    BigSeanDawg is on a distinguished road

      0  

    Default


    Yeah, I hear you. Notice my layout class does actually extend Ext.layout.container.Auto so I am actually using "auto" layout. I just created this code to make it easy to have arbitrary children placed around a configurable dom tree, something our designers needed. The children can be removed and added at runtime, and the domTpl can change without messing with the order of the children.

  4. #4
    Sencha User
    Join Date
    Apr 2010
    Posts
    100
    Vote Rating
    0
    Dipish is an unknown quantity at this point

      0  

    Default


    Notice my layout class does actually extend Ext.layout.container.Auto so I am actually using "auto" layout
    Sorry, I actually overlooked this The issue is still relevant and it seems like Ext is making pure CSS layout more and more difficult to achieve with every new version.

    I opened a very similar issue today: Containers with CSS-driven layouts: how to make Ext get out of the way?
    Would love to see your input if you have any thoughts!

  5. #5
    Sencha User
    Join Date
    Dec 2012
    Posts
    4
    Vote Rating
    1
    faustofonseca is on a distinguished road

      0  

    Default Perfect

    Perfect


    My feedback about this is: you are a God! And all my children will be named after you (even if they are girls).

    This is really working and it is exactly what I needed. This should be integrated into ExtJs!

    Thank you very much!

  6. #6
    Sencha User
    Join Date
    Jun 2013
    Posts
    1
    Vote Rating
    0
    tyhwa is on a distinguished road

      0  

    Default


    It's realy cool! could you upload the whole examples?