Views and Layouts

As discussed in the Application Overview article, an application built with the PhoneJS framework is a single-page application. While such an application has only one web page, it can comprise several application screens defined as named views. A view is defined by a piece of HTML markup that forms the view template. This view template can optionally have JavaScript code and associated style sheets used to customize the look and feel.

Following the MVVM pattern, the view's markup template and style sheets serve as a View. The JavaScript function that is associated with the view prepares the ViewModel and performs the additional actions necessary to set up the view. These actions include interaction with the Model (a JavaScript object providing data, e.g. from a web server) and post-processing of the rendered view. The following diagram demonstrates this.

Application Structure

As you can see, a view's markup template is combined with other HTML elements defined within a layout's markup, which results in the rendering of the final view. You will learn how to define views and how to use custom or predefined layouts below.

Define a View

A view has a unique name that serves as an identifier. The view name is encoded into the fragment identifier of the application URL. The framework uses this name to find the view's HTML markup and the JavaScript function that returns the view's ViewModel.

To implement a view's HTML markup, add a div element and include the required markup in it. Set the div element's data-options attribute to dxView, and specify the required view markup options.

HTML
<div data-options="dxView: { name: 'home', title: 'Home' }">
    <!-- View markup goes here -->
    <h1>'Welcome!'</h1>
</div>

The view's markup may have bindings to the fields of the view's ViewModel. Implement the ViewModel as an object returned by a JavaScript function. This function must have the view's name and must be declared within the application's namespace.

View and Layout Merging

The ViewModel may get the required data from the view's Model - a JavaScript object in a general case. However, there may be scenarios when the ViewModel prepares data by itself.

A view is defined by a piece of HTML markup that forms the view template. This view can optionally have JavaScript code and an associated style sheet used to customize the look and feel. Following the MVVM pattern, the view's markup template and style sheets serve as a View. The JavaScript function that is associated with the view prepares the ViewModel and performs the additional actions necessary to set up the view. These actions include interaction with a web server and post-processing of the rendered View.

A view has a unique name that serves as an identifier. The framework uses this name to find the view's HTML markup and the JavaScript function that returns the view's ViewModel.

Note: As you can see, binding to the ViewModel is organized using the Knockout framework. Generally, the ko.applyBindings() method should be called to activate the Knockout. However, this method is called internally in PhoneJS applications. So you don't have to call this method in your code. At the same time, don't forget about this method because when you insert a markup with the data-bind syntax dynamically, you will have to call the ko.applyBindings() method manually to initialize bindings.

Add a Partial View

You can display a view inside another view to reuse a markup (similar to partial rendering in ASP.NET MVC or ASP.NET user controls). To do so, create an empty div element inside a view's markup. To declare this element as the place where a specified view will be rendered, set the element's data-options attribute to dxViewPlaceholder and pass the name of the view to be rendered.

HTML
<div data-options="dxView: {name: 'header'}>
    <h1 data-bind="text: message"></h1>
</div>
<div data-options="dxView: {name: 'home'}>
    <div data-options="dxViewPlaceholder: { viewName: 'header' }"></div>
    <div>
        <!-- View contents -->
    </div>
</div>

The partial view can be bound to its own ViewModel by using the "with" binding. This ViewModel can be the object assigned to the field of the parent view's ViewModel. Here is an example:

HTML
<div data-options="dxView: {name: 'creadentials'}>
    <p data-bind="with: person">
        Name: <span data-bind="text: name"> </span>,
        Surname: <span data-bind="text: surname"> </span>
    </p>
</div>
<div data-options="dxView: {name: 'home'}>
    <div data-options="dxViewPlaceholder: { viewName: 'credentials' }"></div>
    <div>
        <!-- View contents -->
    </div>
</div>

JavaScript
MyApp.home = function (params) {
    var viewModel = {
        title: ko.observable('Home'),
        person: {
            name: params.name,
            surname: params.surname
        }
    };
    return viewModel;
};

Define Layouts

