Table of Contents
Examples of developing for the Web client
Angular
Minimum Working Example: Basic screen
Create a new component
Components are the building blocks of the Angular staff client. Each one contains some TypeScript and some view code (called a template, which may or may not be broken up into a separate file).
To start with, we will create a super simple component that just displays the words "Evergreen is good at math". To do this, create a new folder called Open-ILS/src/eg2/src/app/staff/math.
Inside your folder, create a file called eg-math.component.ts with the following content:
import { Component } from '@angular/core'; @Component({ template: '<p>Evergreen is good at math</p>', }) export class EgMathComponent { }
Add your component to a module
Next, components have to be grouped into modules – basically collections of similar components. Let's create a math module inside the same folder. Create a file called math.module.ts with the following content:
import {NgModule} from '@angular/core'; import {EgMathComponent} from './eg-math.component'; import {RouterModule, Routes} from '@angular/router'; const routes: Routes = [{ path: 'eg', component: EgMathComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], declarations: [EgMathComponent] }) export class MathModule { }
Add your module to the Staff router
Finally, it's time to let the rest of Evergreen's Angular client know where to find your code. Add this to Open-ILS/src/eg2/src/app/staff/routing.module.ts, within the routes constant:
{ path: 'math', loadChildren: '@eg/staff/math/math.module#MathModule' },
Re-compile, the client, and you should see your brand new screen at https://[yourdomain.edu]/eg2/en-US/staff/math/eg
Add another component with an OpenSRF call
Create a new .ts file in Open-ILS/src/eg2/src/app/staff/math, called adder.component.ts:
import {Component, OnInit} from '@angular/core'; import {NetService} from '@eg/core/net.service'; @Component({ selector: 'eg-adder', templateUrl: './adder.component.html', }) export class AdderComponent implements OnInit { sum: number = 0; addTwoNumbers: (first: number, second: number) => void; constructor( private net: NetService ){} ngOnInit() { this.addTwoNumbers = (first: number = 0, second: number = 0) => { this.net.request( 'opensrf.math', 'add', first, second) .subscribe(response => this.sum = response); } } }
This has some differences from our first one. First of all, it has a selector, which means that we can include it in other components by saying <eg-adder></eg-adder>. Secondly, it has its template in a separate file, Open-ILS/src/eg2/src/app/staff/math/adder.component.html. In fact, let's create that now:
<label i18n>First Number: <input #firstNumber type="number"></label> <label i18n>Second Number: <input #secondNumber type="number"></label> <button (click)="addTwoNumbers(firstNumber.value, secondNumber.value)">Add</button> <p>Sum: {{sum}}</p>
Let's update our module to let it know about the new component:
import {NgModule} from '@angular/core'; import {EgMathComponent} from './eg-math.component'; import {AdderComponent} from './adder.component'; import {RouterModule, Routes} from '@angular/router'; const routes: Routes = [{ path: 'eg', component: EgMathComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], declarations: [EgMathComponent, AdderComponent] }) export class MathModule { }
Finally, let's update our first component's template to include a reference to our new component. To do this, just change the line template: '<p>Evergreen is good at math</p>
to template: '<p>Evergreen is good at math</p><eg-adder></eg-adder>
'
AngularJs
Minimum Working Example: Screen that gets data from OpenSrf
Planning
Decide on the requirements and UI of your new screen
Our example will ask the user for two numbers, then add them when the user presses a button.
Decide which Evergreen module your screen is part of
For example, is it part of circ? booking? cataloging? Today's example is under cataloging module.
Creating your screen
Add a route for your screen
Find the app.js for the appropriate module. The app.js files are in Open-ILS/web/js/ui/default/staff
in the git repository, in /openils/var/web/js/ui/default/staff
for installed systems.
Today, we'll add it to web/js/ui/default/staff/cat/catalog/app.js
:
$routeProvider.when('/cat/catalog/math', { templateUrl: './cat/catalog/t_add', controller: 'MathematicsCtrl', resolve : resolver });
Create a controller for your screen
This can be in the same js file as your route was. Here's one to add to web/js/ui/default/staff/cat/catalog/app.js
:
.controller('MathematicsCtrl', ['$scope', 'egCore', function($scope, egCore) { $scope.firstNumber = 1; $scope.secondNumber = 6; $scope.add_things = function() { egCore.net.request('opensrf.math', 'add', $scope.firstNumber, $scope.secondNumber ).then( function(response) { $scope.sum = response; }); } }])
Create a tt2 template for your screen
Look for an index.tt2 file that includes the JS file you were working on. Create a new file that begins with t_
in the same directory. Starting the template with t_
will add the Evergreen Web client header and other goodies from the index.tt2 file in the same directory. This includes the controller that you created during the last step.
The name and path to your new tt2 file should match the templateUrl you entered in your route. For example, if your templateUrl was ./cat/catalog/t_add
, create a tt2 file at Open-ILS/src/templates/staff/cat/catalog/t_add.tt2
in the git repo or /openils/var/templates/staff/cat/catalog/t_add.tt2
in an installed system.
<form ng-submit="add_things()"> <label>[% l('First Number:')%] <input type="text" ng-model="firstNumber"></label> <label>[% l('Second Number:')%] <input type="text" ng-model="secondNumber"></label> <button type="submit" value="Add" class="btn btn-success" id="submit">[% l('Add')%]</button> Sum: {{sum}} </form>
Test your screen
It will be available at https://[DOMAIN_NAME]/eg/staff/cat/catalog/math
Add a popup modal to your screen
This example builds on the previous example of an addition screen by adding a button, which, when pressed, opens up a modal asking the user to confirm.
Edit the controller
First of all, edit the controller to make sure it can access the egAlertDialog factory:
controller('MathematicsCtrl', ['$scope', 'egCore', 'egAlertDialog', function($scope, egCore, egAlertDialog) {
Then add a function to the controller that makes use of the factory:
$scope.provide_feedback = function() { egAlertDialog.open(egCore.strings.SHARE_YOUR_OPINION); }
Add your string to egCore.strings
We want to be able to translate the egAlertDialog text to the language that the user prefers. To do this, we will define it in the parent .tt2 page (in this case, templates/staff/cat/catalog/index.tt2
).
To do this, look for the area in between <script>angular.module('egCoreMod').run(['egStrings', function(s) {
and }])</script>
, where other egCore.strings are defined. Then add:
s.SHARE_YOUR_OPINION = "[% l('I think so too!') %]";
Edit the tt2 template
Here, we'll just add a simple button outside of the <form> that, when clicked, causes our alert to display:
<button ng-click="provide_feedback()" class="btn btn-default" id="feedback"> [% l('I think Evergreen is cool!') %] </button>
Note: Evergreen has several other built-in types of modals, such as egConfirmDialog, egPromptDialog, and egSelectDialog. You can find a complete list, as well as usage documentation, in this file: https://github.com/evergreen-library-system/Evergreen/blob/main/Open-ILS/web/js/ui/default/staff/services/ui.js
Add an egGrid to your screen
One of the best ways to present tabular data in Evergreen is to use an egGrid. This example builds on the previous two examples by adding an egGrid that displays all the contents of a specific table in the database. Our grid will display a very simple table called biblio.peer_type
.
Edit the controller
First of all, edit the controller to make sure it can access the egGridDataProvider factory:
controller('MathematicsCtrl', ['$scope', 'egCore', 'egAlertDialog', 'egGridDataProvider', function($scope, egCore, egAlertDialog, egGridDataProvider) {
Then configure your grid using the gridControls hash. This is where we will set a query indicating which rows we want to fetch from the database to populate our egGrid. Without this query, egGrid won't know what to fetch, and our grid will be empty.
$scope.gridControls = { setQuery : function() { return { 'id' : { '!=' : null } }; }, }
The query is in a specific JSON syntax that represents an SQL query. This tutorial for a related (but slightly different) JSON syntax should get you started.
Edit the tt2 template
Here, we'll add an eg-grid directive with some additional configuration options:
<eg-grid idl-class="bpt" auto-fields="true" grid-controls="gridControls" persist-key="cat.math.add"> </eg-grid>
In this example, the idl-class
and auto-fields
options work together to automatically fetch the columns from biblio.peer_type
and identify which column is an ID. bpt
is an abbreviation for biblio.peer_type
; such abbreviations can be found in the IDL. Bill Erickson has documented several ways to access the IDL to find such information.
auto-fields
doesn't work for every single table in the database. If the IDL doesn't have a permacrud section defining the appropriate retrieve permissions for the table, for example, the column names won't automatically generate. The retrieve permissions defined in the permacrud section of the IDL also affect which users are able to see data in the egGrid. For example, if you change bpt
to cbho
, users with the ADMIN_HOLD_CAPTURE_SORT
permission will see a grid fully populated with the contents of config.best_hold_order
, but users without that permission will only see the column names and an empty grid.
The grid-controls
option simply refers back to the gridControls
hash we established in our controller. Finally, the persist-key
allows users to change various settings on the grid and save those preferences.
Populate a dropdown menu using Permacrud
Edit the controller
This example uses a simple `retrieveAll` call. If you want to be more selective about the records you pull in, you can put filters, sorting, fleshing, etc. inside the second argument. In this example, we will retrieve all the contents of the `config.best_hold_order` table. Add the following to your controller:
egCore.pcrud.retrieveAll('cbho', {}, {atomic : true}).then( function(list) { $scope.best_hold_order_list = list; });
Edit the tt2 template
Add the following to your template:
<select ng-model="selected_hold_order" ng-options="hold_order.id() as hold_order.name() for hold_order in best_hold_order_list"></select>