Example Zing application

To learn how to use Zing let’s build an example application. Our application will allow someone to create an account and create a personal book list.

Example Application

This is not a very complex application. The user can create an account for themselves, login to that account, add books to a list and remove books from that list.

The process for creating this application has the following steps.

  1. Setup a directory/folder structure for your application.
  2. Setup a Node.js environment with a build process (we will use gulp). You can use anything that will compile TypeScript and SCSS.
  3. Create the data type definition, generate the type code and add their data classes.
  4. Setup the server.
  5. Setup the client.
  6. Create skeleton pages for Login and Books
  7. Add ZUI components to Login page
  8. Add SCSS styling to Login page
  9. Add functionality to Login page
  10. Add ZUI components to Books page
  11. Add SCSS styling to Books page
  12. Add functionality to Books page
  13. Add Rights management

Setup directory structure

Obviously you can develop your code any way that you like but it may help when getting started to set it up like that below. Once you have learned Zing you can structure it more to your liking.

client - where the source code for your client goes
|---clientMain.scss - source code for your client styling
|---clientMain.ts - source code root for your client 
|---pages - where your pages go
|   |---LoginPage.ts - where your login code goes
|   |---BooksPage.ts - where your Books page goes
|   |---package.ts - references to all of your pages 
|---views - where your views go
|   |---

model - the folder where Zing will generate its type definitions
        and where you will add your own type declarations.

node-modules - this will be created for you by npm

server - where the source code for your server goes
|---serverMain.ts

war - web archive where the static pages and images will go
|---client.css - css code generated when you compile clientMain.scss
|---client.js - this will be generated when you compile 
                clientMain.ts

gulpfile.js - source code for your gulp build

package.json - the file that manages your npm modules

server.js - server code compiled from serverMain.ts

zing - where you will place the Zing source code
|---data - where all of the data model code is located
|   |---clientRefs.ts - file to reference to include data declarations
|   |                   suitable for your client
|   |---serverRefs.ts - file to reference to include data declarations
|                       suitable for your server
|---zui- contains all ZUI declarations
    |---refs.ts - file to reference to add ZUI components to your
    |             client
    |---styling.scss - file to reference to provide basic styling
                        for your ZUI components.

zpack.json - your definitions for generating data types

Setup Node.js environment

This is not the place to give a tutorial on setting up Node.js. A Google search will produce many video and web tutorials on how to set up Node.js.

Once you have that setup so that you can reach it from to root directory shown in the example directory, you need to use npm to download the various Node modules that we will need to generate Zing code.

package.json

The package.json file controls all of your Node.js. Create this file as a text file and make it look like that below.

 {
  "name": "booksExample",
  "version": "0.1.0",
  "description": "Example Books App",
  "main": "all.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^11.13.6",
    "gulp": "^3.9.1",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-typescript": "^5.0.1",
    "gulp-uglify": "^3.0.2",
    "typescript": "^3.3.3333"
  },
  "dependencies": {
    "body-parser": "^1.18.3",
    "cookie-parser": "^1.4.4",
    "crypto-js": "^3.1.9-1",
    "dotenv": "^8.1.0",
    "express": "^4.16.4",
    "formidable": "^1.2.1",
    "mongodb": "^3.2.3"
  }
} 

The essential pieces of your package.json are highlighted in bold. For a simple start you can just copy what is give above and change your “name” and “description” entries. The most important parts are the “devDependencies” and “dependencies” sections. These define all of the Node modules that you will need to make your Zing app function.

Install required Node modules

Having created and saved your package.json file in the correct location, just move your command line to that same folder and type the command in bold. If you created your package.json correctly this command will fill the node_modules folder with all of the pieces that you will need.

\booksExample>npm install

Create data type definitions

The heart of Zing are the objects types that define the model data structure for your application. There are three steps to building out these data types:

  1. Create the type definitions
  2. Run the Zing data generator to generate the foundation code
  3. Wrap your own type definitions around those generated by Zing

Creating type definitions

