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.

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 in DevExtreme applications internally. 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 some 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 with 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>
</div>

In the following image, you can see that a layout may include static content. This content is not changed from view to view. However, the content in placeholders is changing. When an application navigates to a view, the view's content is merged with the content of the layout content placeholders.

Content Placeholders

Since there can be several placeholders in a layout, their content will be shown sooner or later depending on the difficulty of the inner elements. So, to show the entire changing content at once, wrap all the content placeholders by a div element and apply the data-options attribute set to dxTransition.

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>

Note that a transition element unites a changing content. The markup outside this element is static.

Transition Element

Use the dxTransition component's type configuration option to set the type of the transition animation - 'slide', 'fade', 'overflow' or 'none'. The specified transition effect will appear when the content that is rendered to the placeholders is changed.

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 an HTML markup, a layout is accompanied by CSS style sheets and is managed by a JavaScript controller. Generally, there is a base DefaultLayoutController that manages the process of showing views, navigation and transitions between views. To bind this controller with your layout markup, create an instance of this controller passing an object with the specified layoutName field as a constructor parameter. Then, add this controller instance to the DevExpress.framework.html.layoutControllers list. Moreover, you can define an environment for your layout markup. Here is an example.

JavaScript
DevExpress.framework.html.layoutControllers.push(
    {
        navigationType: "navbar",
        platform: "android",
        phone: false,
        root: true,
        controller: new DX.framework.html.DefaultLayoutController({ layoutTemplateName: "myLayout" })
    }
)

You can inherit from the DefaultLayoutController to override the basic functionality, if required. In this instance, you can bind a layout template within your controller. To take into account the custom controller, register its instance within the layoutControllers list.

JavaScript
myController = DX.framework.html.DefaultLayoutController.inherit({
    //your implementation here
});
DevExpress.framework.html.layoutControllers.push(
    {
        navigationType: "navbar",
        platform: "android",
        phone: false,
        root: true,
        controller: new DX.framework.html.myController()
    }
)

As a rule, you will not have to define layouts for your views. You can just specify a navigation type for your application. A navigation type determines which predefined layouts must be applied to views at different circumstances, that is the strategy developed to be close to native mobile applications. To learn more about navigation types, refer to the Navigation Types article.

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

Insert View into Layout

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' }">
    <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>

NOTE: If you wrap a view's content by a dxContent element, only the markup inside of the dxContent element(s) will be rendered to the resulting view. Read the View Life Cycle topic below for details.

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.

Device 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 device object as markup options of the dxView/dxLayout components.

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

<div data-options="dxView: { name: 'home', platform: 'ios', tablet: true }">
    This is a view for any tablet.
</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 device from the browser. Thus, the application will display the views and layouts that are most appropriate for the device, and will then apply the style sheets that correspond to this device.

NOTE: Be sure to provide templates that are unique to a specific context. For instance, a template for the iOS platform and a template for iPhones will both be appropriate when an application runs on an iPhone device. When several templates are appropriate for a view, an exception will be raised. To avoid such scenarios, define a single common template, or several templates, that are as specific as possible.

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, use a Knockout binding syntax. Declare the command by using the data-bind attribute and pass the required markup options. These options include: the command identifier, the action to be performed when executing the command, as well as a title, an icon, and enabled and visible states.

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

A command's markup options can be bound to a ViewModel's fields. Here is an example.

HTML
<div data-options="dxView: { name: 'home', title: 'Home' }">
    <div data-bind="dxCommand: { id: 'myCommand', action: add, 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 called when you are executing the Add command or a string representing the URL to navigate to. To learn more about Actions, refer to the Actions section.

To display commands, the layout in which a view is displayed must include command containers. These are the elements in the layout markup that are marked by the data-options attribute set to dxCommandContainer. The following code demonstrates a command container that displays commands by toolbar items.

HTML
<div class="layout-header">
    <div data-bind="dxToolbar: { items: [ { text: title, align: 'center' } ] }" 
        data-options="dxCommandContainer : { id: 'my-container' }">
    </div>
</div>

As there can be several command containers in a layout, and the layout can have several versions - each for a certain platform/type of a device, you should declare that a particular command must be displayed in a particular command container. For this purpose, use the application's command mapping.

JavaScript
new DevExpress.framework.HtmlApplication({
    commandMapping: {
        'my-container': {
            defaults: {
                'showIcon':false, 
                'location':'left'
            },
            commands: [
                {
                    id: 'myCommand',
                    location: 'right' // container defaults can be overridden
                }
            ]
        }
    }
});

When using predefined layouts for views, put your commands to the command containers that are available in these layouts. To learn which command containers are available in these layouts, refer to the Predefined Layouts topic. An application loads default mapping of the "create", "edit", "save", "delete", "cancel" and "back" commands to the command containers of the predefined layouts, and then extends it by your custom command mapping declared within the application's configuration object. If you use the specified identifiers for your commands, these commands are automatically mapped to the command containers of the predefined layouts. It is only required to define a mapping for your custom commands. To learn what commands are mapped to the built-in layouts by default, refer to the Default Command Mapping topic.

