1 Browser-Based Staff Client Development Log
2 ==========================================
4 2013-11-20 Templates and Apache
5 -------------------------------
7 When a path is requested from the server, e.g.
8 /eg/staff/circ/patron/search, there are 2 different aspects of the file
9 that are of intest to us: the content of the HTML file and the path used
15 For Angular apps, the HTML page only needs to provide enough information
16 for Angular to load the correct application -- a seed document. A seed
17 document might look something like this:
20 -----------------------------------------------------------------------------
22 <html lang="en_us" ng-app="egPatronApp" ng-controller="PatronCtrl">
24 <title>Evergreen Staff Patron</title>
25 <base href="/eg/staff/">
26 <meta charset="utf-8">
34 -----------------------------------------------------------------------------
36 Note the body is a single div with an 'ng-view' tag.
38 Building Pages from Seeds
39 ~~~~~~~~~~~~~~~~~~~~~~~~~
41 With the above document we know the Angular application (egPatronApp),
42 we have an Angular Controller (PatronCtrl -- not strictly required
43 here). If we assume this page was fetched using the path
44 '/eg/staff/circ/patron/search', then we have all we need to build the
47 The Angular App will contain a series of app-specific routes based on
48 the URL path. Here, our (relative) path will be '/circ/patron/search',
49 since the base path of the application is '/eg/staff/'. For our route
50 configuration we have a chunk of code like this:
53 -----------------------------------------------------------------------------
54 $routeProvider.when('/circ/patron/search', {
55 templateUrl: './circ/patron/t_search',
56 controller: 'PatronSearchCtrl',
57 resolve : resolver // more on resolvers later...
59 -----------------------------------------------------------------------------
61 When the browser lands on '/eg/staff/circ/patron/search', Angular will
62 locate the template file at './circ/patron/t_search' (the app-relative
63 path to a Template Toolkit template), by performing an HTTP request and,
64 once fetched, will drive the display of the Angular template within with
65 the JS controller called PatronSearchCtrl and insert the content of the
66 template into the body of the page at the location of the <div ng-view>
70 'Note': For speed, it's sometimes better to include Angular templates
71 directly in the delivered document so that one less HTTP request is
72 needed. More on that later.
75 Fetching the Same Page at Different URLs
76 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
78 The egPatronApp might support a variety of different route-specific
79 interfaces that are all driven by the same seed document. This means
80 we have to tell Apache to always deliver the same file when we access
81 files within a given range of URL paths. The secret to this is in
82 Apache Rewrite configuration.
87 -----------------------------------------------------------------------------
88 <Location /eg/staff/circ/patron/>
91 RewriteCond %{PATH_INFO} !/staff/circ/patron/index
92 RewriteCond %{PATH_INFO} !/staff/circ/patron/t_*
93 RewriteRule .* /eg/staff/index [L,DPI]
95 -----------------------------------------------------------------------------
97 In short, any URL path that does not map to the index file or to a file
98 whose name starts with "t_" (more on 't_' below) will result in Apache
99 rewriting the request to deliver the index file (i.e. our seed
102 So, in our example, a request for '/eg/staff/circ/patron/search', will return
103 the index file found at '/eg/staff/circ/patron/index', which maps on the
104 server side to the Template Toolkit file at
105 '/path/to/templates/staff/circ/patron/index.tt2'.
107 Two complications arise from this approach. Help appreciated!
108 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
110 Simpler rewrite rules exist in the wild...
111 ++++++++++++++++++++++++++++++++++++++++++
113 But, they do not take into account that we are fetching Template
114 Toolkit-generated files instead of vanilla HTML. The rules I found
115 online take the form of "if it's not a real file, return the index", but
116 none of the files we fetch are real files, since they are internally
117 mapped to Template Toolkit files. This is why I'm using the 't_'
118 prefix. It makes the mapping trivial. I'm all ears for better solution.
120 Configuration Explosion
121 +++++++++++++++++++++++
123 This I see as a real problem, but one that certainly has a solution.
124 The configuration chunk above is such that we need a new chunk for each
125 top-level Angular app. This will quickly get out of hand. A single,
126 dynamic configuration that can map elemenents of arbitrarily-nested
127 paths (or possibly a small set with predefined path depths) to the
128 correct index file would be ideal.
130 UPDATE: 2013-12-12 Apache Config Repaired
131 +++++++++++++++++++++++++++++++++++++++++
133 I finally got around to fixing the Apache configuration. It now supports
134 arbitrary nesting via EGWeb.pm extension.
137 -----------------------------------------------------------------------------
138 <LocationMatch /eg/staff/>
140 PerlSetVar OILSWebStopAtIndex "true"
143 -----------------------------------------------------------------------------
146 2013-11-21 Angular $scope inheritance
147 -------------------------------------
149 Consider the following document:
152 -----------------------------------------------------------------------------
153 <div ng-controller="TopCtrl">
154 <div>Top: {{attr1}}</div>
155 <div ng-controller="SubCtrl">
156 <div>Sub: {{attr1}}</div>
159 -----------------------------------------------------------------------------
161 And the following code:
164 -----------------------------------------------------------------------------
165 .controller('TopCtrl', function($scope) { $scope.attr1 = 'top-attr' })
166 .controller('SubCtrl', function($scope) { })
167 -----------------------------------------------------------------------------
172 -----------------------------------------------------------------------------
175 -----------------------------------------------------------------------------
177 Now, if we apply a value in the child:
180 -----------------------------------------------------------------------------
181 .controller('SubCtrl', function($scope) { $scope.attr1 = 'sub-attr' })
182 -----------------------------------------------------------------------------
184 -----------------------------------------------------------------------------
187 -----------------------------------------------------------------------------
189 Setting a value in the child does not change the value in the parent.
190 Scopes are inherited prototypically, which means attributes from a
191 parent scope are copied into the child scope and the child's version of
192 the attribute masks that of the parent.
194 For both scopes to share a single value, either the parent needs to
195 provide a setter function on the value:
198 -----------------------------------------------------------------------------
199 .controller('TopCtrl', function($scope) {
200 $scope.attr1 = 'top-attr';
201 $scope.setAttr1 = function(val) {
205 .controller('SubCtrl', function($scope) {
206 $scope.setAttr1('sub-attr');
208 -----------------------------------------------------------------------------
213 -----------------------------------------------------------------------------
216 -----------------------------------------------------------------------------
218 Or the value in question needs to be stored within a structure.
221 -----------------------------------------------------------------------------
222 <div ng-controller="TopCtrl">
223 <div>Top: {{attrs.attr1}}</div>
224 <div ng-controller="SubCtrl">
225 <div>Sub: {{attrs.attr1}}</div>
228 -----------------------------------------------------------------------------
231 -----------------------------------------------------------------------------
232 .controller('TopCtrl', function($scope) { $scope.attrs = {attr1 : 'top-attr'} })
233 .controller('SubCtrl', function($scope) { $scope.attrs.attr1 = 'sub-attr' })
234 -----------------------------------------------------------------------------
239 -----------------------------------------------------------------------------
242 -----------------------------------------------------------------------------
244 Since the child scope is not clobbering the 'attrs' attribute, both
245 scopes share the value, which is a reference to a single object.
247 This last is approach is the best for providing two-way binding across
248 both scopes. For example:
251 -----------------------------------------------------------------------------
252 <div ng-controller="TopCtrl">
253 <div>Top: <input ng-model="attrs.attr1" type="text"/></div>
254 <div ng-controller="SubCtrl">
255 <div>Sub: <input ng-model="attrs.attr1" type="text"/></div>
258 -----------------------------------------------------------------------------
260 With this, typing a value into the first input, will set the value
264 https://github.com/angular/angular.js/wiki/Understanding-Scopes[Understanding-Scopes].
266 2013-11-25 Prototype Install and Hacking
267 ----------------------------------------
272 The code lives at http://git.evergreen-ils.org/?p=working/Evergreen.git;a=shortlog;h=refs/heads/collab/berick/web-staff-proto[working => collab/berick/web-staff-proto]
275 The branch is a child of Evergreen master and will be kept up to date
276 with master as development progresses.
279 Install the code in the usual Evergreen manner -- No DB upgrades are
280 required to date, but there are some IDL changes -- and apply the Apache
281 configuration changes found in eg_vhost.conf:
284 -----------------------------------------------------------------------------
285 # see notes above on how we can condense the configuration...
286 # TODO: some of this is clearly redundant and needs to be rearranged
287 <Location /eg/staff/>
290 RewriteCond %{PATH_INFO} !/staff/index
291 RewriteCond %{PATH_INFO} !/staff/t_*
292 RewriteRule .* /eg/staff/index [L,DPI]
293 <IfModule mod_headers.c>
294 Header append Cache-Control "public"
296 <IfModule mod_deflate.c>
297 SetOutputFilter DEFLATE
298 BrowserMatch ^Mozilla/4 gzip-only-text/html
299 BrowserMatch ^Mozilla/4\.0[678] no-gzip
300 BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
301 SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
302 <IfModule mod_headers.c>
303 Header append Vary User-Agent env=!dont-vary
307 <Location /eg/staff/cat/bucket/record/>
310 RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/index
311 RewriteCond %{PATH_INFO} !/staff/cat/bucket/record/t_*
312 RewriteRule .* /eg/staff/cat/bucket/record/index [L,DPI]
314 <Location /eg/staff/circ/patron/>
317 RewriteCond %{PATH_INFO} !/staff/circ/patron/index
318 RewriteCond %{PATH_INFO} !/staff/circ/patron/t_*
319 RewriteRule .* /eg/staff/circ/patron/index [L,DPI]
322 <IfModule mod_headers.c>
323 Header append Cache-Control "public"
325 <IfModule mod_deflate.c>
326 SetOutputFilter DEFLATE
327 BrowserMatch ^Mozilla/4 gzip-only-text/html
328 BrowserMatch ^Mozilla/4\.0[678] no-gzip
329 BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
330 SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
331 <IfModule mod_headers.c>
332 Header append Vary User-Agent env=!dont-vary
336 -----------------------------------------------------------------------------
338 Once installed, point Chrome or Firefox at https://hostname/eg/staff/ to see
344 Most of the code should seem familiar. The Template Toolkit templates
345 behave as usual. For modifying scripts and Angular-loaded templates, I
346 highly recommend Chrome's "Disable Cache (while dev tools is open)" option
347 found under the Javascript console (dev tools) configuration menu (gear
348 icon, bottom right) since Chrome aggressively caches the templates.
350 2013-11-26 Getting Started with Testing
351 ---------------------------------------
353 Today I wrote one unit test for a stub controller from
354 https://github.com/angular/angular-seed[angular-seed].
355 Here's what I did. I'm still figuring this out, so bear with me...
357 * On my desktop, I installed node.js and npm (node package manager) and
358 the node plugin 'karma'.
361 -----------------------------------------------------------------------------
362 % sudo apt-get install nodejs npm
363 # node.js is installed at 'nodejs' -- npm, etc. assume 'node'
364 % sudo ln -s /usr/bin/nodejs /usr/bin/node
365 % sudo npm install -g karma
366 -----------------------------------------------------------------------------
368 * Clone the angular-seed repository, which provides a stub test environment
371 -----------------------------------------------------------------------------
372 % git clone https://github.com/angular/angular-seed
373 -----------------------------------------------------------------------------
375 * Modify the angular-seed test script, which makes some odd assumptions
376 about the location of binaries
379 -----------------------------------------------------------------------------
380 diff --git a/scripts/test.sh b/scripts/test.sh
381 index 972001f..f6db762 100755
382 --- a/scripts/test.sh
383 +++ b/scripts/test.sh
384 @@ -6,4 +6,5 @@ echo ""
385 echo "Starting Karma Server (http://karma-runner.github.io)"
386 echo "-------------------------------------------------------------------"
388 -$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $*
389 +#$BASE_DIR/../node_modules/karma/bin/karma start $BASE_DIR/../config/karma.conf.js $*
390 +karma start $BASE_DIR/../config/karma.conf.js $*
391 -----------------------------------------------------------------------------
393 * Modify the stock controller and controller unit test to do something
397 -----------------------------------------------------------------------------
398 diff --git a/app/js/controllers.js b/app/js/controllers.js
399 index cc9d305..679570d 100644
400 --- a/app/js/controllers.js
401 +++ b/app/js/controllers.js
405 angular.module('myApp.controllers', []).
406 - controller('MyCtrl1', [function() {
407 + controller('MyCtrl1', ['$scope', function($scope) {
408 + $scope.foo = 'hello';
411 diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js
412 index 23f6b09..ec741a6 100644
413 --- a/test/unit/controllersSpec.js
414 +++ b/test/unit/controllersSpec.js
416 /* jasmine specs for controllers go here */
418 describe('controllers', function(){
419 beforeEach(module('myApp.controllers'));
421 + // create the scope, instantiate the controller
423 + beforeEach(inject(function ($rootScope, $controller) {
424 + scope = $rootScope.$new();
425 + ctrl = $controller('MyCtrl1', {$scope: scope});
428 it('should ....', inject(function() {
430 + expect(scope.foo).toEqual('hello');
432 -----------------------------------------------------------------------------
434 * Launched the test, which fires up Chrome and logs the output to the
438 -----------------------------------------------------------------------------
439 CHROME_BIN=/usr/bin/chromium-browser scripts/test.sh
441 Starting Karma Server (http://karma-runner.github.io)
442 INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/
443 INFO [launcher]: Starting browser Chrome
444 INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket 344641UsyCbWCVxk_28H
445 Chromium 30.0.1599 (Ubuntu): Executed 5 of 5 SUCCESS (0.612 secs / 0.088 secs)
446 -----------------------------------------------------------------------------
448 All 5 tests succeeded, even my rigorous $scope.foo='hello' test.
450 Next steps for testing will be to create an environment around the staff
451 client code so we can write some real tests...
453 2013-12-02 Scopes, Services, and Routing
454 ----------------------------------------
456 If you have two routes like so,
459 -----------------------------------------------------------------------------
460 $routeProvider.when('/circ/patron/1/items_out', {...});
461 $routeProvider.when('/circ/patron/1/holds', {...});
462 -----------------------------------------------------------------------------
464 You can access these two pages by changing your browser's URL or you can
465 use links/actions in the page to jump between routes using pushstate
468 Pushsate routing allows you to change the browser URL and have the JS
469 code respond to the change without actually reloading the web page. In
470 short, it makes page switching faster. It does this in two main ways.
472 1. The browser does not have to fetch the page and scripts from the
473 server (or cache) or re-parse and execute scripts that run at page load
476 2. If two pages share data, which is common for route-linked pages, then
477 it's possible for the second page to access data retrieved by the first,
478 without having to re-fetch it from the server.
480 How do we manage routing and caching with angular scopes and services?
482 Any time a path is loaded in Angular, regardless of how the path was
483 accessed (routing or initial page load), all controllers within the page
484 are re-initialized with an empty $scope. Naturally, this means that any
485 data that was loaded into a $scope will no longer be available when the
486 next route is accessed.
488 To persist data across routes, it has to be managed outside of the
489 controller. The Angular solution is to use a service. Generally
490 speaking, Angular services provide re-usable chunks of logic that are
491 scope and controller-agnostic. They are factory classes which produce
492 singleton objects, instantiated once at page load, and persisted through
493 all routes. Because of this, they double as a great place to store
494 route-global data (i.e. data that should be cached between all of our
497 Some examples, with lots of extra stuff left out:
500 -----------------------------------------------------------------------------
501 .controller('PatronItemsOut', function($scope) {
502 fetchPatronFromServer(id).then(
503 function(p) { $scope.patron = p });
506 .controller('PatronHolds', function($scope) {
507 fetchPatronFromServer(id).then(
508 function(p) { $scope.patron = p });
510 -----------------------------------------------------------------------------
512 Here we fetch the patron object from the server every time the
513 PatronItemsOut or PatronHolds controllers are instantiated. Since
514 these two controllers are managed within the same angular app, they are
515 accessible via routing and can theoretically share date. Fetching the
516 user every time is wasteful.
518 Here's an example where we mange user retrieval with a service.
521 -----------------------------------------------------------------------------
522 .factory('patronService', function() {
524 // only cache the last-accessed user.
525 // caching all accessed users is a recipe for memory leaks
528 service.fetchPatron = function(id) {
529 // on initial page load, service.current will be null
530 if (service.current && service.current.id() == id) {
533 // fetch patron and set service.current = patron
539 .controller('PatronItemsOut', function($scope, patronService) {
540 patronService.fetchPatron(id)
541 $scope.patron = function() { return patronService.current }
544 .controller('PatronHolds', function($scope, patronService) {
545 patronService.fetchPatron(id)
546 $scope.patron = function() { return patronService.current }
548 -----------------------------------------------------------------------------
550 Now the patron object lives within our patronService Angular service. It
551 will persist through page routes until we replace the value or reload
552 the page. Note that we are now accessing our patron at $scope.patron()
553 instead of $scope.patron, because $scope.patron() is guaranteed to always
554 return the correct value for the current patron. Angular templates are
555 perfectly happy to work with functions intead of attributes.
558 -----------------------------------------------------------------------------
559 <div>Username: {{patron().usrname()}}</div>
560 -----------------------------------------------------------------------------
562 2013-12-03 What About Workstations? Printing?
563 ----------------------------------------------
565 Why is the workstation optional in the prototype login page?
566 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
568 I mentioned this briefly toward the bottom of my old "Hey, let's build a
570 http://yeti.esilibrary.com/dev/pub/EvergreenWebStaffClientPrototype.pdf[proposal].
571 I've heard the question again from a number of people, so I'll answer it
572 here in a little more detail.
574 Short answer first, yes, the plan is to continue requiring workstations
575 for the browser-based client. The fact that they are not required in
576 the prototype is the result of an unresolved technical hurdle.
578 What's the problem, exactly?
579 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
581 One of the main benefits of XULRunner is that it runs on your computer
582 as a trusted application. XULRunner can open files on your computer,
583 create raw network sockets, and it can talk directly to your printers.
584 Web browsers, being tasked with executing all kinds of untrusted code,
585 have a whole host of restrictions that prevent that level of access.
586 This is a good thing for security, but it makes accomplishing some tasks
587 understandably much more difficult. This lack of access in the browser
588 may be the biggest challenge we face in developing a browser-based
591 There are two classes of problems that we need to resolve: access to
592 files and access to printers.
594 File access is important for things like storing the workstation
595 information in a way that is not easily deleted on accident and can be
596 shared between more than one browser. We need the same level of control
597 for offline transaction data as well. There are probably more...
599 Modern browsers will let you store quite a bit of data (~5MB for
600 localStorage), and in some browsers you can manually raise the storage
601 limit, but they won't let you control where that data lives or allow
602 other applications to access the data. They also make it fairly easy to
603 delete the data en masse within the various debug consoles, which is
604 obviously bad for files we want to stick around indefinitely.
606 For printing, all you can do from the browser is say "print, please"
607 and then the user is shown a print dialog. Different browsers have
608 different settings for controlling whether the dialog appears, etc. but
609 they are non-standard and still not as flexible as we need. What we
610 need is a way for the web application to send print data to different
611 printers based on the context of the action, without requiring the user
612 to manually select the printer each time.
617 There are probably multiple solutions. The most promising so far, which
618 was discussed at the http://goo.gl/oKo5yt[Evergreen Hackaway] is that of
619 a small service which runs on the client machine independent of the browser
620 and performs privileged tasks that the browser cannot. It would likely
621 operate as a small, local web service, accepting connections from clients
622 (i.e. browsers) on the local machine. Those connections may take the form
623 of Websocket connections or XMLHttpRequest via
624 http://en.wikipedia.org/wiki/Cross-origin_resource_sharing[cross-origin resource sharing]
625 (though, I'm not sure the latter approach would actually work).
627 With this approach, we get to resolve all of the browser limitations
628 with one, cross-platform, browser-agnostic add-on. Depending on how
629 it's implemented, we may also get to leverage printing APIs which are
630 more powerful than those found in XULRunner.
632 This probably goes without saying, but such an add-on, whatever form
633 it takes, will *not* be a *requirement* for using the staff client.
634 One aspect of a browser-based client that I find most appealing is the
635 ease of access from different devices and locations. If I were using a
636 tablet, where the add-on is not (or conceivably cannot be) installed,
637 I would still expect to be able to log in, possibly with a workstation
638 (via bookmark/URL or typed in), possibly without, and perform, say, a
639 patron lookup. If I don't need the extended features, I should not be
640 required to have them installed.
642 Finally, it needs a better name than "add-on" (Add-On 2.0!) and it needs
643 developers/sponsors, and all that fun stuff.
645 2013-12-04 Promises in AngularJS
646 --------------------------------
648 One of my favorite new-ish tools in the Javascript world are called
652 If a function cannot return a value or throw an exception without
653 blocking, it can return a promise instead. A promise is an object that
654 represents the return value or the thrown exception that the function
655 may eventually provide. A promise can also be used as a proxy for a
656 remote object to overcome latency. -- https://github.com/kriskowal/q
659 Typically, with asynchronous network calls, the caller will make the
660 call and pass in a callback function, which is invoked when the async
661 call returns. This works well enough for single calls, but it does not
662 scale well. Promises allow us to manage collections of asynchronous
663 calls much more elegantly.
665 Here's a quick example:
668 -----------------------------------------------------------------------------
669 // non-promise api call
670 request(service, method, params, {oncomplete : callback});
672 // promise-based call
673 request(service, method, params).then(callback);
674 -----------------------------------------------------------------------------
676 At first, the difference seems trivial, but it becomes more
677 pronounced as more requests are added.
679 Batch Parallel Request Example
680 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
683 -----------------------------------------------------------------------------
685 // send three, parallel requests and track the
686 // responses to see when all are done
688 var checkComplete = function() {
689 if (--expected == 0) { // decrement w/ each response
690 // all responses received, move on to other things
693 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
694 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
695 request(s, m, p, function(r) { /*do stuff*/; checkComplete() });
697 // promise-based batch
699 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
700 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
701 promises.push(request(s, m, p).then(function(r) {/*do stuff*/}));
702 $q.all(promises).then(
704 // all responses received, move on to other things
708 // $q.all() takes a collection of promise objects and returns a new
709 // promise which is resolved when all of the embedded promises are resolved
710 -----------------------------------------------------------------------------
712 Not only is the code a little cleaner, but it flows in the same
713 direction as execution. In other words, checkComplete(), which is the
714 last-executed code in the non-promise example is defined before the
715 requests are sent. This creates a spaghetti effect in the code, where
716 you have to jump around to follow the logic. Not so in the promise
717 example. It flows top to bottom.
719 Attaching to Promises
720 ~~~~~~~~~~~~~~~~~~~~~
722 Another neat trick you can do w/ promises is create a promise to
723 represent a long-running task. When clients ask for the result of the
724 task and the task is not yet complete, the promise can be returned to
725 any number of clients.
728 -----------------------------------------------------------------------------
729 function longTask() {
733 this.promise = performAsyncTask();
738 // the first call kicks off the long-running process
739 longTask().then(/*func*/);
741 // subsequent calls from other locations simply pick up the existing promise
743 // different location in the code
744 longTask().then(/*func*/);
746 // different location in the code
747 longTask().then(/*func*/);
749 // different location in the code
750 longTask().then(/*func*/);
752 // when longTask() finally completes, all promise handlers are run
753 // Also, if longTask() was *already* complete, the promise handler
754 // will be immediately run.
755 -----------------------------------------------------------------------------
757 This structure is used to great effect in the prototype page
758 initialization code. Independent controllers that all want to ensure
759 the client has authenticated and retrieved necessary data will call the
760 startup code, the first will kick it off, and the others will latch on
761 the outstanding promise.
763 As a final bonus to using promises within Angular, promise resolution
764 causes another $digest() run in Angular, which causes templates to get
768 -----------------------------------------------------------------------------
771 'open-ils.actor.user.retrieve',
772 egAuth.token(), userId
775 $scope.patron = patron;
778 -----------------------------------------------------------------------------
780 -----------------------------------------------------------------------------
781 <!-- username will magically display in the
782 page after the async call completes -->
783 <div>Username: {{patron.usrname()}}</div>
784 -----------------------------------------------------------------------------
786 Promises also support reject() and notify() handlers for failed requests
787 and intermediate messages. In my egNet module, I'm leveraging notify()
788 for streaming responses.
791 -----------------------------------------------------------------------------
792 egNet.request(service, method, params).then(
793 function(finalResp) { console.log('all done') },
794 function() { console.error('request failed!') },
795 function(resp) { console.log('got a response ' + resp) }
797 // The egNet module has additional examples and docs.
798 -----------------------------------------------------------------------------
800 For the full technical rundown, see also
801 http://docs.angularjs.org/api/ng.$q[Angular $q Docs].
803 2013-12-06 Template Cornucopia and Dynamic Strings Experiment
804 -------------------------------------------------------------
806 Using Angular on top of Template Toolkit gives us lots of options for
807 managing templates. TT lets us INCLUDE (etc.) shared templates on the
808 server. AngularJS lets us ng-include (etc.) both inline templates
809 (i.e. delivered within the main document) and lazy-loaded templates,
810 fetched as needed from the server.
812 When to use each approach comes down to how each template is used in the
813 application. Is it needed on page load? Is it needed on every page?
814 Is it needed often, but not on page load? Is it needed rarely? You
815 have to weigh the cost of adding the template contents into the main
816 page body (does it add a lot of bytes?) to the cost of (potentially)
817 having to fetch it over the network as a separate document.
824 In the prototype, I'm loading the navigation template (t_navbar.tt2)
825 within the base document, i.e. the document used as the base template
826 for all top-level index files.
829 -----------------------------------------------------------------------------
830 [% INCLUDE "staff/t_navbar.tt2" %]
831 -----------------------------------------------------------------------------
833 I'm doing this because the navbar is used on every page. It makes no
834 sense to load the template as a separate file from the server, because
835 that just increases the network overhead.
837 Inline Angular Template
838 ~~~~~~~~~~~~~~~~~~~~~~~
840 Angular templates, regardless of origin, must be represented as
841 addressable chunks. To give an inline chunk of HTML an "address", it
842 can be inserted into a <script> block like so:
845 -----------------------------------------------------------------------------
846 <script type="text/ng-template" id="uncat_alert_dialog">
847 <div class="modal-dialog">
848 <div class="modal-content">
849 <!-- ... content ... -->
853 -----------------------------------------------------------------------------
855 The address is simply the ID, so in this example, the modal dialog is
859 -----------------------------------------------------------------------------
860 $modal.open({templateUrl : 'uncat_alert_dialog', controller : [...]});
861 -----------------------------------------------------------------------------
864 $modal is an angular-ui-bootstrap service. It's not part of core AngularJS.
867 Lazy-Loaded Angular Template
868 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
870 These behave much like inline templates, except the template file is
871 fetched from the server as needed.
873 To change the inline template example above into a lazy-loaded template,
874 drop the <script> tags and move the template to it's own web-accessible
878 -----------------------------------------------------------------------------
879 <!-- New file at circ/checkin/t_uncat_alert_dialog.tt2 -->
881 <div class="modal-dialog">
882 <div class="modal-content">
883 <!-- ... content ... -->
886 -----------------------------------------------------------------------------
888 Then load the file using the URL as the ID.
890 -----------------------------------------------------------------------------
892 templateUrl : './circ/checkin/t_uncat_alert_dialog',
895 -----------------------------------------------------------------------------
897 The template file is not fetched from the server until the first time
898 it's used. After that, it's cached by the browser.
900 Angular Dialog Services w/ Dynamic Strings
901 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
903 In the examples above, each type of template is context-specific. In other
904 words, there is a template file specific to uncataloged item checkin.
905 Context-specific templates have their place, but we also need
906 a way to present generic alerts and confirmation dialogs, because
907 a) we may not always be aware of the type of message
908 we want to display ahead of time and b) if we always use context-specific
909 dialogs, the number of templates will grow and grow, even though 95% of
910 each template is boilerplate dialog rendering HTML -- the only thing unique
911 about each will be the message and how the user actions are handled.
913 I just added two new services: egAlertDialog and egConfirmDialog. Presumably,
914 these will be used frequently, so they are good candidates for genericizing.
917 -----------------------------------------------------------------------------
918 // egAlertDialog.open(message_str, dialog_scope)
919 // egConfirmDialog.open(title_str, message_str, dialog_scope)
921 egConfirmDialog.open(
922 'Copy Alert Message for {{copy_barcode}}. Continue checkin?',
923 evt.payload, // copy alert message text
924 { copy_barcode : barcode_str,
925 ok : function() { console.log('user clicked OK') }
926 cancel : function() { console.log('user clicked cancel') }
929 -----------------------------------------------------------------------------
931 Naturally, we don't want hard-coded strings within the code. Here's where
932 an i18n experiment begins....
938 http://yeti.esilibrary.com/dev/pub/techspecs/web-staff-prototype.html[original tech specs],
939 I suggested that we don't need dynamic strings, since all strings will live
940 in templates. While this is certainly possible, it's probably not
941 sustainable, because of the template repetition issues mentioned above.
942 If possible, though, I would like to keep the management of dynamic strings
943 as simple as possible.
945 For my experiment, I'm embedding the dynamic strings directly within the
946 TT template. This lets us manage regular template strings and dynamic
947 strings within the same gettext environment, so there's one less hurdle
948 for managing i18n. Such string collections could be put within the main
949 template, in an included template, or as a separate t_strings.js.tt2
950 file, loaded as a standalone JS file. For now, I'm just loading them
951 directly in the body of the index.tt2 file:
954 -----------------------------------------------------------------------------
956 angular.module('egCoreMod').factory('egCheckinStrings', function() {return {
959 Put our strings into an angular service so that our controller can
960 access them. Store them as a collection of string-key => string-value
961 pairs. Strings may contain angualr-style variables, which will be
962 interpolated by the dialogs.
966 '[% l('Copy "{{copy_barcode}}" was mis-scanned or is not cataloged') %]',
968 COPY_ALERT_MSG_DIALOG_TITLE :
969 '[% l('Copy Alert Message for {{copy_barcode}}. Continue checkin?') %]'
972 -----------------------------------------------------------------------------
974 And here's the confirm dialog code:
977 -----------------------------------------------------------------------------
978 egConfirmDialog.open(
979 egCheckinStrings.COPY_ALERT_MSG_DIALOG_TITLE,
980 evt.payload, // copy alert message text
981 { copy_barcode : args.copy_barcode,
983 // on confirm, redo checkout w/ override
984 performCheckin(args, true)
986 cancel : function() {
987 // on cancel, push the event on the list
988 // to show that it happened
989 checkinSvc.checkins.items.push(evt);
993 -----------------------------------------------------------------------------
995 You may wonder why I created these as services instead of directives.
996 http://www.befundoo.com/blog/angularjs-popup-dialog/[These folks], who
997 developed some similar code, do a pretty good job of explaining why a service
998 is best in this context. (See "Why create the AngularJS Popup Dialog
999 Service?"). More on Angular directives later.
1001 2013-12-13 Unit Tests with AngularJS/Karma/Jasmine
1002 --------------------------------------------------
1004 We now have 4 unit tests in the repository! For now, the files live
1005 under Open-ILS/tests/staffweb/.
1010 These instructions replace my earlier instructions at
1011 link:web-staff-log.html#_2013_11_26_getting_started_with_testing[2013-11-26 Getting Started with Testing].
1014 -----------------------------------------------------------------------------
1016 % sudo apt-get install nodejs npm
1018 # node.js is installed at 'nodejs' -- npm, etc. assume 'node'
1019 % sudo ln -s /usr/bin/nodejs /usr/bin/node
1021 # install karma test engine node plugin
1022 % sudo npm install -g karma
1024 # fetch a copy of angular / angular-ui-bootstrap
1025 % sudo apt-get install curl # if needed
1026 % cd Open-ILS/tests/staffweb/
1027 % ./update-angular.sh 1.2.4 0.7.0
1030 % CHROME_BIN=chromium-browser scripts/test.sh
1031 -----------------------------------------------------------------------------
1036 -----------------------------------------------------------------------------
1037 Starting Karma Server (http://karma-runner.github.io)
1038 \-------------------------------------------------------------------
1039 INFO [karma]: Karma v0.10.5 server started at http://localhost:9876/
1040 INFO [launcher]: Starting browser Chrome
1041 INFO [Chromium 30.0.1599 (Ubuntu)]: Connected on socket XmJAj4CwHlta3NuSQwC9
1042 Chromium 30.0.1599 (Ubuntu): Executed 4 of 4 SUCCESS (0.623 secs / 0.073 secs)
1043 \-----------------------------------------------------------------------------
1044 -----------------------------------------------------------------------------
1050 -----------------------------------------------------------------------------
1051 /** patronSvc tests **/
1052 describe('patronSvcTests', function() {
1054 it('patronSvc should start with empty lists', inject(function(patronSvc) {
1055 expect(patronSvc.patrons.count()).toEqual(0);
1058 it('patronSvc reset should clear data', inject(function(patronSvc) {
1059 patronSvc.checkout_overrides.a = 1;
1060 expect(Object.keys(patronSvc.checkout_overrides).length).toBe(1);
1061 patronSvc.resetPatronLists();
1062 expect(Object.keys(patronSvc.checkout_overrides).length).toBe(0);
1063 expect(patronSvc.holds.items.length).toBe(0);
1066 -----------------------------------------------------------------------------
1068 These tests are very basic. They ensure that the patronSvc service
1069 (defined in circ/patron/app.js) is properly initialized and that the
1070 resetPatronLists() function behaves as expected.
1075 These types of tests can be very useful for testing complex, client-side
1076 code. However, just like with Evergreen Perl/C unit tests, JS unit
1077 tests are not meant to be executed in a live environment. You can test
1078 code that does not require network IO (as above) and you have the
1079 option of creating mock data which is used in place of network-retrieved
1082 I believe the best long-term approach, however, will be full coverage
1083 testing with the live, end-to-end test structure, also supported by Angular.
1084 It requires more setup and I hope to have time
1085 to research it more fully soon. I say this because Evergreen has fairly
1086 complex data requirements (IDL required, data fetched through opensrf
1087 instead of bare XHR) and practically all of the prototype code uses
1088 network IO or assumes the presence of a variety of network-fetched data,
1089 which will be non-trivial to mock up and will only grow over time.
1090 Fingers crossed that it's not a beast to get running. More on this as
1091 the story develops....
1093 2013-12-13 #2 - Brief Update
1094 ----------------------------
1096 * Last week the prototype acquired a locale selector. It uses the
1097 existing locale bits from EGWeb.pm, so it was easy to add.
1099 * The prototype is nearing completion!
1101 * Beware testing on small screens, as the CSS causes the screen to
1102 break and flow (e.g. for mobile devices) a little too aggressively
1105 2013-12-16 JS Minification
1106 --------------------------
1108 Angular has some built-in magic which forces us to take special care when
1109 preparing for Javascript minification. For the details, see the "A Note
1110 On Minification" section of the
1111 http://docs.angularjs.org/tutorial/step_05[Angular Totorial Docs]. Briefly,
1112 there's a short (auto-magic) way and a long way to declare dependencies
1113 for services, controllers, etc. To use minificaiton, you have to use the
1114 long way. To make sure everything is working as expected, I've set up
1117 Testing Minification
1118 ~~~~~~~~~~~~~~~~~~~~
1120 * Download jar file from https://github.com/yui/yuicompressor/releases[YUICompressor Releases]
1121 * Create a staff.min.js bundle from commonly loaded files:
1124 -----------------------------------------------------------------------------
1125 % cd /openils/var/web/js/ui/default/staff
1127 # note the files have to be combined in a specific order
1128 % cat /openils/var/web/js/dojo/opensrf/JSON_v1.js \
1129 /openils/var/web/js/dojo/opensrf/opensrf.js \
1130 /openils/var/web/js/dojo/opensrf/opensrf_xhr.js \
1139 services/startup.js \
1142 | java -jar yuicompressor-2.4.8.jar --type js -o staff.min.js
1143 -----------------------------------------------------------------------------
1145 * modify /openils/var/templates/staff/t_base_js.tt2
1146 ** comment out <script> references to the JS files listed above
1147 ** add a new <script> tag for staff.min.js
1149 -----------------------------------------------------------------------------
1150 <script src="[% ctx.media_prefix %]/js/ui/default/staff/staff.min.js"></script>
1151 -----------------------------------------------------------------------------
1155 I'm happy to report all pages load successfully with (mostly) minimized JS.
1156 Next on the minification front would be baking minification into the
1157 release-building process.
1162 * My (currently) preferred parent scope, child scope, service pattern
1163 * Displaying bib records in the prototype