We create type definitions in our zpack.json file. Any text editor will do and you can actually name this file anything that you want. Below is an example zpack.json file for our books application.

 1) {   
 2)    "rootClass":"DataObj",
 3)    "rootClassFolder":"",
 4)    "generations":[
 5)        { "prefix":"Z", "relOutFolder":"model"}
 6)    ],
 7)    "classes":{
 8)        "Person":{ "typeCode":"P",
 9)            "keyed":true,
 10)           "fields": {
 11)               "FirstName": {"type_":"string"},
 12)               "LastName": { "type_":"string"},
 13)               "UserName": {"type_":"string"},
 14)               "Password": {"type_":"string"}
 15)           }
 16)       },
 17)       "Book":{"typeCode":"B",
 18)           "keyed":true,
 19)           "fields":{
 20)               "Title":{"type_":"string"},
 21)               "Author": {"type_":"string"},
 22)               "Owner": {"type_":"Person"}
 23)           },
 24)           "find":{
 25)               "byOwner":{ "searchFields":["Owner"]}
 26)           }
 27)       }
 28)   }
 29) } 

Lines 1-6 are pretty standard and you can copy them directly. The root class is always DataObj. The “generations” item (lines 4-6) allows you to do multiple type definition generations. There is rarely a need for this. One line 5 the “prefix”:”Z” will append “Z” to the front of every generated class. This is essential to distinguish the generated classes from your classes. For example, you will have a class Book that has your code in it, and will inherit from the generated class ZBook that has all of the generated code that makes everything work.

Also on line 5 is the “relOutFolder”:”model” specification. This tells Zing where to put the generated classes. This file name is relative to where your zpack.json file is located. Looking back at your directory structure you will see the model directory.

Person class

The “classes” attribute is just a dictionary of all of the classes in your type definition. Lines 8-16, shown below, define your Person class. This is for people who login to your app. On line 8 we define the name of the class “Person” and give it a typeCode of “P”. The typeCode is used to identify this class at run time and when communicating objects across the network. This should be a very short string that is unique among all of your type definitions and is meaningful to you in your code.

 8)        "Person":{ "typeCode":"P",
9) "keyed":true,
10) "fields": {
11) "FirstName": {"type_":"string"},
12) "LastName": { "type_":"string"},
13) "UserName": {"type_":"string"},
14) "Password": {"type_":"string"}
15) }
16) },

Line 9 specified the Person type is keyed (has a key). Keyed objects are independent objects living in the database. You can create them, access them by key and search for them. You can also define types that are not keyed. Such objects are stored within other objects. Types that are not keyed allow for arbitrarily complex objects to be represented in Zing. If an object could be referenced from multiple places or you want to search for such objects, then they should have a key. Otherwise, omitting the key may simplify the use of those objects.

Lines 10-14 define the fields for objects of class Person. Normal camel casing would have these start with a lower case letter. However, Zing appends lower case prefixes such as getFirstName() or setUserName(). For that reason we usually capitalize a field name. Each field is given a type_. The possible types are string, number, boolean and the name of any of the classes that you have declared.

Book class

The Book class is defined in a similar fashion to the Person class. It has a name and a typeCode of “B” and is also keyed. Note that the Owner field on line 22 has a type of Person. The Owner field will not contain a Person object. It will be a string that holds the key for some Person object.

 17)       "Book":{"typeCode":"B",
18) "keyed":true,
19) "fields":{
20) "Title":{"type_":"string"},
21) "Author": {"type_":"string"},
22) "Owner": {"type_":"Person"}
23) },
24) "find":{
25) "byOwner":{ "searchFields":["Owner"]}
26) }
27) }

Note on lines 24-26 we have defined a find function called byOwner. You can name these whatever you want and have as many of them as you want. The byOwner function will search for Book objects using the Owner field. For example you could do

Book.byOwner("P_123237423DEUS9ST",
          (err:string, bookKeys:string[])=>{
                 your code to handle the keys of all the
                 Books whose Owner ==  "P_123237423DEUS9ST" 
           });

We will review the ways to call find functions elsewhere. There are also many places in Zing that use callback functions like that shown above.

Running the Zing generator

Once you have your zpack.json file ready you can run the Zing generator. Assuming that you put the Zing files and the zpack.json file in the locations shown above, you can run the generator from the base directory of your setup.

 \booksExample>node zing/zing.js ./zpack.json 

Adding your own classes to the model directory

After you have run Zing, you will find three new files in the model folder. They are: ZBook.ts, ZPerson.ts and ZMake.ts. Note that the Z’s come from your type definition file where you designated “Z” as the prefix for all of the generated TypeScript files.

If you first open ZMake.ts you will find the following:

