Use Split Layout For Tablets

In mobile applications, content is typically divided between a list view and a detail view. Although this approach is appropriate when applications are running on phones, it becomes unusable when applications are running on tablets. Since there is more space on a tablet screen, the views designed for phones do not fill the bigger tablet screens. A solution for tablet applications is to place both the list and detail views on one screen. The predefined Split layout implements this approach and uses a tablet screen space more efficiently. There are two panes in this layout. The left one is a master pane. It presents a list of items. The pane on the right side is a detail pane. It presents detail information about the item selected in the right pane. In this tutorial, you will learn how to implement an application using the Split layout.

Split Layout Sample

Watch Video

Download Code

Prepare an Application

Use an application project template for your sample application.

To use the predefined Split layout for applications, set the layoutSet option of the HtmlApplication object to DevExpress.framework.html.layoutSets['split'].

JavaScript
window.MyApp = window.MyApp || {};
$(function() {
    MyApp.app = new DevExpress.framework.html.HtmlApplication({
        namespace: MyApp,
        layoutSet: DevExpress.framework.html.layoutSets['split'],
        //...
    });
    //...
});

Include links to the layouts that are used within the Split layout.

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

<link rel="stylesheet" type="text/css" href="layouts/Navbar/NavbarLayout.css" />
<link rel="dx-template" type="text/html" href="layouts/Navbar/NavbarLayout.html"/>
<script type="text/javascript" src="layouts/Navbar/NavbarLayout.js"></script>

<link rel="stylesheet" type="text/css" href="layouts/Empty/EmptyLayout.css" />
<link rel="dx-template" type="text/html" href="layouts/Empty/EmptyLayout.html"/>
<script type="text/javascript" src="layouts/Empty/EmptyLayout.js"></script>

<link rel="stylesheet" type="text/css" href="layouts/Simple/SimpleLayout.css" />
<link rel="dx-template" type="text/html" href="layouts/Simple/SimpleLayout.html"/>
<script type="text/javascript" src="layouts/Simple/SimpleLayout.js"></script>

Remove the 'home' view that is included in the application template by default and add the following views.

Categories List View

Add the 'categories' view to the application. It will include the dxList widget that will get data from the Categories table provided by the sample data service. This service is published for instructional use only.

HTML
<div data-options="dxView : { name: 'categories', title: 'Categories' } " >
    <div class="home-view"  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { dataSource: dataSource  }">
            <div data-options="dxTemplate : { name: 'item' }" data-bind="dxAction: '#products/{CategoryID}'">
                <div data-bind="text: CategoryName"></div>
            </div>
        </div>
    </div>
</div>
JavaScript
MyApp.categories = function (params) {
    var viewModel = {
        dataSource: new DevExpress.data.DataSource({
            load: function (loadOptions) {
                return $.getJSON('http://sampleservices.devexpress.com/api/Categories');
            }
        })
    };
    return viewModel;
};

When clicking an item in the dxList widget of the 'categories' view, the 'products' view is displayed. To pass the category ID as a parameter to the 'products' view and show the products that belong to the chosen category only, modify the application's routing format.

JavaScript
MyApp.app.router.register(":view/:id", { view: 'categories', id: undefined });

Products List View

Add the 'products' view to the application. It will include the dxList widget that will display the products that are related to the category chosen in the previous 'categories' view.

HTML
<div data-options="dxView : { name: 'products', title: 'Products' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { dataSource: dataSource }">
            <div data-options="dxTemplate : { name: 'item' }"
                    data-bind="dxAction: '#product-details/{ProductID}">
                <div data-bind="text: ProductName"/>
            </div>
        </div>
    </div>
</div>
JavaScript
MyApp.products = function (params) {
    var viewModel = {
        categoryId: params.id,
        dataSource: new DevExpress.data.DataSource({
            pageSize: 10,            
            load: function(loadOptions) {
                return $.getJSON('http://sampleservices.devexpress.com/api/Products', {
                    CategoryID: viewModel.categoryId,
                    skip: loadOptions.skip,
                    take: loadOptions.take,
                    searchString: ''
                });            
            }
        })
    };
    return viewModel;
};

When clicking an item in the dxList widget of the 'products' view, the 'product-details' view is displayed. To show details for the product chosen in the 'products' view, the id of this product is passed to the 'product-details' view as a parameter.

Product Detail View

Add the 'product-details' view to the application. It will include fields in which to display the id and the name of the product chosen in the previous 'products' view.

HTML
<div data-options="dxView : { name: 'product-details', title: 'Product' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div class="dx-fieldset">
            <div class="dx-field">
                <div class="dx-field-label">Id: </div>
                <div class="dx-field-value" data-bind="text: id"></div>
            </div>
            <div class="dx-field">
                <div class="dx-field-label">Name: </div>
                <div class="dx-field-value" data-bind="text: name"></div>
            </div>
        </div>
    </div>
</div>
JavaScript
MyApp['product-details'] = function (params) {
    var viewModel = {
        id: params.id,
        name: ko.observable('')
    };
    $.getJSON('http://sampleservices.devexpress.com/api/Products/' + viewModel.id).done(function(data) {
        viewModel.name(data.ProductName);
    });
    return viewModel;
};

