Designing Single-Page Applications Using Oracle JET

The Oracle JET framework includes Knockout for separating the model layer from the view layer and managing the interaction between them. Using Knockout, the Oracle JET ojModule binding, and the Oracle JET oj.Router framework class, you can create single-page applications that look and feel like a standalone desktop application.

Topics:

Understanding Oracle JET Framework Support for Single-Page Applications

Single-page applications (SPAs) are typically used to simulate the look and feel of a standalone desktop application. Rather than using multiple web pages with links between them for navigation, the application uses a single web page that is loaded only once. If the page changes because of the user's interaction, only the portion of the page that changed is redrawn.

The Oracle JET framework includes support for single page applications using the oj.Router class for virtual navigation in the page, the ojModule binding for managing view templates and viewModel scripts, and Knockout for separating the model layer from the view layer and managing the interaction between them.

In the Oracle JET Sample-SPA-ojModule-ojRouter application shown in the image below, the index.html page displays a simple page with a toolbar for choosing the Home, Book, and Tables content.

The image is described in the surrounding text.

When the user selects a toolbar item such as the Book button, the new content displays, and the URL changes to reflect the user's current location on the page.

The image is described in the surrounding text.

If the user chooses several chapters to display, the browser remembers the previous selections. When the user selects the browser's back function, the URL and page content change to reflect the user's previous selection.

In this example, routing is handled using path segments (web_site/public_html/book), but you can also configure routing to use query parameters (web_site/public_html/index.html?root=book).

The image is described in the surrounding text.

In general, use query parameters when your application contains only a few views that the user will bookmark and that are not associated with a complex state. Use path segments to display simpler URLs, especially for nested paths such as book/preface/intro.

The Oracle JET Cookbook and Oracle JET sample applications use the native templating mechanism included with Knockout.js, and many use the Oracle JET ojModule namespace to manage the Knockout binding. With ojModule, you can store your HTML content for a page section in a template file and the JavaScript functions that contain your viewModel in a viewModel file.

When ojModule and oj.Router are used in conjunction, you can configure an ojModule object where the module name is the router state. When the router changes state, ojModule will automatically load and render the content of the module name specified in the value of the current RouterState object.

ojModule is not specific to single-page applications, and you can use it to reuse content in multi-page applications. For additional information about Knockout templates and ojModule, see Using Knockout.js Templates and the ojModule Binding.

Creating a Single-Page Application in Oracle JET

The Oracle JET Cookbook includes complete examples and recipes for creating a single-page application using path segments and query parameters for routing and examples that use routing with the ojModule binding. Regardless of the routing method you use, the process to create the application is similar.