1)  /// <reference path="Person.ts"/>
2)  /// <reference path="Book.ts"/>
3)
4)  function ZMake(expectedType:string,json:any):DataObj {
5)     let type:string=json._t_; 

      ..... more code that we can ignore for now .....

Line 4 declares a function ZMake that translates raw JSON data into objects of class Person or Book. This is the special sauce that makes our strongly typed TypeScript objects transmittable across the network. There is no reason for you to look at or use the ZMake function but you should be aware that it is there. As you will see later, your client and server implementations should reference the ZMake.ts file because that is where all the definitions for your application’s model objects are referenced.

Lines 1 and 2 reference the files Person.ts and Book.ts. These do not exist and will cause a compilation error. You need to create these files. These are where all of your custom behavior for these classes will be located.

Creating Book.ts

You should create a new TypeScript file Book.ts in your model folder. This file should look like the following:

1)  /// <reference path="ZBook.ts"/>
2)  
3)  class Book extends ZBook {
4)      static makeNew(title:string, author:string,
5)                done: (err:string,book:Book)=>void){
6)          let user:Person = use RightsManager to find current user
7)          let newBook = new Book( 
8)                      { Title:title, Author:author, Owner: user}
9)                );
10)          newBook.PUT( (err:string,book:Book)=>{
11)             done(err,book);
12)         })
13)     }
14) } 

Line 1 will reference the ZBook.ts file that was generated for you. This is necessary because in line 3 you are going to specify the Book class inherits all of the generated functionality in ZBook. This is how your Book code gets attached to all that code that Zing generated for you. Just declaring the Book class is enough for your code to compile.

It is helpful to also create the static makeNew method at line 4. The constructor for ZBook, which Book inherits uses raw JSON as its source. This is frequently not programmer friendly. The makeNew method takes the title and author of the book as parameters. It also has a callback function called done that we will talk about in a minute.

In addition to the title and author our Book type specification also has an Owner field. Our makeNew method on line 6 will go to the Rights manager and find the current user. We will cover this code later. Lines 7-9 will construct a new Book object (constructor is inherited) from a simple JSON object with values for each of the fields.

Once we have a Book stored in newBook it is not yet in the database, known to the server or in any way useful to us. Line 10 uses the inherited PUT method to tell the DataSource about our new object. PUT is an asynchronous method. It may access a database or go out over an http connection or anything else that the current datasource might do. When it is done it will call the function that we passed as a parameter to PUT. If there is an error the err parameter will have a message. What we really want is the book object. This will probably (but not necessarily) be identical to our newBook object. The most important addition is that it will have been assigned a key which can be accessed vie book._key. This key is how the book will be identified throughout the system.

In the PUT callback in our makeNew method we just package up err and book and call makeNew‘s own callback function called done.

We may have other things to build into the Book class, but this is enough for now. The reason that the makeNew method is not automatically generated is that we may have many ways that we want to build Book objects and we can create many methods for them. In our Book example we don’t want the programmer to specify the Owner. We want the Owner to automatically be the current user. We encoded this into makeNew. We could just have no makeNew at all and used the constructor directly.

Creating Person.ts

We also need to create a Person.ts file for our Person class. It should look as follows:

1)  /// <reference path="ZPerson.ts"/>
2)  
3)  class Person extends ZPerson {
4)      static makeNew(firstName:string, lastName:string,
5)              userName:string, password:string,
6)              done:(err:string,person:Person)=>void){
7)          let newPerson = new Person({
8)              FirstName:firstName,
9)              LastName:lastName,
10)             UserName:userName,
11)             Password:password
12)         })
13)         newPerson.PUT((err:string,person:Person)=>{
14)             done(err,person);
15)         })
16)     }
17) } 

As with Book, we reference ZPerson.ts to access the generated code (Line 1) and declare our Person to extend ZPerson (line4). Our makeNew method has more parameters for the various fields in a Person and has a callback (Lines 4-6). We assemble a JSON description of our new object and use it to create a new Person (lines 7-12). We then use PUT to add that person to the datasource (lines 13-15) and when the adding is complete we send our new object out using the done callback (line 14).

Setup server

Our next step is to create a server. You will need Node.js installed. There are plenty of tutorials on how to install Node.js. You will also need to install MongoDB on your local machine to run this example. Again there are many tutorials on how to set this up.

The key file is the serverMain.ts file in your server folder. It should look as follows.