Normally, there is a commonality between application screens. In the following image, a toolbar and navigation bar are located on each screen.

View Merged into Layout

PhoneJS allows you to organize a structure for each screen. This structure is called layout. It is defined once - by a markup declared as a dxLayout markup component. This markup includes so-called placeholders for the varying content. In the image above, the list on the first screen is changed to another list on the second screen, and to a set of fields on the third screen. In addition, the title and a set of buttons on the toolbar are changed from screen to screen. The changing content is defined within the dxView markup component. When navigating to a view, the markup of the corresponding dxView template is merged to the markup of the required dxLayout component. The resulting markup is then rendered as a page.

View Merged into Layout

To define a layout, add a div element and include the required markup in it. Set the div element's data-options attribute to dxLayout, and specify the required layout markup options.

HTML
<div data-options="dxLayout: { name: 'myLayout'}">
    <!-- Layout markup goes here -->
</div>

To add a placeholder to a layout, add a div element, set the data-options attribute to dxContentPlaceholder and specify the required placeholder markup options.

HTML
<div data-options="dxLayout: { name: 'myLayout'}">
    <!-- Layout markup goes here -->
    <div data-options="dxContentPlaceholder: {name: 'content'}"
</div>

You can associate a visual transition effect with the layout's placeholders. This effect will appear when the content that is rendered to the placeholders is changed. To specify a transition effect, wrap the required placeholders with a div element with the data-options attribute set to dxTransition. Use the dxTransition component's configuration options to set the type of the transition animation - 'slide', 'fade', 'overflow' or 'none'.

HTML
<div data-options="dxLayout: { name: 'myLayout'}>
    <div data-options="dxTransition: { name: 'main', type: 'slide' }">
        <div data-options="dxContentPlaceholder : { name: 'header' } " ></div>
        <div data-options="dxContentPlaceholder: {name: 'content' }"></div>
        <div data-options="dxContentPlaceholder : { name: 'footer' } " ></div>
    </div>
</div>

The dxTransition component can be used not only for placeholders, but for any elements in a markup.

To set a specific transition effect for a specific placeholder, set the transition option of the placeholder's markup to 'slide', 'fade', 'overflow' or 'none'.

HTML
<div data-options="dxLayout: { name: 'myLayout'}>
    <div data-options="dxContentPlaceholder: {name: 'content', transition:'slide'}"></div>
</div>

In addition to HTML markup, a layout is accompanied by CSS style sheets and a JavaScript controller. Generally, there is a base DefaultLayoutController that manages the process of showing views, navigation and transitions between views. However, you can inherit from it to override the basic functionality, if required. To take into account the custom controller, register its instance within the layoutControllers list.

JavaScript
myController = DX.framework.html.DefaultLayoutController.inherit({
    //your implementation here
});
DX.framework.html.layoutControllers.myLayoutController = new myController();

To bind the custom controller with the layout's markup, specify the markup's controller markup option.

HTML
<div data-options="dxLayout: { name: 'myLayout', controller: 'myLayoutController'}">
    <!-- Layout markup goes here -->
</div>

As a rule, an application includes a single layout and all views are shown in it. This layout must be specified as the application's default layout via the application's defaultLayout configuration option.

JavaScript
$(function() {
    app = new DevExpress.framework.html.HtmlApplication({
        defaultLayout: 'default'
    });
});

However, there can be views that are displayed using a specific layout (e.g., a splash screen). In such a case, the view that is shown in that screen must indicate the specific layout to be used (read below).

Insert View into Layout

A view is displayed in a particular layout. Since most views in an application are shown in a single layout, this layout is declared as the default layout in the application's configuration object (see above). In this case, you do not need to specify a layout for the view. However, if a specific view must be shown in a particular layout, specify this layout for the view.

HTML
<div data-options="dxView: { name: 'splash', layout: 'splashLayout' }">
    <!-- View markup goes here -->
</div>

When defining a view, specify the layout placeholder to which the view's markup will be rendered. To do this, wrap the required markup of the view by a div element, denote this element as the dxContent component and pass the name of the required placeholder as the targetPlaceholder option of this component.