View Life Cycle

The replacement of a view with another view is initiated either by invoking the navigate method of the HtmlApplication object or by using navigation actions, which also results in a call of the navigate method. When the application navigates to a view, the previous view is hidden or disposed. When being hidden, this view can be restored from the cache quickly to be displayed again. When being disposed, a new life cycle is initiated when the application navigates to this view repeatedly. All these steps of a view life cycle are detailed below.

1 - Get View Info

Step 1

At the beginning of a view display process, everything that is known about the view 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. However, 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 or the cache is disabled. In this instances, the information on the view is gathered from scratch and added to the cache so that the next time everything that is needed to display this view is contained in the cache.

viewInfo is an object that collects information on a view gathered during the whole view display process. At this step, the fields of this object provide the following information on the view.

  • viewName
    A string specifying the name of the displayed view.

  • routeData
    An object representing route parameters for the displayed view.

  • uri
    The URI to which the application is currently navigating.

  • viewTemplateInfo
    An object that provides the specified values of the dxView component options.

  • layoutController
    The controller that will be used to display the view within a layout markup. This controller, as all the controllers that are registered in the application, is initialized beforehand. This means that a markup for a blank view is already prepared and an empty layout template is ready to take view content inside.

    Blank View Withdrawing

    Before the blank view is shown it is added to the layout transition element in an inactive state.

    Layout with Inactive Blank View

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 search 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 fields of the object passed as a parameter.

  • resolveLayoutController
    This event is fired before an appropriate layout controller is found based on the specified navigation strategy and the current navigation context. Handle this event to set a custom layout controller for the displayed view. This layout controller will use the required layout template for the view.

2 - Show a Blank View

Step 2

While performing all the preparations required to display the view, a quick response to an end-user is required. So, a blank view that was prepared at the layout controller's initialization is shown first. Often, this blank view contains a loading indicator provided by the layout content. In addition, the view's title is shown by the layout content elements that are bound to the title variable.

If this is the first view that is shown by the current layout controller, the layout template with the blank view is added to the view port element of the application's page. The blank view becomes active and is shown with the specified transition.

Layout with Inactive Blank View

3 - Create a View Model

Step 3

To get the View Model 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.

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

  • afterViewSetup
    This event is fired after the ViewModel object is created for the view. At this time, you can modify the created ViewModel object. It is available for you as the model field of the viewInfo object exposed by the parameter object.

4 - Render the View

Step 4

When showing a view for the first time or when information on it has been removed from the cache, the viewInfo object does not contain the renderResult field. At this step, the view is rendered and the result of rendering is assigned to this field.

To be shown within the layout, the content of the view's dxContent elements is merged with the corresponding dxContentPlaceholder elements of the layout. The result of merging is added to the corresponding dxTransition element of the layout as an additional view markup in an inactive state.

View Rendering

NOTE: Only the markup that is added to the dxContent elements will be rendered to the resulting view.

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

  • viewShowing
    This event is raised before rendering (or showing, if the render result is available).

  • viewRendered
    This event is fired after a markup was rendered for the view. This markup can be accessed using the markup field of the renderResult object that is exposed by the viewInfo object passed as the event handler's parameter.

5 - Show the View

Step 5

To show the view, the inactive markup corresponding to this view is made active while the other markup elements, which correspond to the previously shown view, are made inactive.

Show View

NOTE: You can access the active view using the $('.dx-active-view .my-selector') selector.

Handle the following event at this step.

  • 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.

6- Hide the View

If the navigation to another view implies conserving the current view in the navigation history, the view markup becomes inactive. Information on the view is stored in the cache.

Handle the following event at this step.

  • viewHidden
    This event is fired each time after a view is hidden. Access the viewInfo object using the viewInfo field of the object passed as the parameter.

7 - Display View Repeatedly

The next time the application navigates to the view, everything is ready to show the view if information on this view is stored in the cache. If the previous view was displayed by the same layout controller, there is an "inactive" markup of the current view in the dxTransition element of the layout. So, the view is made active and the previous active content is made inactive.

Show View

If the layout controller of the previous view is not the controller of the displayed view, the previous controller is deactivated first. This means that the layout markup provided by this controller is removed from the view port element of the application page. The controller of the view that must be displayed is activated then. Thus, the layout markup provided by this controller is inserted to the view port element of the application page.

View-related events are raised in the following order.

  • navigating
  • viewShowing
  • viewShown

8 - Dispose the View

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

  • viewDisposing
    This event is raised before you dispose of the view.

  • viewDisposed
    This event is raised after you dispose 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 exposes the events that are raised for each view displayed in the application. You can handle these events to perform certain actions for all the views in the application. At the same time, you may need to handle a particular event for a certain view only. In such a case, add a field with the event's name to the view's ViewModel and assign the required function to it. The following is the list of the events that can be handled in this manner.

  • viewShowing
  • 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,
        //...
    };
};