1)  /// <reference path="../Zing/data/serverRefs.ts"/>
2)  /// <reference path="../model/ZMake.ts"/>
3)  /// <reference path="../BooksEnv.ts"/>
4)  /// <reference path="ServerRightsManager.ts"/>
5)  require("dotenv").config();
6)  let env = new BooksEnv();
7)  let dataSource = new MongoDataSource(
8)      env.mongoCredentials()+env.mongoHost(),
9)      env.mongoPort(),
10)     env.mongoDB()
11) );
12) let rightsManager = new ServerRightsManager(dataSource);
13) dataSource.setRightsManager(rightsManager);
14) 
15) let app = new ZingExpress(dataSource,env);
16) 
17) env.serverStartNotice();
18) app.listen(env.serverPort()); 

Line 1 will access all of the needed code from Zing. This will include all of the classes and methods that you need for the server. Line 2 will ultimately include all of the model. The ZMake class will reference all of your data model classes which in turn will reference all the generated classes. Note that ZMake is generated when you run Zing against your type definitions.

Line 6 creates a new environment object. Lines 7-11 create your dataSource. A DataSource is an object that is used to store and retrieve all of your objects. In this example we are using the MongoDataSource implementation. This will store everything in a MongoDB database. Most of the necessary information is retrieved from the environment object that will be described below.

Lines 12 and 13 set up the relationship between the DataSource and a RightsManager. We will show how to set up a rights manager for your server a little later.

Line 15 launches your server using your datasource and your environment variable. Line 17 puts out a debug notice so you know the environment information that your server is running under. Line 18 starts the server at the environment variable’s port.

Creating the BooksEnv class

Lines 3 and 4 reference classes that you need to write, which we will talk about a little later.

ZingEnv is class defined in Zing that is used for creating environment settings that control your server and client. The purpose of this is to gather together the setting that move your software from development to testing and then release. Reading the ZingEnv class will make most of this clear when you need that. For now we will create a BooksEnv class in the BooksEnv.ts file that looks like the following. This should go in your root folder.

1)  /// <reference path="Zing/data/ZingEnv.ts"/>
2)  
3)  class BooksEnv extends ZingEnv{
4)      indexHTML():string{
5)          return `
6)          <html>
7)              <head>
8)                  <script
9)                      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
10) integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E="
11)                     crossorigin="anonymous">
12)                 </script>
13)                 <link rel="stylesheet" type="text/css" href="/clientMain.css"/>
14)             </head>
15)             <body>
16)                 <div id="content"></div>
17)                 <script src="/clientMain.js"></script>
18)                 <div id="modaloverlay" class="hidden"></div>
19)             </body>
20)         </html>
21)         `
22)     }
23)
24)     pageHTML(root:string,pageName:string):string {
25)         return this.indexHTML();
26)     }
27) } 

The method indexHTML() will return the HTML code for your home page. This will be pretty much as you see it above. You may upgrade the version of JQuery to use (lines 9-12). Line 13 references your generated style sheet that we will talk about later. Line 16 defines a <div> named content. This is critical. This is where ZUI will put its user interface. This is where most of your interactive pages will appear. Line 17 references your generated client code and line 18 provides a place for messages to appear from the Modal class. Moving beyond this simple example you may have other javascript or stylings to import. These call all be placed here. This is just simple HTML.

In some cases you may want the HTML for pages to be distinguished in some way. The pageHTML() method will be called to generate that HTML code for each page. In our simple application we just return the HTML for the home page.

Line 27 defines what port your server will be listening on. Line 28 defines the information to reach the MongoDB server. Line 29 defines the credentials for the MongoDB server. Line 30 defines name of the MongoDB database to be used by this server.

Create the .env file

There are settings that you do not want to put into your code (like credentials) and there are things that need to change between local debugging, development releases and full releases. This information is place in a file called “.env” that should go in the root directory of your server. This file is read by the config method on line 5 of the sample server code. The .env file should look as follows:

1) serverPort=3000
2) mongoHost=127.0.0.1
3) mongoCredentials=username:password@
4) mongoPort=27017
5) mongoDB=TestDB
6) DB=verbose
7) https=keyCertificationPrefix

