GET Data Into Your App
In this post, I will demonstrate three methods for loading data into your JET application using GET calls to retrieve data from a REST API. You don’t need to know anything about JET to follow through the examples. However, I do assume that you’re familiar with JET so I won’t be explaining most of the JET functionality. If you would like to know more about Oracle JET, check out these resources.
Setup
The setup section will walk through creating a functioning JET application you can use to call each of the examples and display the data. If you prefer to just read through the examples you can skip down to the REST GET Examples section.
In order to make the REST calls, we’ll need an application with a REST API. I’ll be using DinoDate for the back end API.
Go to https://github.com/oracle/dino-date and follow the installation instructions. Verify that DinoDate is running before proceeding.
DinoDate includes a JET front end that you can look through, but for this post, we’re going to replace it with the navbar JET template.
Start with a template
Rename the commonClient directory and create a new empty directory:
|
cd dino-date/ mv commonClient/ commonClientOld mkdir commonClient cd commonClient |
Install Oracle JET following the Oracle Jet Get Started guide:
|
npm -g install yo bower grunt-cli npm -g install generator-oraclejet yo oraclejet tempJet --template=navbar |
Oracle JET is a very active open source project so the following may change over time. If the version you’re using is different, try to locate the files mentioned in the following commands. Locating the /src directory is required. If you can’t find the rest of these files, the examples should still work, they just won’t be as pretty.
Alternatively, you can install the version of the generator used for this post instead of the global install above:
|
npm install generator-oraclejet@2.3.0 |
This JET template comes with a lot of pre-installed functionality added to the tempJet directory. For this post, we’ll grab only the files that we need and put them in a jet directory
- Create the directory structure ‘jet/css/libs/oj/v2.3.0/alta’.
- Copy everything from tempJet/src to the root of the new jet folder.
- Copy and rename the .css file.
- Copy the fonts and images directories.
- Delete the tempJet folder.
- CD into jet.
|
mkdir -p jet/css/libs/oj/v2.3.0/alta cp -r tempJet/src/* jet cp tempJet/themes/alta/web/alta.min.css jet/css/libs/oj/v2.3.0/alta/oj-alta-min.css cp -r tempJet/themes/alta/web/fonts jet/css/libs/oj/v2.3.0/alta/fonts cp -r tempJet/themes/alta/web/images jet/css/libs/oj/v2.3.0/alta/images rm -r tempJet cd jet |
Now if you haven’t already started DinoDate,
follow the instructions to start your preferred middle tier.
Open a browser and go to http://localhost:3000/ (use the port for the mid-tier you started) and you should now see this.

Convert Existing Module
Rather than create a new module from scratch, I’m going to change the Customer module into a DinoDate member search module. This way we can see what’s included in the template and how it’s ‘wired’ together.
If you’re not already there, navigate to dino-date/commonClient/jet/
Rename the files js/views/customers.html and js/viewModels/customers.js to search.html and search.js.
|
mv js/views/customers.html js/views/search.html mv js/viewModels/customers.js js/viewModels/search.js |
In order to change our the Navigation List Item from ‘Customer’ to ‘Search’, we need to edit js/appController.js.
Change the code on the following lines (Line numbers may change with future versions of JET):
22:
'customers': {label: 'Customers'}, to
'search': {label: 'Search'},
33:
{name: 'Customers', id: 'customers', to
{name: 'Search', id: 'search',
34:
iconClass: 'oj-navigationlist-item-icon demo-icon-font-24 demo-people-icon-24'}, we’ll use the magnifier icon
iconClass: 'oj-fwk-icon-magnifier oj-fwk-icon oj-navigationlist-item-icon'},
DinoDate creates a user token as part of the login process. This token is required to access the API. This process is not really relevant to the examples so while we’re in appController.js we’ll add a helper (cheater) function to make the examples work.
The $.ajax function will automatically log us in as the Administrator user and set the authorization token when the application starts.
The getHeaders function will be used later to generate the headers used by DinoDate for authorization and processing options.
Add this code after
self.navDataSource = .... , line 38.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
self.token = ko.observable(); $.ajax({ type: "POST", url: "api/v1/logins", dataType: 'json', data: JSON.stringify({ email: "admin@example.com", password: "testtest" }), contentType: 'application/json', success: function(res) { self.token(res.token); }, failure: function(jqXHR, textStatus, errorThrown) { console.log(textStatus); self.headerText('Login Failed'); } }); // Generate authorization headers to inject into rest calls self.getHeaders = function() { return { 'headers': { 'Authorization': 'Bearer ' + self.token(), 'userToken': self.token(), 'DD-Process-Type': 'spatial' } }; }; |
Edit js/index.html and add an id property to the <div> with role=”main”.
|
<div role="main" class="oj-web-applayout-max-width oj-web-applayout-content" data-bind="ojModule: router.moduleConfig"> |
|
<div id="mainContent" role="main" class="oj-web-applayout-max-width oj-web-applayout-content" data-bind="ojModule: router.moduleConfig"> |
Refresh the page and click the Search tab.
Prepare the viewModel
Edit js/viewModels/search.js
Add the Jet components we plan to use in our view, to the list of dependencies.
|
define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojcomponents', 'ojs/ojtable', 'ojs/ojpagingcontrol', 'ojs/ojarraytabledatasource', 'ojs/ojmodel', 'ojs/ojcollectiontabledatasource'], |
Change
CustomerViewModel to
SearchViewModel for the function name and in the return statement at the bottom.
Delete everything inside the SearchViewModel function except for
var self = this;
Add a variable ‘rootViewModel’ to give us access to js/appController.js. We’ll use this to access the getHeaders function.
If you used a different id value in the
<div id="mainContent" role="main" section above, use that id instead of ‘mainContent’.
|
var rootViewModel = ko.dataFor(document.getElementById('mainContent')); |
Add some Knockout observables we’ll need for the view data-binding. We’ll use the observable searchRun to display the method used to call the REST API.
|
//Used by the HTML search criteria components self.keywords = ko.observable(); self.maxDistance = ko.observable(1000); //Data object used by the Table and Paging components self.memberData = ko.observable(); //Used to display which search was run self.searchRun = ko.observable(); |
Add a function to generate the URL for the search API.
|
//Used to generate the REST URL to include the search criteria self.searchURL = ko.computed(function() { return "api/v1/members" + '?searchString=' + self.keywords() + '&maxDistance=' + self.maxDistance(); }); |
Stub in the search functions.
|
self.searchViewModel = function(event, ui) { self.searchRun('View Model'); }; self.searchSharedModel = function(event, ui) { self.searchRun('Shared Model'); }; self.searchAJAX = function(event, ui) { self.searchRun('AJAX'); }; |
Change the View
Now let’s switch over to our view, edit js/views/search.html.
Delete the all of the HTML in the file.
If you want to dig into the details on the components we’re using you can check out the CookBook. For this example, we’ll just identify the components we’re using.
Add an ojInputText and an ojInputNumber to accept our search criteria for a Keyword and Distance. Set their values to the correct observables in the viewModel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
<div id='member-search' class="oj-flex"> <div class="oj-md-3"> <label for="keywordsInputSearch">Keywords</label> <input id="keywordsInputSearch" placeholder="Search..." data-bind="ojComponent: {component: 'ojInputText', value: keywords, rootAttributes: {style:'max-width: 20em'}}"> </div> <div class="oj-md-1"> <label for="maxDistanceInput">Distance</label> <input id="maxDistanceInput" placeholder="KM" data-bind="ojComponent: {component: 'ojInputNumber', max:10000, min:1, step:1, value: maxDistance, rootAttributes: {style:'max-width: 20em'}}"> </div> </div> |
Add three ojButton components, one for each method we’re going to demonstrate. We’ll set them to be disabled until the token in appController.js is populated to prevent querying before we’re logged in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
<div id='member-search-button' class="oj-flex"> <div class="oj-md-4"> <button class="oj-button-primary" data-bind="click: searchViewModel, ojComponent: { component:'ojButton', label: 'View Model', chroming: 'full', disabled: $root.token()==null}"> </button> </div> <div class="oj-md-4"> <button class="oj-button-primary" data-bind="click: searchSharedModel, ojComponent: { component:'ojButton', label: 'Shared Model', chroming: 'full', disabled: $root.token()==null}"> </button> </div> <div class="oj-md-4"> <button class="oj-button-primary" data-bind="click: searchAJAX, ojComponent: { component:'ojButton', label: 'AJAX', chroming: 'full', disabled: $root.token()==null}"> </button> </div> </div> |
Display which search function as been run and add an ojTable component to display the returned data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
<div class="oj-flex oj-flex-items-pad"> <div> <h5>Search Using: <span data-bind="text: searchRun"></span></h5> </div> <div class="oj-md-12 oj-flex-item"> <table id="membersTable" summary="Member List" aria-label="Member Table" data-bind="ojComponent: {component: 'ojTable', data: memberData, columns: [{headerText: 'Member', field: 'name', sortProperty: 'name'} ]}"> </table> </div> </div> |
And finally, an ojPagingControl to add pagination controls.
|
<div id="paging" data-bind="ojComponent: {component: 'ojPagingControl', data: memberData, pageSize: 10}"> </div> |
Notice the data property for the table and paging controls are both using the same ko.observable, memberData.
The setup is complete, let’s flesh out our search functions.
REST GET Examples
viewModel
Using the viewModel method is typically the default approach for making REST calls in JET. The JET components (oj.Model and oj.Collection) implement a lot of functionality behind the scenes allowing you to focus on your application instead of generic plumbing.
- Define the Member model by extending oj.Model . Since we don’t plan to do any single record functions, we only need to define the idAttribute of a Member.
- Define a collection of by extending oj.Collection.
- Set the base URL for the REST API.
- Set the model to Member, which we defined above.
- Set customURL to our getHeaders function in appController.js. This adds the headers needed for DinoDate.
- Parse the returned object and return the member array ‘items’.
Sometimes we only need part of the data returned by the REST API, so we need to define a function to parse the response. For our examples, all we need is the members.items array. If your REST API returns only the array, you shouldn’t need to define a parse function at all.
- Create a new Members collection.
- Create the members variable using membersColl to create a new oj.CollectionTableDataSource which is then used to create a new oj.PagingTableDataSource.
- Modify the URL of membersColl using the searchURL function. This will create the URL using the ko.observables keywords and maxDistance.
- Call the fetch function.
- Set self.memberData(self.members); after the fetch() has returned.
Replace the stubbed searchViewModel function with this code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
self.searchViewModel = function(event, ui) { self.searchRun('View Model'); var Member = oj.Model.extend({ idAttribute: "memberId" }); var Members = oj.Collection.extend({ url: "api/v1/members", model: Member, //used to generate authentication and config headers customURL: rootViewModel.getHeaders, // The parse function modifies the response object, extracting and returning the members array. parse: function(members) { return members.items; } }); var membersColl = new Members(); // Data source for ojTable and ojPagingControl controls. var members = new oj.PagingTableDataSource(new oj.CollectionTableDataSource(membersColl)); //Re-build the url to include the search criteria membersColl.url = self.searchURL(); membersColl.fetch().then(function() { // Set the data object value to the fetched records. self.memberData(members); }); }; |
In your browser, refresh your page and search for the letter ‘a’ using the ViewModel button. You should see a list of Member names. The ojPagingControl defines a page as 10 records so you may have multiple pages of members.
Shared Model
Sometimes you will want to use the same Model and/or Collection across multiple functions or viewModels. Trying to keep our application as DRY as possible, we’ll move the model and collection definitions out to their own files.
Let’s create a new directory
/js/models and two new files
/js/models/Member.js and
/js/models/Members.js .
For both files, we’ll define components we need to include and also add the variable rootViewModel.
In Member.js, we’ll create a Members Model. This model could be used by itself to work with a single Member object, see oj.Model for more information.
- Create a Member by extending oj.Model, as shown in the last example.
- Add the urlRoot for the members endpoint of your RESTFull API.
- The customURL property is used for the DinoDate headers, just like in the previous Collection example. If your application doesn’t need to modify the headers, you could exclude this property.
- Return the new Model.
Edit Member.js and add the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
'use strict'; /* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. */ define(['ojs/ojcore', 'knockout', 'ojs/ojmodel'], function (oj, ko) { // Object used to access RootViewModel in main.js var rootViewModel = ko.dataFor(document.getElementById('mainContent')); var Member = oj.Model.extend({ idAttribute: "memberId", urlRoot: "api/v1/members", //used to generate authentication and config headers customURL: rootViewModel.getHeaders }); return Member; } ); |
For our Members Collection:
- Add the new member model to the define dependencies array.
- Accept the member model as an argument to the function.
- Create a Members collection by extending oj.Collection, like in the last viewModel example.
- Set the URL. In this case, it’s the same as the rootUrl for Member.
- Set the model to Member.
- We use the customURL for the DinoDate headers, the same as in the other examples.
- Parse the returned object. When DinoDate returns a set of members it includes some extra data. We are only interested in the items array.
- Return the collection.
Edit Members.js and add the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
'use strict'; /* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. */ define(['ojs/ojcore', 'knockout', 'models/Member', 'ojs/ojmodel'], function (oj, ko, Member) { var rootViewModel = ko.dataFor(document.getElementById('mainContent')); var Members = oj.Collection.extend({ url: "api/v1/members", model: Member, //used to generate authentication and config headers customURL: rootViewModel.getHeaders, //The parse function modifies the returned object, returning the members array. parse: function (members) { return members.items; } }); return Members; } ); |
Back to search.js.
Include our new collection in the define dependencies array, add ‘models/Members’ right after ‘jquery’.
|
define(['ojs/ojcore', 'knockout', 'jquery', 'models/Members', |
Accept the Members Collection into the function definition by adding the Members argument.
|
function(oj, ko, $, Members) { |
To flesh out the searchSharedModel function we simply copy the code from searchViewModel() excluding the Model and Collection definitions.
- Create a new Members collection.
- Create the members variable using membersColl to create a new oj.CollectionTableDataSource which is then used to create a new oj.PagingTableDataSource.
- Modify the URL of membersColl using the searchURL function. This will create the URL using the ko.observables keywords and maxDistance.
- Call the fetch function.
- Set self.memberData(self.members); after the fetch() has returned.
Replace the stubbed searchSharedModel function with this code.
|
self.searchSharedModel = function(event, ui) { self.searchRun('Shared Model'); var membersColl = new Members(); // Data source for ojTable and ojPagingControl controls. var members = new oj.PagingTableDataSource(new oj.CollectionTableDataSource(membersColl)); //Re-build the url to include the search criteria membersColl.url = self.searchURL(); membersColl.fetch().then(function() { // Set the data object value to the fetched records. self.memberData(members); }); }; |
Refresh your page and search for ‘a’ again using the Shared Model button, you should see the same results as the ViewModel button.
AJAX
Sometimes you may need some specific functionality not supported by the framework. If extending oj.Collection or oj.Model doesn’t meet all of your needs, you can use the jQuery ajax method.
- We’re using a standard $.ajax call with a type of “GET”.
- Set the headers needed by DinoDate using the getHeaders() function in appController.js.
- The searchURL function will create the URL using the ko.observables keywords and maxDistance.
- Assuming everything is setup properly and we get a successful response we need to create the proper object types for the Jet components we’re using in the view.
- Since we are using a paging control, we’ll need an oj.PagingTableDataSource object which we’ll create from an oj.ArrayTableDataSource object.
- The oj.ArrayTableDataSource object is created from the items array returned in the GET response.
- We also need to identify the idAttribute of our returned objects, which is ‘memberId’.
- Finally, we’ll set the Knockout Observable self.memberData to our new resData object.
- If there’s a failure, show it in an alert.
Replace the stubbed searchAJAX function with this code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
self.searchAJAX = function(event, ui) { self.searchRun('AJAX'); $.ajax({ type: "GET", headers: rootViewModel.getHeaders().headers, url: self.searchURL(), success: function(res) { var resData = new oj.PagingTableDataSource(new oj.ArrayTableDataSource(res.items, { idAttribute: 'memberId' })); //Set the data object value to the response items. self.memberData(resData); }, failure: function(jqXHR, textStatus, errorThrown) { alert(textStatus); } }); }; |
Refresh your page and search for ‘a’ using the AJAX button, you should see the same results as the other buttons.
If you are watching the execution time in the returned object, be aware that the time is only tracking the call from the middle tier to the database, so these changes to the client side code do not affect the execution time. Any differences for these examples should be ignored.
Be Flexible
As you build applications with Oracle JET, you will probably use the viewModel method the most. However, don’t be afraid to use other methods when they make more sense for your application. It is perfectly acceptable to mix and match these different approaches as needed. Use the shared model to try and stay DRY and the AJAX method for special cases.
As the saying goes; When you only have a hammer, everything looks like a nail. Keep your toolbox full.
You must be logged in to post a comment.