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!