The serverPort (line 1) is the port that the server will be listening to for HTTP requests. The mongoHost (line 2) is the IP address or the domain name where the MongoDB server that is managing your database is listening. The mongoCredentials (line 3) has the user name and password that will give your application access to the database. The trailing @ sign is required. The mongoPort (line 4) is the port number that your MongoDB server is listening on. By default MongDB listens to port 27017. The mongoDB on line 5 is the name of the database you will be using. Note that your username:password must apply specifically to the named database.

Line 6 controls debugging output from your server. If your .env file contains DB=quiet, then all of the debugging information generated by the DB class defined in Zing’s DB.ts file will be suppressed. Any thing else including omitting the DB flag will allows the debug messages to be generated.

Lint 7 specifies the certification key information for being able to serve using HTTPS. When debugging, having HTTPS is not important but when using your application in practice you definitely don’t want user information flowing across the internet in the clear.

When you get your certificate from a service you should end up with several files with names derived from your domain name. If your domain name is app.mysite.net, then you will have several files such as app_mysite_net.key or app_mysite_net.pem. Notice that the dots in your domain name have been replaced with underscores. Copy all of these files into the folder where your server will run and then set up a line in your .env file that has https=app_mysite_net. This will tell the server where to find your certification information.

You can put other information in your .env file. The information is found on process.env. For example the mongoHost is at process.env.mongoHost.

Build and run the server

All that remains is to use your favorite build tools to run the TypeScript compiler on the server/serverMain.ts file to generate the server.js file.

Once the server is compiled, it can be run under Node.js as follows:

\booksExample> node server.js

Setup client

The client is more complicated that the server. The server primarily accepts instructions to manipulate the various types of objects describe in your type specification. With the generated type information the server simply does its job of serving and storing objects.

Because the client provides the user interface and most of the logic for serving the user, it is more complicated than the server. The Zing generator benefits the client as well as the server in that it ensures consistency between data objects on the client and those stored on the server. This eliminates a lot of debugging grief for the developer.

The actual clientMain.ts file is relatively straightforward. This is because all of the work is done in either the type classes such as Person or Book or in the ZUI pages. ZUI stands for Zing UI and provides the mechanisms for setting up the user interface for your client.

For our example application, the client/clientMain.ts file is as follows.

1)  /// <reference path="../Zing/data/clientRefs.ts"/>
2)  /// <reference path="../model/ZMake.ts"/>
3)  /// <reference path="ClientRightsManager.ts"/>
4)  /// <reference path="pages/package.ts"/>
5)
6)  let httpSource = new HTTPDataSource(window.location.origin+"/");
7)  let source = new CacheDataSource(httpSource);
8)
9)  DataObj.globalSource=source;
10) let rm:RightsManager = new ClientRightsManager(source);
11) source.setRightsManager(rm);
12) ZUI.pageManager = new PageManager(source,
13)         new LoginPage(null),"#content"); 

Line 1 provides access to the Zing declarations that are needed for a client. In particular, this includes the ZUI components and the DataSource components. Line 2 references the model classes generated when you ran Zing on your type declarations. Line 3 references the RightsManager that will be created for the client.

Every client built with ZUI consists of one or more Pages and each page has one or more Views. Line 4 references the pages for our application (we will have two).

Line 6 creates a new HTTPDataSource that gets its data from the same URL as the client page. Remember that the HTML for our client page was defined in our BooksEnv class. The server almost always generates the same HTML for all pages. On the server we used the MongoDataSource to manage our data objects. In the client we will use the HTTPDataSource which will transfer all of our DataSource requests into http requests of the server. You will rarely access DataSource requests directly. As will be shown, all you need is to manipulate your class objects and the underlying DataSource access will take care of itself.

Line 7 creates a CacheDataSource from our httpSource. The HTTPSource would access the server for every data operation, which would be slow. The CacheDataSource wraps around any other data source and provides a local cache for data objects. This means that our client code can freely manipulate data objects without incurring unnecessary http traffic. Line 7 provides us with this service.

Line 9 tells DataObj (the superclass of all our data objects) that source contains the DataSource to be used throughout the client. This makes the connection between our generated data types and the DataSource system.

Line 10 creates the client’s RightsManager object and line 11 tells the source which RightsManager to use.

The pages for ZUI are managed by a PageManager object. Lines 12 and 13 create that object and give it to ZUI as its pageManager. We give the PageManager the DataSource that it should use. We also tell the page manager the page that it should use as its home page. In our application this will be our LoginPage. Lastly we tell the page manager on line 14 that it should put all of its user interface pieces inside of the <div> named content. This <div> was created on line 16 when we set up our BooksEnv class.