About View

Leave the 'about' view that is added to the application template by default as is. In a company with the 'categories' view, the 'about' view will be available from the application's global navigation.

Navigation View

As the predefined Split layout does not include a widget for global navigation, add the 'navigation' view to the application, as it is described in the Add Global Navigation Commands to a View tutorial. This view contains the dxList widget whose items will display the commands created based on the navigation option of the HtmlApplication object. Make the 'navigation' view a start view in the application.

JavaScript
window.MyApp = window.MyApp || {};
$(function () {
    MyApp.app = new DevExpress.framework.html.HtmlApplication({ 
        namespace: MyApp,
        layoutSet: DevExpress.framework.html.layoutSets['split'],
        navigation:[
            {
                title: "Categories",
                action: "#categories",
                icon: "home"
            },
            {
                title: "About",
                action: "#about",
                icon: "info"
            }
        ]
    });
    MyApp.app.router.register(":view", { view: "navigation" });
    MyApp.app.navigate();
});

Now, if you run the application on a tablet device, you will see two panes on the screen. All the views will be displayed within the 'master' pane (to the left). In the next step, you will learn how to specify a required pane for a view.

Specify a Pane for a View

Currently, all application views are displayed within the 'detail' pane.

Navigation <-> Categories <-> Products <-> Product Details

The Split layout is designed to organize the application screen so that there is no empty space on it. Typically, list/navigation information is displayed within the left pane - the 'master' pane, and detail information is displayed within the right pane - the 'detail' pane. To display the 'navigation', 'categories', 'products' and 'about' views within the 'master' pane, set the pane option of the dxView markup component representing this view to 'master' (by default, this option is set to 'detail').

HTML
<div data-options="dxView : { name: 'navigation', title: 'Menu', pane: 'master' } " >
    <div class="home-view"  data-options="dxContent : { targetPlaceholder: 'content' } " >
    </div>
</div>

<div data-options="dxView : { name: 'categories', title: 'Categories', pane: 'master' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
    </div>
</div>

<div data-options="dxView : { name: 'products', title: 'Products', pane: 'master' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
    </div>
</div>

<div data-options="dxView : { name: 'about', title: 'About', pane: 'master' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
    </div>
</div>

Now, the navigation scheme of the application looks different.

The 'master' pane:

Navigation <-> Categories <-> Products

The 'detail' pane:

Product Details

Generally, there can be views that are invoked from the 'product-details' view and displayed within the 'detail' pane. These views are added to the navigation stack that is based on the view to which the application navigated as a result of clicking an item in the list on the "master" pane.

Product Details <-> Another View <-> One more view

In order for this scheme to function, the view that is navigated from the "master" pane must be a root view. So, set the root navigation parameter to true when navigating to the 'product-details' view.

HTML
<div data-options="dxView : { name: 'products', title: 'Products', pane: 'master' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { dataSource: dataSource }">
            <div data-options="dxTemplate : { name: 'item' }"
                    data-bind="dxAction: function () {$root.navigateToDetails($data.ProductID);}">
                <div data-bind="text: ProductName"/>
            </div>
        </div>
    </div>  
</div>
JavaScript
MyApp.products = function (params) {
    var viewModel = {
        //...
        navigateToDetails: function (productId) {
            this.selectedProductID(productId);
            MyApp.app.navigate({ view: 'product-details', id: productId }, { root: true });
        }
    };
    return viewModel;
};

NOTE: If you assign a URI (whether a string or an object) to the dxAction binding directly, you cannot specify the root navigation parameter. However, the Split Layout controller catches such cases when a view configured for a "detail" pane is navigated from the "master" pane. In this instance, this view is turned into a root view automatically. Since this trick is invisible in your code, we recommend that you use the navigate method to navigate to a root view, and to have a capability to specify the root navigation parameter.

Highlight the Selected Item

Currently, when clicking a product in the product list presented in the 'master' pane, the details of the clicked product are displayed in the 'detail' pane. However, it will be better if the clicked product is highlighted in the product list, so that it is clear for which product detailed information is given in the 'detail' pane.

  • Save the currently chosen item in the dxList widget to the selectedProductID field of the 'products' ViewModel.

    JavaScript
    MyApp.products = function (params) {
        var viewModel = {
            //...
            navigateToDetails: function (productId) {
                this.selectedProductID(productId);
                MyApp.app.navigate({ view: 'product-details', id: productId }, { root: true });
            },
            selectedProductID: ko.observable()
        };
        return viewModel;
    };
  • Call the ViewModel's navigateToDetails method when the action associated with the list item is performed.

    HTML
    <div data-options="dxView : { name: 'products', title: 'Products', pane: 'master' } " >
        <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
            <div data-bind="dxList: { dataSource: dataSource }">
                <div data-options="dxTemplate : { name: 'item' }"
                        data-bind="dxAction: function () {$root.navigateToDetails($data.ProductID);}">
                    <div data-bind="text: ProductName"/>
                </div>
            </div>
        </div>  
    </div>
  • Add a CSS file for the 'products' view. Introduce the following styles in it.

    CSS
    .dx-theme-ios7.dx-device-tablet .list-item-selected {
        background: #ccf;
    }
    .dx-theme-android.dx-device-tablet .list-item-selected {
        background: rgba(44,159,201,.8);
    }
    .dx-theme-win8 .list-item-selected {
        background: #00c1ec;
    }
  • Add the introduced 'list-item-selected' style to the product in the dxList widget whose ID equals the one that is saved to the selectedProductID field. For this purpose, use css Knockout binding.

    HTML
    <div data-options="dxView : { name: 'products', title: 'Products', pane: 'master' } " >
        <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
            <div data-bind="dxList: { dataSource: dataSource }">
                <div data-options="dxTemplate : { name: 'item' }"
                        data-bind="dxAction: function () {$root.navigateToDetails($data.ProductID);},
                        css: { 'list-item-selected': ProductID === $root.selectedProductID()}">
                    <div data-bind="text: ProductName"/>
                </div>
            </div>
        </div>  
    </div>