HTML
<div data-options="dxLayout: { name: 'myLayout'}">
    <!-- Layout markup goes here -->
    <div data-options="dxContentPlaceholder: {name: 'content'}"></div>
</div>

<div data-options="dxView: { name: 'home', title: 'Home' }">
    <!-- View markup goes here -->
    <div data-options="dxContent: {  targetPlaceholder: 'content')">
        <h1 data-bind="text: message"></h1>
    </div>
</div>

When you require different parts of a view to be displayed in different placeholders, wrap each part by the dxContent div and specify the required placeholder for it.

HTML
<div data-options="dxLayout: { name: 'myLayout'}">
    <div data-options="dxContentPlaceholder: {name: 'content1'}"></div>
    <!-- Layout markup goes here -->
    <div data-options="dxContentPlaceholder: {name: 'content2'}"></div>
</div>

<div data-options="dxView: { name: 'home', title: 'Home' }">
    <div data-options="dxContent: {  targetPlaceholder: 'content1')">
        <!-- View markup goes here -->
    </div>
    <div data-options="dxContent: {  targetPlaceholder: 'content2')">
        <!-- View markup goes here -->
    </div>
</div>

The following image illustrates how the content of a dxView markup component is merged to a placeholder of the dxLayout markup component to prepare a final markup for the view.

View and Layout Merging

In some cases, you may be required to add a placeholder to a dxView component and define the content for this placeholder within a dxLayout component. The resulting view markup will be generated in the same manner - by merging the dxView and dxLayout markups.

View and Layout Merging

For instance, the "Navbar" and "Slideout" predefined layouts include the "dxContent: { targetPlaceholder: 'view-footer')" component in the "iOS" version. To render the markup of this component, the view that uses one of these layouts must include the "dxContentPlaceholder: { name: 'view-footer')" component in its markup.

Note: An application can display views without a layout. In this instance, check to see that the default layout is not set for the application's defaultLayout option.

Platform Specific Markup

You can define special views and layouts for specific devices. In addition, you can define multiple views/layouts with the same name, which are targeted for different devices. To set a target device for a view/layout, use the fields of the devices object as markup options of the dxView/dxLayout components.

HTML
<div data-options="dxView: { name: 'home', platform: 'ios' }">
    This is a view for the iOS platform.
</div>

<div data-options="dxView: { name: 'home', phone: true }">
    This is a view for any phone.
</div>

<div data-options="dxView: { name: 'home', tablet: true }">
    This is a view for any tablet.
</div>

<div data-options="dxView: { name: 'home'}">
    This is a view for any device.
</div>

As you can see, you can specify the target platform as well as the device type.

A PhoneJS application, when running, retrieves information about the platform from the browser. Thus, the application will display the views and layouts that are most appropriate for the platform used, and will then apply the style sheets that correspond to this platform.

Add Commands to Views

A view may include a functionality for performing operations. Suppose you want a view to contain "Back" and "Add" buttons. In an application designed for the iPhone, the "Back" button must be located on the left side of the title bar and the "Add" button must be located on the right side of the title bar. If you want to make an application for an Android phone, you will have to display the "Add" button on the bottom navbar, and not display the "Back" button at all, since the phone has a "Back" button built into the hardware. When implementing an application that will be run on both the iPhone and an Android phone, you will not only have to define two different layouts, but also create and manage different widgets for these platforms. To make things simpler, we offer you the ability to define a single view with two commands, so that you don't have to manually manage different widgets. We provide buttons created for commands that will work properly on both iPhone and Android devices; these buttons will also deliver a native user experience.

A command is an abstract action associated with a view. Commands help you produce truly cross-platform applications with a native look and feel.

Commands are declared in a view markup within a root element. To add a command, add a div element with the data-options attribute set to dxCommand, and pass the required markup options. These options include: the action to be performed when executing the command, an abstract location for the command on the screen, as well as a title, an icon, and enabled and visible states.

