DevExtreme React - Views and Layouts
As discussed in the Application Project article, an application built with the DevExtreme SPA 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.
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.
<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.
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.
Within the function that returns a ViewModel, you can use the information that is passed when navigating to the view. This information is accessible using the following parameters of the function.
Navigation parameters
When navigating to a view, extra navigation parameters can be specified in addition to the name of the target view. These parameters conform to the application's routing format. You can access these navigation parameters as the fields of the object passed as the first parameter of the function that returns the ViewModel of the target view.View info
The second parameter passed as the function's parameter represents an object that specifies the information that is gathered on the view by the time the function is called. The following fields of this object are accessible: key, viewName, uri, routeData, canBack and previousViewInfo.
Define Layouts
Normally, there is a commonality between application screens. In the following image, a toolbar and navigation bar are located on each screen.
The framework 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 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.
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.
<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.
<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.
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.
<div data-options="dxLayout: { name: 'myLayout'}"> <div data-options="dxTransition: { name: 'main', animation: '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 changing content. The markup outside this element is static.
Use the dxTransition component's animation configuration option to set the animation that will be used when the content that is rendered to the placeholders included to the dxTransition element is changed.
To set a specific transition effect for a specific content placeholder, exclude this placeholder from the dxTransition element and specify the animation option.
<div data-options="dxLayout: { name: 'myLayout'}> <div data-options="dxContentPlaceholder: {name: 'content', animation:'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 name field as a constructor parameter. Then, add this controller to the application's layout set defining a navigation context for this controller. Here is an example.
window.AppNamespace = {}; $(function () { AppNamespace.myController = new DevExpress.framework.html.DefaultLayoutController({ name: "myLayout" }) AppNamespace.app = new DevExpress.framework.html.HtmlApplication({ namespace: AppNamespace, layoutSet: [ { platform: 'ios', phone: true, root: false, controller: AppNamespace.myController } ], //... }); AppNamespace.app.router.register(":view/:name", { view: "home", name: '' }); AppNamespace.app.navigate(); });
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 layoutSet list.
myController = DevExpress.framework.html.DefaultLayoutController.inherit({ //your implementation here }); AppNamespace.app = new DevExpress.framework.html.HtmlApplication({ namespace: AppNamespace, layoutSet: [ { platform: 'ios', phone: true, root: false, controller: new myController() } ], //... }); AppNamespace.app.router.register(":view/:name", { view: "home", name: '' }); AppNamespace.app.navigate();
As a rule, you will not have to define layouts for your views. The framework comes with predefined layout sets. When a view is displayed, an appropriate layout controller from the application's layout set is used to render a layout markup for this view. To learn more about predefined layout sets, refer to the Built-in Layouts article.
$(function() { app = new DevExpress.framework.html.HtmlApplication({ layoutSet: DevExpress.framework.html.layoutSets['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.
<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.
<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.
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.
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.
Defer View Content Rendering
View rendering may take significant time. For instance, there may be a delay while waiting for data from a server or the view content may be so "heavy" that its rendering takes a lot of time. In this instance, an end-user should be notified that view content is being loaded so that the view does not look as broken. To indicate a loading state of a view, use the DeferRendering widget. Wrap the view content that will be shown with a delay by the widget's element.
<div data-options="dxView : { name: 'Products', title: 'Products' } " > <!--...--> <div data-options="dxContent : { targetPlaceholder: 'content' } " data-bind="dxDeferRendering: { showLoadIndicator: true, renderWhen: isReady }" > <!--...--> </div> </div>
MyApp.Products = function(params, viewInfo) { var isReady = $.Deferred(); return { isReady: isReady.promise(), viewShown: function() { db.Products.load().done(function() { isReady.resolve(); }); } }; };
The content enclosed into the DeferRendering widget is shown when the Promise object assigned to the widget's renderWhen option is resolved. While waiting for the moment when the deferred content is allowed to be rendered, you can show a loading indicator. For this purpose, set the widget's showLoadIndicator option to true.
To specify the animation to be used when showing the deferred content enclosed to the DeferRendering widget, use the widget's animation option.
If your view is "heavy" enough, it may take a lot of time to display the whole view at once. So, you can divide the view into several blocks each enclosed into a separate DeferRendering widget. If you do not specify the renderWhen option of these widgets, the content of each DeferRendering widget will be rendered one after another from top to bottom. This will provide a quicker response to a user.
See Also
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.
<div data-options="dxView: { name: 'header' }"> <h1 data-bind="text: message"></h1> </div> <div data-options="dxView: { name: 'home' }"> <div data-options="dxContent: { targetPlaceholder: 'content' }"> <!-- View contents --> <div data-options="dxViewPlaceholder: { viewName: 'header' }"></div> </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:
<div data-options="dxView: { name: 'credentials' }"> <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="dxContent: { targetPlaceholder: 'content' }"> <!-- View contents --> <div data-options="dxViewPlaceholder: { viewName: 'credentials' }"></div> </div> </div>
MyApp.home = function (params) { var viewModel = { title: ko.observable('Home'), person: { name: params.name, surname: params.surname } }; return viewModel; };
Context Specific Markup
You can define view and layout HTML templates for specific contexts. When running an application, the HTML template that is most appropriate for the current context will be used to display a view. Below, you will learn how to define templates specific for different contexts.
Device Specific Markup
You can define multiple views/layouts with the same name that 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.
<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 an iPad. </div>
As you can see, you can specify the target platform as well as the device type.
A DevExtreme 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 used device, and will then apply the style sheets that correspond to this device.
Orientation Specific Markup
You can define view templates that are specific to the 'portrait' and 'landscape' device orientations. To set a target device orientation for a view, use the orientation configuration option of the corresponding dxView markup component. In the following example, the 'home' view will have data displayed using the List widget when the device on which the application is currently displayed has a 'portrait' orientation. When changing the device orientation to 'landscape', the 'home' view will be rerendered. In the newly applied HTML template, data will be displayed using the TileView widget. When rotating the device back to the 'portrait' orientation, the view will be rerendered and will display data using the List widget again.
<div data-options="dxView : { name: 'home', title: 'Home', orientation: 'portrait' } " > <div class="home-view" data-options="dxContent : { targetPlaceholder: 'content' } " > <div data-bind="dxList: { dataSource: dataSource }"> <div data-options="dxTemplate : { name: 'item' } " > <div class="product-image-box"> <img data-bind="attr: { src: Image, alt: Name }" /> </div> <div> <div data-bind="text: Name"></div> <div><strong data-bind="text: Globalize.formatCurrency(Price, 'USD')"></strong></div> </div> </div> </div> </div> </div> <div data-options="dxView : { name: 'home', title: 'Home', orientation: 'landscape' } " > <div class="home-view" data-options="dxContent : { targetPlaceholder: 'content' } " > <div data-bind="dxTileView: { dataSource: dataSource, height: '100%', baseItemHeight: 140, baseItemWidth: 140 }"> <div data-options="dxTemplate : { name: 'item' } " class="gallery-item"> <img data-bind="attr: { src: Image, alt: Name }" /> <div data-bind="text: Name"></div> <div><strong data-bind="text: Globalize.formatCurrency(Price, 'USD')"></strong></div> </div> </div> </div> </div>
Custom Context Specific Markup
You can introduce a custom context for a view template by specifying a custom option within the configuration object of the corresponding dxView markup component. In the following example, the 'home' view has two HTML templates - for day and night.
<div data-options="dxView : { name: 'home', title: 'Home', timeOfDay: 'day' } " > <div data-options="dxContent : { targetPlaceholder: 'content' } " > <p>It's time to work</p> </div> </div> <div data-options="dxView : { name: 'home', title: 'Home', timeOfDay: 'night' } " > <div data-options="dxContent : { targetPlaceholder: 'content' } " > <p>It's time to sleep</p> </div> </div>
To notify the application that the timeofDay context has changed, access the application's templateContext object and set the timeofDay option for it using the option(optionName, optionValue) method.
var templateContext = Application.app.templateContext(); function timeOfDayChangedHandler(currentTimeOfDay) { templateContext.option("timeOfDay", currentTimeOfDay); }
Context Specific View Model
The DevExtreme SPA framework allows you to define a view markup specifically for the current context (for a particular platform, device orientation or any custom context). You can learn how to do this in the Context Specific Markup topic. At the same time, you may need to have ViewModel fields specific to the current context too. In this instance, use the view's viewShown and viewHidden events to subscribe to and unsubscribe from tracking a context change.
The following example demonstrates how to change a view title when device orientation changes.
function onOrientationChanged(e) { viewModel.title(e.orientation); } var viewModel = { title: ko.observable(DevExpress.devices.orientation()), viewShown: function() { DevExpress.devices.on("orientationChanged", onOrientationChanged); }, viewHidden: function() { DevExpress.devices.off("orientationChanged", onOrientationChanged); } };
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 handler to be performed when executing the command, as well as a title, an icon, and enabled and visible states.
<div data-options="dxView: { name: 'home', title: 'Home' }"> <div data-bind="dxCommand: { id: 'myCommand', onExecute: '#product-details', title: 'Add' } "></div> </div>
Command markup options can be bound to a ViewModel's fields. Here is an example.
<div data-options="dxView: { name: 'home', title: 'Home' }"> <div data-bind="dxCommand: { id: 'myCommand', onExecute: 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 that is called when you are executing the Add command or a string/object representing the URL to navigate to. To learn how to define a URL using a string or an object, refer to the Navigate to a View section.
In some scenarios, it is not appropriate to add commands to a view using the view's markup. For instance, you may need to add the same commands to several views, or to populate a command collection dynamically (within the onViewShown event handler), etc. For this purpose, the DevExtreme SPA framework allows you to define a command collection within a ViewModel object.
var viewModel = { commands: [ an array of commands can be here ] };
The objects that represent commands in the commands array must have the same structure as the configuration objects of the dxCommand markup components.
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.
<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.
new DevExpress.framework.HtmlApplication({ commandMapping: { 'my-container': { defaults: { 'showIcon':false, 'location':'before' }, commands: [ { id: 'myCommand', location: 'after' // container defaults can be overriden } ] } } });
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 that you 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.
Depending on the command container chosen for a command in a layout and the device the application is running on, different widgets will be used to display commands. You can specify widget configuration settings directly within the dxCommand component's configuration object.
<div data-bind="dxCommand: { id: 'myCommand', onExecute: '#product-details', title: 'Add', hint: 'Add a product' } "></div>
In the code above, the specified hint option will be applied if the widget that will display the command has the hint option in its configuration.
View Life Cycle
The replacement of a view with another view is initiated by invoking the navigate method of the HtmlApplication object. When the application navigates to a view, the previous view is hidden or disposed. When it is hidden, this view can be restored from the cache quickly to be displayed again. When it is disposed, a new life cycle is initiated when the application navigates to this view repeatedly. All of the view life cycle steps are detailed below.
- Get View Info
- Create a View Model
- Render the View
- Show the View
- Hide the View
- Display the View Repeatedly
- Dispose the View
1 - Get View Info
When a view's display process begins, the only thing we know 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 view cache is used. Information on all 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 the view has not been displayed before or the cache is disabled. In this instance, 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 the layout markup. This controller, like all controllers that are registered in the application, is initialized beforehand.
The following events of the HTMLApplication object can be handled to change the flow of the view display at this step.
navigating
This event fires 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 fires before an appropriate layout controller from the application's layout set is found, based on 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 - Create a View Model
To get the View Model object, a function with the same name as the view is searched for 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 fires 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 fires after the ViewModel object is created for the view. At this time, you can modify the created ViewModel object. It is available to you as the model field of the viewInfo object exposed by the parameter object.
3 - Render the View
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 the 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 merged result is added to the corresponding dxTransition element of the layout as an additional view markup in an inactive state.
The following events of the HTMLApplication object can be handled to change the flow of the view display at this step.
- viewRendered
This event fires after 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.
4 - Show the View
To show the view, the inactive markup corresponding to this view is made active while other markup elements, which correspond to the previously shown view, are made inactive.
Handle the following event at this step.
viewShowing
This event is raised before showing the view.viewShown
This event fires 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.
5- 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 fires each time after a view is hidden. Access the viewInfo object using the viewInfo field of the object passed as a parameter.
6 - Display the View Repeatedly
The next time the application navigates to the view, the view is ready to be displayed 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.
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 then activated. Therefore, 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.
7 - Dispose the View
The following events are raised when the current view is not displayed and information on this view 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.
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.
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.
<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...', valueChangeEvent: 'keyup', min: 0 }"></div> </div> </div> </div>
MyApp.home = function(params) { var billTotal = ko.observable(), //... function viewShown() { $("#billTotalInput").dxNumberBox("instance").focus(); } return { billTotal: billTotal, viewShown: viewShown, //... }; };
If you have technical questions, please create a support ticket in the DevExpress Support Center.