Sencha Inc. | HTML5 Apps

Blog

Creating Custom Layouts in Ext JS and Sencha Touch

July 15, 2014 | Arthur Kay

custom layouts in Ext JS and TouchThe Layout system is one of the most powerful and unique parts of the Sencha frameworks. Layouts handle the sizing and positioning of every Component in your application, so you don't need to manage those pieces manually. Ext JS and Sencha Touch have many similarities between their Layout classes, and our own Ivan Jouikov recently analyzed them in detail in this Sencha blog post.

Having said that, most Ext JS and Sencha Touch developers have probably never looked under the hood of the Layout system. Sencha frameworks deliver the most common application layouts out of the box, so unless your application required more exotic functionality, it is unlikely you have investigated the inner workings of the Layout system.

Imagine for a moment that your company needed to display UI elements for your application using a “3D Carousel”. None of the standard Sencha layouts offer this sort of ability — so how might you approach this problem?

Choosing a Base Class

When developing any custom component in Ext JS and Sencha Touch, the first step should always be to consider which base class is the best choice to extend. In this situation, we need a layout that can house items in 3D space. Therefore, we can start pretty low on the Layout inheritance chain because we don’t require any specific functionality beyond managing items. In this case, Ext.layout.container.Container (Ext JS) and Ext.layout.Default (Sencha Touch) make the most sense.

In Ext JS, the Layout system is responsible for manually managing many of the size and positioning calculations, primarily because Ext JS supports legacy browsers that don’t support some modern CSS features like Flexbox. As a result, Ext.layout.container.Container includes a number of methods like calculate(), getContainerSize() and getPositionOffset() which our 3D carousel layout will still need to override to lay out its children.

It is also important to note that the Ext JS layouts perform “runs”, and each run may manage multiple “cycles”. For example, Box layouts configured with stretchmax would require at least two cycles, in which the layout would first determine the maximum size of each child component, and then stretch all children of the layout to the same size via a second cycle. Operations causing large numbers of layout runs and/or cycles to occur (e.g. adding or removing many items) may want to preemptively suspend layouts to improve performance, resuming layouts only after the operations are complete.

By contrast, Ext.layout.Default in Sencha Touch allows the browser to handle the positioning and sizing of most items in the layout via CSS (because Sencha Touch only supports modern browsers, all of which implement CSS Flexbox). Therefore, Ext.layout.Default contains mostly methods relating to the addition, removal and repositioning of child items.

Now that we understand which classes we should extend for our new “3D Carousel” layout, let’s explore the necessary steps to physically build it.

CSS3 Transforms and Other Magic

In order to build a “3D Carousel”, we will need to use some advanced CSS 3D transforms such as transform, transition, rotateX/rotateY and translateZ. CSS 3D transforms can be very complicated, but in a nutshell we need to do the following things for our new Sencha layout:

  • Apply a perspective and transform to the parent container (making it appear 3D)
  • Apply a transform to the child components in the layout (rotating them around the 3D shape to form its sides)
  • Apply a transform to an inner DOM element of the parent container (to physically rotate the 3D shape as the user interacts with it)

As you might expect, the actual DOM generated by Ext JS and Sencha Touch is a bit different, so while the approach taken in the exercise is the same in both frameworks, the resulting CSS is slightly different. The additional CSS we need to include with our new “3D Carousel” layout looks like this (for Sencha Touch):

 
.x-layout-carousel {
  -webkit-perspective : 1500px;
  -moz-perspective    : 1500px;
  -o-perspective      : 1500px;
  perspective         : 1500px;
  position            : relative !important;
}
 
.x-layout-carousel .x-inner {
  -webkit-transform-style : preserve-3d;
  -moz-transform-style    : preserve-3d;
  -o-transform-style      : preserve-3d;
  transform-style         : preserve-3d;
}
 
.x-layout-carousel.panels-backface-invisible .x-layout-carousel-item {
  -webkit-backface-visibility : hidden;
  -moz-backface-visibility    : hidden;
  -o-backface-visibility      : hidden;
  backface-visibility         : hidden;
}
 
.x-layout-carousel-item {
  display  : inline-block;
  position : absolute !important;
}
 
.x-layout-carousel-ready .x-layout-carousel-item {
  -webkit-transition : opacity 1s, -webkit-transform 1s;
  -moz-transition    : opacity 1s, -moz-transform 1s;
  -o-transition      : opacity 1s, -o-transform 1s;
  transition         : opacity 1s, transform 1s;
}
 