HTML
<div data-options="dxView: { name: 'home', title: 'Home' }">
    <div data-options="dxCommand: { action: '#product-details', location: 'create', title: 'Add' } "></div>
</div>

You may need to change a command's markup option dynamically. In this instance, use a Knockout binding syntax. Declare the command by using the data-bind attribute in the same way you used the data-options attribute.

HTML
<div data-options="dxView: { name: 'home', title: 'Home' }">
    <div data-bind="dxCommand: { action: add, location: 'create', title: 'Add' } "></div>
</div>

To function properly, the code above must be accompanied by the ViewModel object that exposes the "add" field. This field can be a function that will be called when executing the Add command or a string representing the URL to navigate to. To learn more about Actions, refer to the Actions section.

Returning to the sample above, the "home" view must be displayed within the layout that displays commands from the "create" location. For this purpose, there should be a dxCommandContainer markup component within the layout. This component defines from which locations it can display commands, how to display them and where.

HTML
<div data-options="dxLayout: { name: 'myLayout', platform: 'ios', controller: 'myLayoutController'}">
    <div data-options="dxContentPlaceholder : { name: 'header', transition: 'slide' } " >
        <div data-options="dxCommandContainer : { locations: [
                {'name':'back','showIcon':false,'align':'left'},
                {'name':'cancel','showIcon':false,'align':'left'},
                {'name':'create','showText':false,'align':'right'},
                {'name':'edit','showIcon':false,'align':'right'},
                {'name':'save','showIcon':false,'align':'right'} ] } "
            data-bind="dxToolbar: { }" >
        </div>
    </div>
    <!-- ... -->
</div>

In the example above, the layout declares a toolbar with several command locations. Commands assigned to the "back" and "cancel" locations are displayed as toolbar items on the left side of the toolbar. Commands assigned to the "create", "edit" and "save" location are displayed as toolbar items on the right side of the toolbar, which is displayed at the top of the page - within the "header" placeholder.

Commands