When we finish fleshing out the other files you need to compile client/clientMain.ts using the TypeScript compiler. This will compile all of the code for your client and should place it in war/client.js.

Skeleton pages for Login and Books

All of our client application is found in two pages: LoginPage and BooksPage. We first create skeletons of these two pages and then flesh them out with views that will give them substance.

The following is a skeleton implementation for LoginPage. This goes in the client/pages/LoginPage.ts file.

1)  /// <reference path="../../Zing/zui/refs.ts"/>
2)  
3)  class LoginPage extends Page{
4)      constructor(pageState:PageState){
5)          super(pageState);
6)          this.content = new DivUI([
7)              new TextUI("Login Page"),
8)              new ButtonUI("to Books")
9)                  .click(()=>{
10)                     PageManager.PUSHTO("books");
11)                 })
12)             ]);
13)     }
14) }
15) PageManager.registerPageFactory("login",(state:PageState)=>{
16)     return new LoginPage(state);
17) })

Line 1 creates a reference to a file in ZUI that references all of the ZUI source code. This makes all of the components available to this page. Line 3 declares and class and makes it a subclass of Page, which is itself a subclass of ZUI.

All pages take a PageState argument for their constructor (line 4). The pageState contains a dictionary of name/string pairs that contain information about the current state of the interaction. This can be anything you want. This information is also placed into the query part of the URL so that refreshing that URL will bring you back to the same page and state of the page. Line 5 propagates the state up to the Page superclass.

Lines 6-12 define a very simple user interface for this page. This will get us started. It simply puts up “Login Page” and an unstyled button that says “to Books”. Line 10 shows this button pushing to the “books” page. Pushing means that the current page will be pushed onto the back button stack. This is a minimal UI that lets us test our progress in building up our client.

Lines 15-17 are important because they register a page factory function with the PageManager. This tells the page manager that whenever the “login” page is requested, execute the function that will create a new LoginPage object using the new state. There is lots of flexibility in how the factory function builds the new “login” page but the pattern shown here is pretty universal.

We create a similar client/pages/BooksPage.ts file for the BooksPage class.

1)  /// <reference path="../../Zing/zui/refs.ts"/>
2)  
3)  class BooksPage extends Page{
4)      constructor(pageState:PageState){
5)          super(pageState);
6)          this.content = new DivUI([
7)              new TextUI("Books Page"),
8)              new ButtonUI("to Login")
9)                  .click(()=>{
10)                     PageManager.PUSHTO("login");
11)                 })
12)             ]);
13)     }
14) }
15) PageManager.registerPageFactory("books",(state:PageState)=>{
16)     return new BooksPage(state);
17) })

As with the LoginPage our BooksPage class is very minimal with a title and a button that goes back to “login”. These two classes allow us to test our client and test minimal navigation among pages. This is a good point to test out our application to make sure that everything can be compiled into the right place and does the right thing.

Login page ZUI

Once we have a skeleton for the Login page we can start adding components to the page to create our user interface. The new code for client/pages/Login.ts is as follows:

1)  /// <reference path="../../Zing/zui/refs.ts"/>
2)
3)  class LoginPage extends Page{
4)      userName:string;
5)      password:string;
6)      firstName:string;
7)      lastName:string;
8)      newUser:string;
9)      newPW:string;
10)
11)     constructor(pageState:PageState){
12)         super(pageState);
13)         this.content = new DivUI([
14)             new Messages(),
15)             new TextUI("Welcome to Booklists"),
16)             new TextFieldUI()
17)                 .getF(()=>{ return this.userName; })
18)                 .setF((newUserName:string)=>{ this.userName=newUserName; })
19)                 .placeHolder("user name"),
20)             new TextFieldUI()
21)                 .getF(()=>{ return this.password; })
23)                 .setF((newPassword:string)=>{ this.password=newPassword; })
24)                 .placeHolder("password"),
25)             new ButtonUI("Login")
26)                 .click(()=>{ this.login() }),
27)             
28)             new TextFieldUI()
29)                 .getF(()=>{ return this.firstName })
30)                 .setF((newFN:string)=>{ this.firstName=newFN })
31)                 .placeHolder("first name"),
32)             new TextFieldUI()
33)                 .getF(()=>{ return this.lastName })
34)                 .setF((newLN:string)=>{ this.lastName=newLN })
35)                 .placeHolder("last name"),
36)             new TextFieldUI()
37)                 .getF(()=>{ return this.newUser })
38)                 .setF((newUser:string)=>{ this.newUser=newUser })
39)                 .placeHolder("user name"),
40)            new TextFieldUI()
41)                 .getF(()=>{ return this.newPW })
42)                 .setF((newPW:string)=>{ this.newPW=newPW })
43)                 .placeHolder("password"),
44)            new ButtonUI("Create Account")
45)                 .click(()=>{ this.createAccount()})
46)         ]);
47)     }
48)     private login(){

...           this code is shown later

66)     }
67)     private createAccount(){

...           this code is shown later

81)     }
82) }
83) PageManager.registerPageFactory("login",(state:PageState)=>{
84)     return new LoginPage(state);
85) })