.x-layout-carousel-ready .x-inner {
  -webkit-transition : -webkit-transform 1s;
  -moz-transition    : -moz-transform 1s;
  -o-transition      : -o-transform 1s;
  transition         : transform 1s;
}
 

The CSS for our Ext JS layout looks nearly identical, with only minute changes to the CSS selectors.

In both Sencha Touch and Ext JS, we will have to modify some additional CSS during runtime to respond to the user’s interaction with our “3D Carousel”. Let’s first extend our base layout classes, and then explore how to add the interactive functionality.

Extending the Base Layout Classes

When we first extend Ext.layout.Default in Sencha Touch, we primarily want to add some configuration options for the look and feel of our new “3D Carousel”, as well as some utilities for correctly positioning the child items within this layout.

Our extension initially looks like this:

 
Ext.define('Ext.ux.layout.Carousel', {
    extend : 'Ext.layout.Default',
    alias  : 'layout.carousel',
 
    config : {
        /**
         * @cfg {number} portalHeight
         * Height of the carousel, in pixels
         */
        portalHeight : 0,
 
        /**
         * @cfg {number} portalWidth
         * Width of the carousel, in pixels
         */
        portalWidth  : 0,
 
        /**
         * @cfg {string} direction
         * 'horizontal' or 'vertical'
         */
        direction    : 'horizontal' //or 'vertical'
    },
 
    onItemAdd : function () {
        this.callParent(arguments);
        this.modifyItems();
    },
 
    onItemRemove : function () {
        this.callParent(arguments);
        this.modifyItems();
    },
 
    modifyItems : function () {
        //calculate child positions, etc
    }
});
 

Aside from the config object, we see three methods: onItemAdd(), onItemRemove(), and modifyItems(). The first two methods are simple overrides of Ext.layout.Default which allow us to modify the positioning of child items upon addition/removal, and modifyItems() is a new method for calculating the fancy CSS 3D transforms.

The action inside the Layout system ultimately comes alive when the Layout classes assign their container (from Ext.layout.Default):

 
setContainer: function(container) {
    var options = {
        delegate: '> component'
    };
 
    this.dockedItems = [];
 
    this.callSuper(arguments);
 
    container.on('centeredchange', 'onItemCenteredChange', this, options, 'before')
        .on('floatingchange', 'onItemFloatingChange', this, options, 'before')
        .on('dockedchange', 'onBeforeItemDockedChange', this, options, 'before')
        .on('afterdockedchange', 'onAfterItemDockedChange', this, options);
},
 

For our layout extension, we need to piggyback on this method in order to do some further initialization:

 
Ext.define('Ext.ux.layout.Carousel', {
 
    //...
 
    setContainer : function (container) {
        var me = this;
 
        me.callParent(arguments);
 
        me.rotation = 0;
        me.theta = 0;
 
        switch (Ext.browser.name) {
            case 'IE':
                me.transformProp = 'msTransform';
                break;
 
            case 'Firefox':
                me.transformProp = 'MozTransform';
                break;
 
            case 'Safari':
            case 'Chrome':
                me.transformProp = 'WebkitTransform';
                break;
 
            case 'Opera':
                me.transformProp = 'OTransform';
                break;
 
            default:
                me.transformProp = 'WebkitTransform';
                break;
 
        }
 
        me.container.addCls('x-layout-carousel');
        me.container.on('painted', me.onPaintHandler, me, { single : true });
    },
 
    onPaintHandler : function () {
        var me = this;
 
        //add the "ready" class to set the CSS transition state
        me.container.addCls('x-layout-carousel-ready');
 
        //set the drag handler on the underlying DOM
        me.container.element.on({
            drag      : 'onDrag',
            dragstart : 'onDragStart',
            dragend   : 'onDragEnd',
            scope     : me
        });
 
        me.modifyItems();
    }
 
});
 

