Layout - Enyo: Up and Running (2015)

Enyo: Up and Running (2015)

Chapter 4. Layout

In this chapter we’ll explore how to enhance the appearance of Enyo apps by using various layout strategies to place controls where we want them. By combining the knowledge gained in the previous chapters with the layout tools in this chapter, you’ll have most of the knowledge you need to create compelling apps using Enyo. We’ll explore each of the layout tools using examples you can run in your browser.

As with visual controls, Enyo provides both core layout strategies and an optional library called Layout. The core strategies provide the “simpler” approach to layout while the Layout library provides some more advanced features. The Onyx library also provides a layout tool in the form of the Drawer component and the Moonstone library has a number of enhanced layout controls.

Responsive Design

Before we begin talking about layout strategies we should discuss responsive design. Responsive design means that an app or web page changes its appearance (or functionality) depending upon the device or display size it is used on. It’s important to consider how your app will look on different displays when designing a cross-platform app. Responsive web design is a topic that probably deserves a book of its own. You are encouraged to research the associated tools and techniques on the Web. Many of those same tools are used both within Enyo and by Enyo app developers. In particular, CSS media queries are often used in Enyo apps. We’ll discuss the tools that Enyo makes available for designing responsive apps, but you may need to supplement these tools in certain circumstances.

Core Layout Features

Enyo provides two useful mechanisms for layout in the core: scrollers and repeaters. The Scroller kind implements a section of the display that is scrollable by the user while the Repeater kind is useful for making repeating rows of items. There are also data-aware controls for list and grid layout that we will cover in Chapter 5.

Scrollers

One of the bigger challenges in a mobile app is presenting a scrolling area of information that would otherwise be too big to fit. While many solutions exist, their cross-platform performance varies greatly. The Enyo team has spent a considerable amount of time analyzing performance issues and bugs across various browsers to produce the Scroller component.

Scrollers require very little configuration but do have some settings you can control. The vertical and horizontal properties default to automatically allow scrolling if the content of the scroller exceeds its size. Setting either to 'hidden' disables scrolling in that direction while setting either to 'scroll' causes scroll thumbs to appear (if enabled) even if content otherwise fits. The touch property controls whether desktop browsers will also use a touch-based scrolling strategy (instead of thumb scrollers).

For more information on scrollers, visit the scroller documentation page.

Repeaters

Another challenge is to display a list of repeating rows of information. The Repeater component is designed to allow for the easy creation of small lists (up to 100 or so items) of consistently formatted data. A repeater works by sending an event each time it needs data for a row. The method that subscribes to this event fills in the data required by that row as it is rendered. The following sample shows a repeater that lists the numbers 0 through 99:

enyo.kind({

name: 'RepeaterSample',

kind: 'Scroller',

components: [{

kind: 'Repeater',

count: 100,

components: [{ name: 'text' }],

onSetupItem: 'setupItem',

ontap: 'tapped'

}],

setupItem: function(sender, event) {

var item = event.item;

item.set('$.text.content', 'This is row ' + event.index);

return(true);

},

tapped: function(sender, event) {

enyo.log(event.index);

}

});

A Repeater control

TIP

You’ll notice that we placed the Repeater into a Scroller. As the contents would (likely) be too large to fit onto your screen, we needed the scroller to allow all the content to be viewable. The components block of the Repeater is the template for each row and can hold practically any component, though it is important to note that fittables (see Fittable) cannot be used inside a repeater.

Also of note is the fact that each time we respond to the onSetupItem event, we reference the component(s) in the components block directly off the item passed in through the event. The repeater takes care of instantiating new versions of the components for each row. If you need to update a specific row in a repeater, you should call the renderRow() method and pass in the index of that row.

TIP

To redraw the whole repeater, such as when the underlying data has changed, set a new value for the count property. It is a good idea to pass a truthy value for the third parameter to set() in the case where only the data but not the number of records has changed (e.g., this.set('$.repeater.count', 100, true);). Alternately, you can call the build() method to redraw the list.

Layout Library Features

The modular Layout library includes several kinds for arranging controls. Three of the kinds we’ll discuss are Fittable, List, and Panels. Visit the Enyo docs website to find out more information on the Layout library and the kinds not covered here.

Fittable

One aspect of layout that Enyo makes easier is designing elements that fill the size of a given space. Enyo provides two layout kinds, FittableColumnsLayout and FittableRowsLayout, to accomplish this. Fittable layouts allow for a set of components to be arranged such that one (and only one) component expands to fill the space available while the others retain their fixed size. FittableColumnsLayout arranges components horizontally while FittableRowsLayout arranges them vertically. To specify the child component that will expand to fit the space available, set the fit property totrue.

To apply the fittable style to controls, set the layoutKind property. To make it easier to use, the Layout library includes two controls with the layout already applied: FittableColumns and FittableRows. Fittables can be arranged within each other, as the following code sample shows:

enyo.kind({

name: 'Columns',

kind: 'FittableColumns',

components: [

{ content: 'Fixed width', classes: 'dont' },

{ content: 'This expands', fit: true, classes: 'do' },

{ content: 'Another fixed width', classes: 'dont' }

]

});

enyo.kind({

name: 'FittableSample',

layoutKind: 'FittableRowsLayout',

components: [

{ content: 'Fixed height', classes: 'dont' },

{ kind: 'Columns', fit: true, classes: 'do' },

{ content: 'Another fixed height', classes: 'dont' }

]

});

TIP

In the previous sample, we used both styles of applying a fittable layout, using a layoutKind for the row layout and using the FittableColumns for the column layout. We applied a simple CSS style that added colored borders to the expanding regions. If you resize the browser window, you’ll see that the area in the middle will expand while the areas above and to the sides have fixed heights and widths, respectively.

