In this article, I will describe how I took the Oracle JET Sample application that I deployed to Node.js – locally and in the Oracle Application Container Cloud-and extended it with a new tab containing a table component that is data bound to a collection populated from a REST API that also runs on Application Container Cloud and accesses a DBaaS instance.
The initial application was described in this previous article (Deploying an Oracle JET application to Application Container Cloud and running on Node.js); in that article I took the Oracle JET Starter Template Quickstart sample application, configured it to run on Node.js (locally) and then configured it for deployment on Application Container Cloud and subsequently ran it on the cloud. In this article, I will leverage the REST API exposed from a Node.js application Data API discussed in an earlier article: REST API on Node.js and Express for data retrieved from Oracle Database with node-oracledb Database Driver running on Application Container Cloud. This Data API connects to a DBaaS instance and retries data from the DEPARTMENTS table in the HR schema.
Extending the sample application with an additional tab that can be selected and shown, containing a table component with data binding to a newly defined viewmodel did not prove trivial at first. This was my first encounter with some of the frameworks included in JET – Require and especially Knock Out – and I ran into many small typos and fairly large misunderstandings. Now I believe I nailed it – and this article is the proof.
The Sample application’s directory structure is as follows:
I have listed the relevant files for this example. The Templates provide the UI content for specific areas in the main page that itself is defined through index.html in the root directory of the JET application (/public).
The js/modules directory contains the JavaScript counterparts to the templates – that provide the View Model for the content areas defined by the templates.
The js directory contains the JavaScript counterpart to index.html: main.js. It also contains a user defined (me defined) JavaScript module that is imported into main.js – to demonstrate modularization of the code which is not strictly required for this article (or even relevant to be honest).
The overall effect of these files on the application is demonstrated in the next figure:
The steps I went through to add the new HRM tab are these:
- create a template file (hrm.tmpl.html in directory /views/templates)
- create a new JavaScript file (hrm.js in directory /public/js/modules)
- add new tab to router configuration in main.js in directory /public/js
- add new application navigation option for HRM tab in header.js in /public/js/modules
View Models for Data Binding
Every template has an associated JavaScript Module – in this case hrm.js for the hrm template in file hrm.tmpl.html. This JavaScript file can contain a function that is invoked by the Knock Out framework to return the ViewModel for the HTML content defined in the template. The data binding between the template content and this view model is carried out implicitly, and does not require an explicit call to applyBindings. This is one of the things that had me confused: you only have to just return a view model from the function in the JavaScript module for this template, ensure that this view model contains the right data, and the data binding context is available for UI components in the HTML template to bind against.
Additionally: an HTML container (a DIV usually) can be bound to a view model only once. If the root HTML container on a page is bound to a view model, then contained elements such as child DIVs cannot be bound to a more specific view model – thereby overruling the global view model. Or at least, that is the out of the box behavior that I ran into. The built in templating mechanism clearly works around that limitation and allows the template content to be bound to the view model defined in the corresponding JavaScript module. On closer inspection of the Knockout documentation and some StackOverflow threads, I have learned that there are ways around this behavior. An obvious one is to work with child view model in the root view model (simply include a specific area’s viewmodel as a property in the root viewmodel en use data binding references such as data: nestedVM.specificProperty. More elegant are more recent options in Knockout to stop binding (from a root view model) at a certain point – to be replaced with a more specific view model for a certain area.
Just to get a little feel for how this works, I have created a new JavaScript module – app.js in the directory public/js. This module defines a function that returns an object with one property called departmentVM. The value of this property is a function (departmentViewModel) that itself defines a single property (personName).
define(['ojs/ojcore', 'knockout'], function(oj, ko, $) { function departmentViewModel() { var self = this; self.personName= ko.observable('Bob the Builder'); } return {'departmentVM': departmentViewModel}; });
In main.js – I have added the new module app to the list of required modules. Then I added app as an input parameter to the callback function. I have finally added a new instance of function departmentVM returned from module app to the rootViewModel, as property childvm.
require(['ojs/ojcore', 'knockout', 'jquery', 'app', ...], function (oj, ko, $,app) // this callback gets executed when all required modules are loaded { ... //bind model returned from app to section in hrm var vm = new app.departmentVM(); var rvm = new RootViewModel(); rvm.childvm = vm; //bind your ViewModel for the content of the whole page body. ko.applyBindings(rvm, document.getElementById('globalBody')); ... } );
In the index.html page – whose view model is provided by main.js – I have added a data bound component:
<a data-bind="text : childvm.personName">
The data binding for this anchor element is automatically in linked to the rootViewModel define in main.js. The binding childvm.personName refers to the childvm property in the rootViewModel that contains the departmentViewModel defined in app.js; this contains the personName property – whose value is initialized to Bob the Builder, the value we see in the screenshot.
Add the Data Bound Table
Add the table component to the template:
<div id="hrm-content" class="demo-apphrm"> <div class="oj-row"> <div id="deptList" class="oj-md-9 oj-col"> <table id="table" data-bind="ojComponent: {component: 'ojTable', data: datasource, columns: [{headerText: 'Remove', id: 'column1', sortable: 'disabled'}, {headerText: 'Department Id', field: 'DepartmentId', sortable: 'enabled'}, {headerText: 'Department Name', field: 'DepartmentName', sortable: 'enabled'}, {headerText: 'Location', field: 'Location'}], selectionMode: {row: 'none', column:'none'}}"> </table> </div> </div> </div>
The table component is data bound to a collection that should be present in the current view model – the one returned from the hrm.js module. This collection model should be called datasource in the view model object and also should contain a set of elements (aka records). Each record should contain the properties used in the field definition in the elements in the columns array: DepartmentId, DepartmentName and Location. When we define the view model, we have to make sure of these property names. See how sorting is enabled on the columns Department Id and Department Name; this sorting is handled automatically and client side; note that sorting and paging are probably not a good combination.
Create the JavaScript function in hrm.js to return the view model that contains the datasource collection. Using various examples of the collection model in Oracle JET I managed to put the following code together and sort of understand it. It is not intuitive: it took me a little while to get used to. It felt that there are one or two levels too many required to define the collection. But perhaps I will come to appreciate it the way it is. What it does by the way is pretty cool: the collection model will go out to the REST service url, fetch the data set and turn the JSON response into the records in the collection; it can then produce the records in the structure that we need in our client, which may differ from the internal structure returned by the REST service. In my example, I have turned the all capital and underscore property names (straight out of the database) to more JavaScript [programmer] friendly initcapped field names.
To dissect the code:
- datasource needs to provide an instance of CollectionTableDataSource that the ojTable component can work with
- this CollectionTableDataSource can be created from an ojCollection , in this case held in DeptCol and based on DeptCollection.
- DeptCollection is a new ojCollection instance – tied to a REST service url that it will invoke to fetch a JSON array of objects. For each object in that array, DeptCollection will create an instance of Department()
- Department defines a single record – based on ojModel; this record definition in its most simple form specifies the property that is the record’s identifier (here DepartmentId) and the function that creates the record from a single JSON element; this function is parseRESTDBDepartment in this case. When the REST service has returned the JSON array, for each element in the array this function is invoked to produce the proper record structure
- function parseRESTDBDepartment is straightforward: it returns a JavaScript “object” constructed from the data passed in (the JSON element taken from the array returned by the REST service) and anything else it can get its hands on (in this case the constant Zoetermeer)
- the hrmViewModel returned by the outer JavaScript function is the view model that provides the data binding context for the UI content in the hrm template. The ojTable binds its data property to the datasource property in that view model
define(['ojs/ojcore', 'knockout','ojs/ojmodel', 'ojs/ojtable'], function(oj, ko) { function hrmViewModel() { var self = this; self.serviceURL = 'https://data-api-lucasjellema.apaas.em2.oraclecloud.com/departments'; self.DeptCol = ko.observable(); // DeptCol is an observable so when its data is in (from the REST service), the datasource is triggered self.datasource = ko.observable(); // datasource is an observable so when it is triggered/refreshed, the table component is triggered function parseRESTDBDepartment(response) { return {DepartmentId: response['DEPARTMENT_ID'], DepartmentName: response['DEPARTMENT_NAME'], Location: "Zoetermeer"}; } // think of this as a single record in the DB, or a single row in your table var Department = oj.Model.extend({ parse: parseRESTDBDepartment, idAttribute: 'DepartmentId' }); // this defines our collection and what models it will hold var DeptCollection = oj.Collection.extend({ url: self.serviceURL , model: new Department(), comparator: "DepartmentId" }); self.DeptCol(new DeptCollection()); self.datasource(new oj.CollectionTableDataSource(self.DeptCol())); } return hrmViewModel; });
Deploy and Run the JET Application on Application Container Cloud
Running the application locally during development is very simple: using npm start – node.js is started to serve the application on localhost port 3000. Changes in source code are picked up immediately and passed on to the browser.
Once the development is complete, the application can be zipped – provided the manifest.json and package.json are there, as discussed in my previous article (Deploying an Oracle JET application to Application Container Cloud and running on Node.js) – and deployed to the Application Container Cloud, as illustrated in these screenshots:
Then open the application once deployment is complete:
The data we are looking at, comes from the DBaaS instance that is accessed by the DataApi node.js application that implements the REST service. We can also look at that data in SQL Developer, going straight to the DBaaS instance:
Let’s update the name of department 40, from Human Resources to Human Capital:
Now when we refresh the browser – or even switch between tabs – the table is refreshed:
and the updated values are shown:
Creating a data bound table that fetches its data from a REST API turns out to be very hard at all. Running all-cloud applications – Front End Web application, REST API, DBaaS instance – and tying them together is not that difficult either. Once you know how to do it.
Resources
Oracle JET Documentation: On Data Binding, Using the Oracle JET Common Model and Collection Framework, Using Knockout.js Templates and the ojModule Binding
Quick Tip: Telling Knockout to Skip Binding Part of a Page
Andrejus Baranovskis’ blog article Oracle JET – Rendering Table from ADF BC REST Service
Download the Oracle JET application discussed in this article: oraclejetwithrest_onnode.zip . Note: the libraries shipped with Oracle JET (directory public\js\libs) were removed to reduce the size of the zip file.