After we assign the layout’s container internally, we must wait until the container physically renders in order to assign event handlers to its underlying DOM. Next, we need to fill in the functional gaps for transforming the child items managed by this layout:

 
Ext.define('Ext.ux.layout.Carousel', {
 
    //...
 
    modifyItems : function () {
        var me = this,
            isHorizontal = (me.getDirection().toLowerCase() === 'horizontal'),
            ct = me.container,
            panelCount = ct.items.getCount(),
            panelSize = ct.element.dom[ isHorizontal ? 'offsetWidth' : 'offsetHeight' ],
            i = 0,
            panel, angle;
 
        me.theta = 360 / panelCount;
        me.rotateFn = isHorizontal ? 'rotateY' : 'rotateX';
        me.radius = Math.round(( panelSize / 2) / Math.tan(Math.PI / panelCount));
 
        //for each child item in the layout...
        for (i; i < panelCount; i++) {
            panel = ct.items.getAt(i);
            angle = me.theta * i;
 
            panel.addCls('x-layout-carousel-item');
 
            // rotate panel, then push it out in 3D space
            panel.element.dom.style[ me.transformProp ] = me.rotateFn + '(' + angle + 'deg) translateZ(' + me.radius + 'px)';
        }
 
        // adjust rotation so panels are always flat
        me.rotation = Math.round(me.rotation / me.theta) * me.theta;
 
        me.transform();
    },
 
    transform : function () {
        var me = this,
            el = me.container.element,
            h = el.dom.offsetHeight,
            style= el.dom.style;
 
        // push the carousel back in 3D space, and rotate it
        el.down('.x-inner').dom.style[ me.transformProp ] = 'translateZ(-' + me.radius + 'px) ' + me.rotateFn + '(' + me.rotation + 'deg)';
 
        style.margin = parseInt(h / 2, 10) + 'px auto';
        style.height = me.getPortalHeight() + 'px';
        style.width = me.getPortalWidth() + 'px';
    },
 
    rotate : function (increment) {
        var me = this;
 
        me.rotation += me.theta * increment * -1;
        me.transform();
    }
});
 

In our case, there’s a fair amount of complex math to determine the proper placement of each item, as well as the need to manually update the CSS transform values on the container itself.

Finally, we need to add our event handlers to capture when the user interacts with our “3D Carousel”:

 
Ext.define('Ext.ux.layout.Carousel', {
    //...
 
    onDragStart : function () {
       this.container.element.dom.style.webkitTransitionDuration = "0s";
    },
 
    onDrag : function (e) {
        var me = this,
            isHorizontal = (me.getDirection().toLowerCase() === 'horizontal'),
            delta;
 
        if (isHorizontal) {
            delta = -(e.deltaX - e.previousDeltaX) / me.getPortalWidth();
        }
        else {
            delta = (e.deltaY - e.previousDeltaY) / me.getPortalHeight();
        }
 
        me.rotate((delta * 10).toFixed());
    },
 
    onDragEnd : function () {
       this.container.element.dom.style.webkitTransitionDuration = "0.4s";
    }
 
});
 

These event handlers simply evaluate how far the user has dragged their mouse across the carousel, and then update the CSS transition.

The full Sencha Touch code for this class can be found here. The code for Ext JS, which extends Ext.layout.container.Container, is again very similar but does have minute API changes due to the differences between frameworks. The Ext JS code for this example can be found here.

Reviewing Ext.ux.layout.Carousel

Let’s take a moment to step back and review what just happened.

We chose to extend the Sencha Touch class Ext.layout.Default because our new “3D Carousel” layout only needed to inherit the baseline functionality of the layout system. Next, we added overrides for onItemAdd(), onItemRemove(), and setContainer() to append some runtime configuration of our layout. Finally, we implemented a few utility methods and event handlers to physically manage the positioning of the child items in our layout.

Although the idea of a “3D Carousel” is a whimsical example of what can be built using Sencha Touch or Ext JS, the point here is that building creative new layouts in Sencha applications is actually really simple. The key is understanding how layouts are initialized and what happens during runtime — and the underlying framework code is far less complicated than you might think. And while the Layout system is a bit different under the hood between Sencha Touch and Ext JS, the general approach is exactly the same.

Please note: this is actually just a tech demo, and I can’t guarantee my code works on every browser. The fact that I’m using CSS3 transforms already means I’ve excluded older versions of several browsers, so please don’t try to use this in production.

Other interesting examples of customized layouts include this Sencha Fiddle by Sencha Support engineer Seth Lemmons involving a circle menu, and this video of a custom navigation component by Andrea Cammarata, Sencha Professional Services engineer.

There are 4 responses. Add yours.

Blake

4 months ago

Awesome blog post!

Shreeraj Pillai

4 months ago

Nice one….

Tom Coulton

4 months ago

Thanks for the great article. We translated it into Japanese here: http://www.xenophy.com/sencha-blog/11355

Tom Coulton

4 months ago

...and the Japanese version of the video is here: http://vimeo.com/101592704

Comments are Gravatar enabled. Your email address will not be shown.

Commenting is not available in this channel entry.