On lines 4-9 we have added object variables to the page where we can store the values that the user may type into the various text boxes. Line 14 adds a component that will display any messages generated by the user interface. You place this component where you want them to appear. In our case they will appear at the top of the page because we placed the Messages component first. If there are no messages then this component is hidden.

Line 15 is a simple page title.

Lines 16-24 create the type in box for entering the user name for logging in. Line 17 defines the function to be called when the text box needs the user name and line 18 defines the function that will be called whenever the user name changes. These two methods (getF and setF) provide the linkage between the component and our actual implementation. Line 19 provides the text to appear in the box when nothing has been entered.

Lines 20-24 create the text box for entering the password.

Lines 25-26 define the Login button. For now the method this.login() is called when the button is clicked. We will define its action later.

Lines 28-45 provide the text fields and button needed for the creation of new accounts. These work similarly to what was done for logging in.

Login page SCSS Styling

If we run our application at this point the login page will look something like the following.

We want our login page to look like this.

To get this to happen we first need to add style information to our ZUI components that we defined earlier. The new code will appear as follows.

13)         this.content = new DivUI([
14)             new Messages(),
15)             new TextUI("Welcome to Booklists")
                    .style("Login-welcome"),
16)             new TextFieldUI()
17)                 .getF(()=>{ return this.userName; })
18)                 .setF((newUserName:string)=>{ this.userName=newUserName; })
19)                 .placeHolder("user name")
                    .style("Login-textbox-left"),
20)             new TextFieldUI()
21)                 .getF(()=>{ return this.password; })
23)                 .setF((newPassword:string)=>{ this.password=newPassword; })
24)                 .placeHolder("password")
                    .style("Login-textbox-right"),
25)             new ButtonUI("Login")
26)                 .click(()=>{ this.login() })
                    .style("Login-button"),
27)             
28)             new TextFieldUI()
29)                 .getF(()=>{ return this.firstName })
30)                 .setF((newFN:string)=>{ this.firstName=newFN })
31)                 .placeHolder("first name") 
                    .style("Login-textbox-left") ,
32)             new TextFieldUI()
33)                 .getF(()=>{ return this.lastName })
34)                 .setF((newLN:string)=>{ this.lastName=newLN })
35)                 .placeHolder("last name") 
                    .style("Login-textbox-right") ,
36)             new TextFieldUI()
37)                 .getF(()=>{ return this.newUser })
38)                 .setF((newUser:string)=>{ this.newUser=newUser })
39)                 .placeHolder("user name") 
                    .style("Login-textbox-left") ,
40)            new TextFieldUI()
41)                 .getF(()=>{ return this.newPW })
42)                 .setF((newPW:string)=>{ this.newPW=newPW })
43)                 .placeHolder("password") 
                    .style("Login-textbox-right") ,
44)            new ButtonUI("Create Account")
45)                 .click(()=>{ this.createAccount()}) 
                    .style("Login-button") 
46)         ]); 

Each of the additional methods adds a SCSS class name to the corresponding component. We can use these class names in our SCSS file to handle any styling of the user interface that we want.

We now want to create a client/pages/package.scss file to contain our SCSS definitions. In addition, we want to create our client/clientMain.scss file. The clientMain.scss file should look like the following.

1)  @import "../Zing/zui/zuiMain.scss";
2)  @import "pages/package.scss;

Line 1 should reference wherever your Zing code is stored. This contains all of the default styling for all of the ZUI components. This should always be imported first. This also contains the basic layout features for doing 12-column layout with gutters that will simplify our layout process. Line 2 imports the styling for our own pages. Initially pages/package.scss should be as follows.