An example of fittable layout

TIP

Fittables only relayout their child controls in response to a resize event. If you need to relayout the controls because of changes in the sizes of components, call the resize() method on the fittable component.

While fittables provide an easy way to create specific layouts, they should not be overused. Reflows are performed in JavaScript and too many nested fittables can affect app performance.

Lists

Earlier we covered repeaters, which display a small number of repeating items. The List component serves a similar purpose but allows for a practically unlimited number of items. Lists include a built-in scroller and support the concept of selected items (including multiple selected items). Lists use a flyweight pattern to reduce the number of DOM elements that get created and, therefore, speed up performance on mobile browsers.

All this performance doesn’t come without downsides, though. Because list items are rendered on the fly it is difficult to have interactive components within them. It is recommended that only simple controls and images be used within lists:

enyo.kind({

name: 'ListSample',

kind: 'List',

count: 10000,

handlers: {

onSetupItem: 'setupItem',

ontap: 'tapped'

},

components: [{ name: 'text' }],

setupItem: function(sender, event) {

this.set('$.text.content', 'This is row ' + event.index);

return(true);

},

tapped: function(sender, event) {

enyo.log(event.index);

}

});

TIP

In both this example and the Repeater example, we knew the number of items to display and set the count property when creating them. Often, you won’t know how many items to display while writing your app. In that case, leave the count property undefined and set() it once you have received the data. Once set, the List will render itself. If the underlying data changes, call refresh() to redraw at the current position or reset() to redraw from the start.

In order to make a List row interactive, you must first use the prepareRow() method. Then, a call to performOnRow() can be used to act on the row. Finally, lockRow() should be called to return the row to its non-interactive state. Let’s modify the tap handler from the last example to show how to add an interactive element to a row:

enyo.kind({

name: 'ListSample',

kind: 'List',

count: 1000,

items: [],

handlers: {

onSetupItem: 'setupItem'

},

components: [

{ name: 'text', kind: 'Input', ontap: 'tapped',

onchange: 'changed', onblur: 'blur' }

],

create: function() {

this.inherited(arguments);

for(var i = 0; i < this.count; i++) {

this.items[i] = 'This is row ' + i;

}

},

setupItem: function(sender, event) {

this.$.text.setValue(this.items[event.index]);

return(true);

},

tapped: function(sender, event) {

this.prepareRow(event.index);

this.set('$.text.value', this.items[event.index]);

this.$.text.focus();

return(true);

},

changed: function(sender, event) {

this.items[event.index] = sender.getValue();

},

blur: function(sender, event) {

this.lockRow();

}

});

TIP

Making a +List+ row interactive

In this version, we detect a user tapping into a row and then lock that row so that we can make the Input editable. If we did not prepare the row, then the input control would not be properly associated with the row being edited and our changes would not be preserved. We look for the onblurevent so we can call lockRow() to put the list back into non-interactive mode.

TIP

This sample isn’t complete, as there are ways to move out of fields without triggering the blur event correctly. A better way to handle this kind of situation would be to use Popup to open a dialog on top of the list in response to a click on the row.

List and Repeater have data-aware versions that are easier to work with. We’ll cover data-aware components in Chapter 5.

Panels

Panels are one the most flexible layout tools Enyo has to offer. Panels give you the ability to have multiple sections of content that can appear or disappear as needed. You can even control how the panels arrange themselves on the screen by using the arrangerKind property. The various arrangers allow for panels that collapse or fade as moved, or that are arranged into a carousel or even a grid.

Panels have an index property that indicates the active panel. Although the various arrangers can present more than one panel on the screen at a time and all such visible panels can be interactive, the active panel is important. You can easily transition the active panel by using the previous()and next() methods, or detect when a user has moved to a new panel (e.g., by swiping) by listening for the onTransitionFinish event.

A quick example of how to use Panels will help explain. In this example, we’ll set up a layout that can have up to three panels, depending on the available width. As the available width shrinks, the number of panels visible will also shrink, until only one remains:

enyo.kind({

name: 'PanelsSample',

kind: 'Panels',

arrangerKind: 'CollapsingArranger',

classes: 'panels-sample',

narrowFit: false,

handlers: {

onTransitionFinish: 'transitioned'

},

components: [

{ name: 'panel1', style: 'background-color: blue' },

{ name: 'panel2', style: 'background-color: grey' },

{ name: 'panel3', style: 'background-color: green' }

],

transitioned: function() {

this.log(this.index);

}

});

In order to achieve the sizing, we’ll use a little CSS and some media queries to size the panels appropriately:

.panels-sample > * {

width: 200px;

}

@media all and (max-width: 500px) {

.panels-sample > * {

min-width: 200px;

max-width: 100%;

width: 50%;

}

}

@media all and (max-width: 300px) {

.panels-sample > * {

min-width: 100%;

max-width: 100%;

}

}

TIP

An example of the Panels component

For this sample, we set the narrowFit property to false. By default, the individual panels in a CollapsingArranger panel will fill the available space when the screen size is below 800px. We overrode the default in order to use 200px as the minimum width of a panel. The CSS we used detects when the screen gets below 500px and we limit each panel to half the space. Then, when the screen gets below 300px, we cause the panels to take up all the space. The user can still swipe left and right to reveal panels that aren’t currently visible.

We have only touched on the power of the Panels component. You should check out the Panels documentation for more ideas on how to use them.

Summary

You are now well on your way to producing beautiful apps that perform well on mobile and desktop platforms. We looked at some techniques for designing responsive apps that make the best use of a user’s display size. In the next chapter we’ll learn how to create data-driven applications using Enyo.