To create a single-page application in Oracle JET:

  1. Create the application that will house your main HTML5 page and supporting JavaScript. For additional information, see Getting Started with Oracle JET Web Application Development or Getting Started with Oracle JET Hybrid Mobile Application Development.

  2. Design the application's structure.

    Identify the templates and ViewModels that your application will require. For example, the Sample-SPA-ojModule-ojRouter single-page sample application defines templates for the header and footer. The content for the main container will vary, depending upon the router configuration. The code sample below shows the index.html file.

    <!DOCTYPE html>
     
    <html lang="en-us">
      <head>
         ... contents omitted
      </head>
      <body>
        <div id="globalBody" style="display: none">
          <header id="headerWrapper" role="banner" data-bind="ojModule: 'header'"></header>
          <!-- This is where your main page content will be loaded -->
          <div id="mainContainer" role="main" data-bind="ojModule: router.moduleConfig"></div>
          <footer id="footerWrapper" role="contentinfo" data-bind="ojModule: 'footer'"></footer>
          </div>
       </body>
    </html>
    

    In this example, ojModule manages content replacement for the header, main content, and footer sections of the body, and oj.Router provides routing support. For additional information about working with Knockout templates and ojModule, see Using Knockout.js Templates and the ojModule Binding.

    The application also contains content that displays when the user clicks Book and Tables in the application header. To manage the content for those sections, the application includes the bookContent.html and tablesContent.html view templates with corresponding bookContent.js and tablesContent.js ViewModels.

    The image below shows a portion of the directory structure for the Sample-SPA-ojModule-ojRouter single-page sample application. The ViewModel definitions are contained in JavaScript files in the js/viewModels folder, and the view templates are contained in HTML files in the js/views folder.

    The image is described in the surrounding text.
  3. Add code to your application's main script that defines the states that the router can take, and add the ojs/ojrouter module to your require() list.

    For example, in the Sample-SPA-ojModule-ojRouter application, the router can be in the home, book, or tables states. The code sample below shows the code that configures the router.

    require(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout', 'ojs/ojmodule', 'ojs/ojrouter', 'text'],
            function(oj, ko, $) // this callback gets executed when all required modules are loaded
      var router = oj.Router.rootInstance;
      router.configure({
          'home':   { label: 'Home',   value: 'homeContent', isDefault: true },
          'book':   { label: 'Book',   value: 'bookContent' },
          'tables': { label: 'Tables', value: 'tablesContent' }
      });
      // Only the router is need in the viewModel
      var viewModel = {
          router: router
      };
    
      // To change the URL adapter, un-comment the following line
      // oj.Router.defaults['urlAdapter'] = new oj.Router.urlParamAdapter();
     
      oj.Router.sync().then(
          function() {
              ko.applyBindings(viewModel);
              $('#globalBody').show();
          },
          function(error) {
             oj.Logger.error('Error when starting router: ' + error.message);
          });
    });
    

    To configure the router for query parameters, remove the comment as directed above:

    oj.Router.defaults['urlAdapter'] = new oj.Router.urlParamAdapter();
    
    To configure the router for path segments, you must also set the baseUrl property to the base URL of the application. This provides the router with a base to calculate which part belongs to the router state and which part is the existing URL.
    requirejs.config({
       // Need to set baseUrl or nested view won't work because module location relative to current url.
       // Change to the correct baseUrl when deployed to site like: http://host/myApp
       baseUrl: window.location.href.split('#')[0].substring(0, window.location.href.split('#')[0].lastIndexOf('/')) + '/js',
    // Path mappings for the logical module names
       paths: {
       ... contents omitted
    });
    

    Note:

    Routing with path segments also requires that the web server recognizes the paths as existing resources and routes the requests to the root page. If you're using the Apache web server, you can use the mod_rewrite module to rewrite requested URLs on demand. For other web servers, you can create a servlet filter or use one of the publicly available URL rewrite filters.

  4. Add code to the markup that triggers the state transition and displays the content of the current state.

    For example, the Oracle JET Sample-SPA-ojModule-ojRouter single-page application defines buttons in a button set in the header section that trigger the router state transition when the user clicks a button. The code sample below shows a portion of the header.html markup.

    <div class="oj-toolbar-half-chrome" aria-controls="player"
         data-bind="ojComponent: {component:'ojToolbar'}">
      <div class="oj-button-half-chrome"
           data-bind="ojComponent: { component: 'ojButtonset',
                                     checked: $root.router.stateId(),
                                     focusManagement: 'none'}">
        <!-- ko foreach: $root.router.states -->
        <label data-bind="attr: {for: id}"></label>
        <input type="radio" name="view" 
               data-bind="value: id, click: go, attr: {id: id}, 
                         ojComponent: {component: 'ojButton', label: label}"/>
        <!-- /ko -->
      </div>
    </div>
    

    When the user clicks one of the toolbar buttons in the header, the content is loaded according to the router's current state. For example, when the user clicks the Book button, the method specified in the click option executes. In this example, the go() method of the RouterState object associated with the Book button executes. Once the state successfully changes to book, the value of router.moduleConfig changes to an object representing the book module, and ojModule changes the content to display the bookContent.html view and load the bookContent.js ViewModel.

    For additional information about creating templates and ViewModels, see Using Knockout.js Templates and the ojModule Binding.

  5. To manage routing within a module, add a child router using oj.Router.createChildRouter().

    For example, when the user clicks Books , the page displays a list of chapters that the user can select. The user can move back and forth between the chapters, and the child router maintains the state. The HTML markup is stored in the bookContent.html template and is shown below.

    <div id="chapter" class="oj-flex oj-flex-items-pad">
      <div class="oj-xl-2 oj-lg-2 oj-md-2 oj-sm-12 oj-flex-item">
        <br>
        <div id="menu" data-bind="ojComponent:{ component: 'ojNavigationList', 
                                                  selection: router.stateId(),
                                                  beforeSelect: selectHandler,
                                                  drillMode: 'none' }">
          <ul data-bind="foreach: router.states">
            <li data-bind="attr: {id: id}">
              <a data-bind="text: label"></a>
            </li>
          </ul>
        </div>
      </div>
      <div class="oj-xl-10 oj-lg-10 oj-md-10 oj-sm-12 oj-flex-item" data-bind="if: router.stateId()">
        <div role="main" class="oj-panel oj-web-applayout-content" data-bind="with: router.currentState()">
          <h2 id="currentPage" class="oj-header-border" data-bind="text: label"/>
          <h4 data-bind="text: value"/>
        </div>
      </div>
    </div>
    

    The template defines the Oracle JET ojNavigationList component and uses the router's state IDs as an argument to the ojNavigationlist selection option. To display the chapter list, the template iterates through each RouterState object in the router.states property.

    The code sample below shows the bookContent.js file that defines the child router and a portion of the book content. As with the parent router, the code defines all possible states for the page region.

    define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout',
            'ojs/ojrouter', 'ojs/ojnavigationlist'], function(oj, ko, $) {
      var chapters = {
             'preface': 'Darn beamed hurriedly because banal more ...',
             'chapter1': 'Affectingly and yikes one that along ...'
             'chapter2': 'More up mistaken for a kissed therefore ...',
             'chapter3': 'Reindeer up while the far darn falcon...'
      };
     
       // The view model for the book page
       var viewModel = {
         router: undefined,
     
         initialize: function(params) {
         // Retrieve parentRouter from ojModule parameter
         var parentRouter = params.valueAccessor().params['ojRouter']['parentRouter'];         
         // Restore current state from parent router, if exist.
         var currentState = parentRouter.currentState();
         if (!currentState.storage) {
           currentState.storage = chapters;
         }
         this.router = parentRouter.createChildRouter('chapter')
           .configure({
             'preface': {  label: 'Preface',   value: currentState.storage['preface'] },
             'chapter1': { label: 'Chapter 1', value: currentState.storage['chapter1'] },
             'chapter2': { label: 'Chapter 2', value: currentState.storage['chapter2'] },
             'chapter3': { label: 'Chapter 3', value: currentState.storage['chapter3'] }
           });
     
          // Now that the router for this view exist, synchronise it with the URL
             oj.Router.sync();
          },
     
          selectHandler: function(event, ui) {
             if ('menu' === event.target.id && event.originalEvent) {
                // Invoke go() with the selected item.
                viewModel.router.go(ui.key);
             }
          },
     
          dispose: function() {
             this.router.dispose();
             this.router = null;
          }
       };
     
       return viewModel;
    });
    
  6. Add any remaining code needed to complete the content or display.

You can download the Oracle JET Sample-SPA-ojModule-ojRouter application at SPA-ojModule-ojRouter.zip. The sample application includes the simple index.html page displayed in the preceding example and the content for the Book and Tables sections.

The Oracle JET Cookbook also includes additional Routing Demos that implement oj.Router and a link to the oj.Router API documentation.