1)  .Login-welcome{
2)      @include layout(12);
3)      text-align: center;
4)      font-size: 40px;
5)      margin: 50px;
6)  }
7)
8)  .Login-textbox-left {
9)      @include layout(4,both,2);
10)     border: solid 4px blue;
11)     font-size: 20px;
12) }
13) 
14) .Login-textbox-right {
15)     @include layout(4);
16)     border: solid 4px blue;
17)     font-size: 20px;
18) }
19)
20) .Login-button {
21)     @include layout(8,both,2);
22)     border-radius: 6px;
23)     background-color: rgb(2, 170, 2);
24)     border: solid 3px rgb(2, 170, 2);
25)     color: white;
26)     margin-top:10px;
27)     margin-bottom:40px;
28)     font-size:20px;
29)     &:hover {
30)         border: solid 3px darkgreen;
31)     }
32) } 

Most of the pages/package.scss file is standard SCSS and CSS styling instructions that can be found in other documentation. The layout, however, is unique to ZUI. The resulting UI looks like the following, which is close to our original design.

Line 2 indicates that the Login-welcome is to be a full 12 columns wide (the whole page) with gutters on both sides (which does not matter in this case. Line 9 specifies the the text boxes on the left should be 4 columns wide and should be offset from the left edge by two columns. It also specifies that both gutter margins (left and right) should be present. Line 14 specifies that the text boxes on the right should be 4 columns wide. The gutters default to “both” and the offset defaults to 0. Line 21 specifies that the buttons should be 8 columns wide with both gutter margins and and offset from the left of 2 columns.

Login page functionality

The actual functionality of the page is in the two methods login and createAccount.

login()

48)     private login(){
49)         Messages.clear();
50)         if (!this.userName || this.userName.length==0){
51)             Messages.error("missing user name to login");
52)             return;
53)         }
54)         if (!this.password || this.password.length==0){
55)             Messages.error("missing password to login");
56)             return;
57)         }
58)         let ds = DataObj.globalSource;
59)         ds.login(this.userName,this.password, (err:string)=>{
60)             if (err){
61)                 Messages.error(err);
62)             } else {
63)                 PageManager.PUSHTO("books",{})
64)             }
65)         })
66)     } 

Line 49 clears out any messages that are being displayed from previous user actions. This is usually a good thing to do when any user action is detected. It keeps the messaging area clean. Lines 50-53 check to see if there is a user name. If there is not then a message is posted in line 51. Similarly lines 54-57 check for a password. If we had any restrictions on passwords, such as length or special characters, they also could be checked here an reported similarly if there is a problem.

Line 58 will get the global data source. This is our primary mechanism for accessing whatever repository we are using for user data.

Line 59 tells the data source to login a user with the given name and password. The login will involve accessing the server to verify the user and credentials. Because the server is involved the action is asynchronous. Whenever the login finishes the function at the end is called with an error message. Line 60 checks to see if there is a message and if so, posts it (line 62). If there is no message then the login succeeded and we move to the “books” page (line 63).

createAccount()

67)     private createAccount(){
68)         Messages.clear();
69) 
70)         let userDesc = {
71)             FirstName:this.firstName,
72)             LastName:this.lastName,
73)             UserName:this.newUser,
74)         }
75)         DataObj.globalSource.createUser(userDesc,
76)                  this.newPW,(err:string)=>{
77)             if (err)
78)                 Messages.error(err);
79)         })
80)         ZUI.notify();
81)     }
82) } 

Creating a new account first clears the messages from any prior actions. Lines 70-74 create an object that describes the account to be created. This object is used on line 75 in the call to createUser. This information will be forwarded to the UserManager. The default UserManager does nothing with this information. When you create your own UserManager (as will be discussed later) this userDesc will be passed to your method. Farther along in this example we will show you how to create your UserManager and define createUser so that it will store the information as one of your Person objects.

Line 76 passes in the password and a function to be called with the user creation is done. This function receives an error message. If there is a message then line 78 posts it.

After everything is done we tell ZUI to refresh the page using ZUI.notify() on line 80. This should be done whenever any changes are made to the data so that ZUI can refresh the screen. We did not do this in the login method because we moved to a new page and the old one did not need refreshing.

Books page ZUI

Books page SCSS styling

Books page functionality

Add rights management