Extend the Application for Phones

The Split layout is designed for tablet applications only. To run the application implemented based on the steps above on a phone, provide a custom layout set for the application.

JavaScript
window.MyApp = window.MyApp || {};
$(function() {
    var layoutSet = [
        { platform: "ios", tablet: true, controller: new DevExpress.framework.html.IOSSplitLayoutController() },
        { platform: "android", tablet: true, controller: new DevExpress.framework.html.AndroidSplitLayoutController() },
        { win8: false, phone: true, controller: new DevExpress.framework.html.NavBarController() },
        { win8: true, phone: true, root: true, controller: new DevExpress.framework.html.PivotLayoutController() },
        { platform: "android", phone: true, root: false, controller: new DevExpress.framework.html.SimpleLayoutController() },
        { win8: true, phone: true, root: false, controller: new DevExpress.framework.html.SimpleLayoutController() }
    ];
    MyApp.app = new DevExpress.framework.html.HtmlApplication({
        namespace: MyApp,
        layoutSet: layoutSet,
        //...
    });
    //...
});

According to the code above, the following layouts will be used for different devices.

  • iOS
    Tablet: Split layout;
    Phone: NavBar layout.
  • Android
    Tablet: Split layout;
    Phone, Root views: Navbar layout;
    Phone, Non-root views: Simple layout.
  • Windows 8
    Phone, Root views: Pivot layout;
    Phone, Non-root views: Simple layout.

As you can see, the layouts used for phone application versions include a widget that serves as a global navigation control (the dxNavbar and dxPivot widgets). So when the application starts on a phone, the "navigation" view, which you set as a start view for the application, should be replaced with the "categories" view. So modify code in the app.js file as shown below.

JavaScript
window.MyApp = window.MyApp || {};
$(function () {
    var layoutSet = [
        { platform: "ios", tablet: true, controller: new DevExpress.framework.html.IOSSplitLayoutController() },
        { platform: "android", tablet: true, controller: new DevExpress.framework.html.AndroidSplitLayoutController() },
        { win8: false, phone: true, controller: new DevExpress.framework.html.NavBarController() },
        { win8: true, phone: true, root: true, controller: new DevExpress.framework.html.PivotLayoutController() },
        { platform: "android", phone: true, root: false, controller: new DevExpress.framework.html.SimpleLayoutController() },
        { win8: true, phone: true, root: false, controller: new DevExpress.framework.html.SimpleLayoutController() }
    ];
    var device = DevExpress.devices.current(),
        startupView = "navigation";
    if (device.phone) {
        startupView = "categories";
    };
    MyApp.app = new DevExpress.framework.html.HtmlApplication({
        namespace: MyApp,
        layoutSet: layoutSet,
        navigation:[
            {
                title: "Categories",
                action: "#categories",
                icon: "home"
            },
            {
                title: "About",
                action: "#about",
                icon: "info"
            }
        ]
    });
    MyApp.app.router.register(":view/:id", { view: startupView, id: undefined });
    MyApp.app.navigate();
});

In addition, consider the scenario of navigation from a list view to a detail view. On phones, the detail view should be added after the list view in the current navigation stack. On tablets, the detail view should be shown on the "detail" pane, thus it should be navigated as a root view. To resolve this inconsistency, check whether the Split layout is used for the list view.

HTML
<div data-options="dxView : { name: 'products', title: 'Products', pane: 'master' } " >
    <div  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { dataSource: dataSource }">
            <div data-options="dxTemplate : { name: 'item' }"
                    data-bind="dxAction: function () {$root.navigateToDetails($data.ProductID);}">
                <div data-bind="text: ProductName"/>
            </div>
        </div>
    </div>  
</div>
JavaScript
MyApp.products = function (params, viewInfo) {
    var showDetailAsRoot = viewInfo.layoutController.name === 'split';
    var viewModel = {
        //...
        navigateToDetails: function (productId) {
            this.selectedProductID(productId);
            MyApp.app.navigate({ view: 'product-details', id: productId }, { root: showDetailAsRoot });
        }
    };
    return viewModel;
};