While a command's place is defined once - by the location option - the command will actually be displayed in different places on different devices. This is because a layout can have several versions - each for a certain platform/type of a device, and there are different places for command locations in different layout versions. These places can be determined by the device platform's (or type's) guidelines, as well as by the choice of the layout's developer.

Built-in Layouts

In most cases, you do not have to define layouts yourself. The framework comes with a set of predefined layouts that are located in the lib | layouts folder of your PhoneJS zip archive. In addition, you can find the layouts folder in the project template that comes with PhoneJS - this will assist you as you begin building an application. The following is a list of built-in layouts along with available placeholders and command locations.

  • Navbar
    The following table defines what is available in this layout for devices that run on iOS.
    dxContentPlaceholder dxComandContainer dxContent
    'header' Locations: 'back', 'cancel', 'create', 'edit', 'save'
    'content'
    'footer' Locations: 'navigation'
    Locations: 'delete' Target placeholder: 'view-footer'
    To place a command in the 'delete' location, add the 'view-footer' dxContentPlaceholder component to your view. In this instance, the dxContent component with the command container containing the 'delete' location (which is defined in the layout) will be displayed.
     
    The following table defines what is available in this layout for devices that run on Android.
    dxContentPlaceholder dxComandContainer
    'header' Locations: 'navigation'
    'content'
    'footer' Locations: 'cancel', 'create', 'edit', 'menu', 'delete', 'save'
     
    The following table defines what is available in this layout for devices that run on a Win8 tablet.
    dxContentPlaceholder dxComandContainer
    'header' Locations: 'navigation'
    'content'
     
    The following table defines what is available in this layout for the devices that run on a Win8 phone.
    dxContentPlaceholder dxComandContainer
    'header' Locations: 'navigation'
    'content'
    'footer' Locations: 'cancel', 'create', 'edit', 'delete', 'save'
     
    The following table defines what is available in this layout for devices that run on Tizen.
    dxContentPlaceholder dxComandContainer dxContent
    'header' Locations: 'navigation'
    'content'
    'footer' Locations: 'cancel', 'create', 'edit', 'delete', 'save', 'menu'
  • SlideOut
    The following table defines what is available in this layout for devices that run on iOS.

    dxContentPlaceholder dxComandContainer dxContent
    'header' Locations: 'menu', 'back', 'cancel', 'create', 'edit', 'save'
    'content'
    Locations: 'delete' Target placeholder: 'view-footer'
    To place a command in the 'delete' location, add the 'view-footer' dxContentPlaceholder component to your view. In this instance, the dxContent component with the command container containing the 'delete' location (which is defined in the layout) will be displayed.

     
    The following table defines what is available in this layout for devices that run on Android.

    dxContentPlaceholder dxComandContainer
    'header' Locations: 'create', 'edit', 'menu', 'delete', 'save'
    'content'

     
    The following table defines what is available in this layout for devices that run on a Win8 phone.

    dxContentPlaceholder dxComandContainer
    'header'
    'content'
    Locations: 'cancel', 'create', 'edit', 'delete', 'save'

     
    The following table defines what is available in this layout for devices that run on Tizen.

    dxContentPlaceholder dxComandContainer dxContent
    'header' Locations: 'menu','create', 'edit', 'save','delete'
    'content'
  • Split
    The following table defines what is available in this layout.

    dxContentPlaceholder dxComandContainer
    'header' Locations: 'previousPage'
    'content'
    'footer' Locations: 'cancel', 'create', 'edit', 'delete', 'save'
  • Simple
    The following table defines what is available in this layout for devices that run on a Win8 phone.

    dxContentPlaceholder dxComandContainer
    'content'
    Locations: 'cancel', 'create', 'edit', 'delete', 'save'
  • Desktop
    The following table defines what is available in this layout for devices that run on a desktop.

    dxContentPlaceholder dxComandContainer
    Locations: 'navigation'
    Locations: 'cancel', 'create', 'edit', 'delete', 'save'
    'content'
  • Empty
    The following table defines what is available in this layout.

    dxContentPlaceholder dxComandContainer
    'content'

You can remove unnecessary layouts from your application project and leave only those that are used in your application. Link the layouts that you use in your application in the index.html file.

HTML
<!-- Layouts -->
<script type="text/javascript" src="layouts/Split/SplitLayout.js"></script>
<link rel="stylesheet" type="text/css" href="layouts/Split/SplitLayout.css" />
<link rel="dx-template" type="text/html" href="layouts/Split/SplitLayout.html"/>

In addition, set a default layout for your application using the name that is specified as the name option of the layout markup.

JavaScript
$(function() {
    app = new DevExpress.framework.html.HtmlApplication({
        defaultLayout: 'navbar'
    });
});

View Life Cycle

A view change is initiated either by invoking the navigate method of the HtmlApplication object or by using navigation actions, which also results to the call of the navigate method. To learn more about these techniques, refer to the Navigate to a View topic. The navigate method displays the required view. The process of a view display can be divided into the following steps.

1 - Get View Info

Step 1

At the beginning, everything that is known about the view to be displayed is its name. It is the view name that is specified in the URI passed as the navigate function's parameter or the default view name specified in the routing rule. To get more information about the view, the application's cache is used. Information on all the views that are contained in the current navigation history is stored within the cache. There can be no information on a view in the cache, because the view was removed during the application flow or it has not been displayed before. In this instance, the information on the view is gathered and added to the cache so that the next time everything needed to display this view is contained in the cache.

At this point, the maximum information that can be obtained about a view is its ViewModel object. To get this object, a function with the same name as the view is searched in the application's namespace, and, if found, the function is called. The object returned by this function is the view's ViewModel.

The following events of the HTMLApplication object can be handled to change the flow of the view display at this step.

  • navigating
    This event is fired at the very beginning - before you seach the view in the cache. Handle this event to cancel the display of the view, or to redirect to another view. For this purpose, use the cancel and uri parameters.

  • beforeViewSetup
    This event is fired before creating the ViewModel object for the view. You can set a custom ViewModel object to be used for the view. To do this, add the model field to the viewInfo object passed as a parameter.

  • afterViewSetup
    This event is fired after the ViewModel object is created for the view, but before it is placed in the cache. At this time, you can modify the created ViewModel object. It is available for you as the model field of the viewInfo parameter.

  • viewShowing
    This event is raised after obtaining the viewInfo object that represents information of the view each time before it is displayed.

2 - Show an Blank View

Step 1

If the viewInfo object does not have the renderResult field specified after the previous step, an initial ("blank") markup for the view is prepared to be rendered. The view's HTML template is searched as well as the HTML template for the layout that must be used for this view. As a result, the renderResult file is added to the viewInfo object. This field represents an object that exposes the $markup and layoutControllerName fields. Note that the $markup field is set to the layout's HTML template.

Then, the layout controller with the name that is set to the layoutControllerName field of the viewInfo object is searched and activated. This controller displays the markup provided by the viewInfo object.

NOTE: A view is not displayed entirely at once. А blank view is rendered first to provide a fast response to an end-user's action. While preparing the final markup of the view (see the step 3 below), a load indicator is shown.

Handle the following event at this step.

  • blankViewRendered
    This event is raised after the markup is ready to be rendered by the layout controller. This markup represents the layout's HTML template only. You can access this markup using the $markup field of the renderResult object exposed by the event handler's viewInfo parameter.

3 - Complete a View Display

Step 1

If the renderResult object does not have the rendered field specified, or this field is set to false, the resulting markup for the view is prepared to be rendered. The view's markup and the markup of the layout are merged, partial views are inserted and bindings to the ViewModel are applied. Finally, commands are created within the command containers. At this moment, the viewInfo object has the renderResult.$markup field set the final markup and the rendered field set to true. Once the resulting markup is ready, the layout controller displays the view.

Handle the following events at this step.

  • viewRendered
    This event is fired after a markup was ready for the view and the layout controller displayed the view.

  • viewShown
    This event is fired each time after a view is displayed. Handle this event to refresh data in the view each time the view is shown. Access the view's ViewModel using the model field of the object passed as the viewInfo parameter.

4 - Display View Repeatedly

The next time the application navigates to the view on which there is all the required information in the cache, events are raised in the following order:

  • navigating;
  • viewShowing;
  • viewShown.

5 - Dispose the View

The following events are raised when the current view is going to be removed, and information on it has already been removed from the cache.

  • viewDisposing
    This event is raised before disposing of the view.

  • viewDisposed
    This event is raised after disposing of the view.

To learn when a view is removed from a cache, refer to the Navigation and Routing topic.

Handle View Events

The HtmlApplication object's events that are detailed in the topic above are raised for each view shown in the application. At the same time, you may need to handle a particular event for a certain view only. In this instance, add a field with the event's name to the view's ViewModel and assign the required function to it. The following is a list of the events that can be handled in this manner.

  • viewShowing
  • blankViewRendered
  • viewRendered
  • viewShown
  • viewHidden
  • viewDisposing
  • viewDisposed

For instance, you can handle the viewShowing event to get the required data for the view. In the following example, the viewShown event is handled to set a focus to a particular numberbox on the view.

HTML
<div data-options="dxView : { name: 'home' }">            
    <!-- ... -->
    <div class="dx-fieldset">            
        <div class="dx-field">
            <div class="dx-field-label">Bill Total:</div>                
            <div id="billTotalInput" class="dx-field-value" data-bind="dxNumberBox: { value: billTotal, placeholder: 'Type here...', valueUpdateEvent: 'keyup', min: 0 }"></div>
        </div>
    </div>
</div>
JavaScript
MyApp.home = function(params) {
    var billTotal = ko.observable(),
    //...
    function viewShown() {
        $("#billTotalInput").dxNumberBox("instance").focus();
    }
    return {
        billTotal: billTotal,
        viewShown: viewShown,
        //...
    };
};