Chapter 8. Collection+JSON Clients

“The human animal differs from the lesser primates in his passion for lists.”

H. Allen Smith

The last hypermedia format we’ll review in this book is the Collection+JSON (Cj) format. It has similarities with both HAL (see Chapter 4) and Siren (see Chapter 6) but also has a rather unique approach. The Cj format is designed, from the very beginning, as a list-style format—it is meant to return lists of records. As we’ll see when we review the design in the next section, there is more to the format than just that, but lists are what Cj is designed to deal with.

Cj also takes a cue from the classic Create-Read-Update-Delete (CRUD) pattern used by most web API clients today. We covered this style in Chapter 2, and some of the lessons from the client we built in that chapter will apply for the Cj client, too.

In this chapter, we’ll take a quick look at the format design, the Cj representor code, and the Cj general client. Then, as we have for other client implementations, we’ll introduce changes to see how well the API client holds up as the backend API makes backward-compatible changes.

Finally, we’ll be sure to check in on our progress along the path to meeting the OAA Challenge. While HAL excels at handling Addresses and Siren has great support for Actions, Cj was designed to meet the Objects challenge—sharing metadata about the domain objects at runtime. And, as we’ll see in the review, Cj meets the Object challenge with a novel solution—by making them inconsequential to the client–server experience.

The Collection+JSON Format

I designed and published the Collection+JSON format in 2011—the same year Mike Kelly released his HAL specification. Collection+JSON (aka Cj) was designed to make it easy to manage lists of data like blog posts, customer records, products, users, and so on. The description that appears on the Cj specification page says:

[Cj] is similar to the Atom Syndication Format (RFC4287) and the Atom Publishing Protocol (RFC5023). However, Collection+JSON defines both the format and the protocol semantics in a single media type. [Cj] also includes support for Query Templates and expanded write support through the use of a Write Template.

One way to think about Cj is that it is a JSON version of the Atom format with forms added (see Figure 8-1). The good news is that Cj follows Atom’s support for the Create-Read-Update-Delete (CRUD) pattern. That means most developers can understand Cj’s read/write semantics rather easily. The added bonus for Cj is that it has elements for describing HTML-like forms for filtering data (with Cj’s queries element) and for updating content on the server (via the template element). However, as we’ll see in the review that follows, the way the template element is used can be a bit of a challenge.

Tip

You can find a more complete description of the Collection+JSON media type by checking out the online Cj documentation. There is also a Cj discussion list and GitHub organization where additional information is shared. See “References” at the end of this chapter for details.

rwcl 0801
Figure 8-1. The Collection+JSON document model

The basic elements of every Cj message are:

Links

A set of one or more link elements. These are very similar to HAL and Siren link elements.

Items

One or more data items—basically the API domain objects. The properties of HAL and Siren are similar to Cj items.

Queries

These are basically HTML GET forms. Cj queries are like HAL’s templated links and Siren’s action elements (with the method set to GET).

Template

In Cj, all write operations (HTTP POST and PUT) are done using the template element. It contains one or more data objects—each one like HTML input elements. Again, this is similar to Siren action elements. HAL doesn’t have anything that matches the Cj template.

Cj also has an error element for returning error information and a content element for returning free-form text and markup. We’ll not cover these here today. You can read up on them in the Cj documentation mentioned in the reference section (“References”).

Here’s an example of a simple Collection+JSON message that shows the major sections] of a Cj document, including links (1), items (2), queries (3), and the template (4) element:

{
  "collection": {
    "version": "1.0",
    "href": "http://rwcbook12.herokuapp.com", 5
    "title": "TPS - Task Processing System",
    "links": [ 1
      {
        "href": "http://rwcbook12.herokuapp.com/",
        "rel": "collection",
        "prompt": "All task"
      }
    ],
    "items": [ 2
      {
        "rel": "item",
        "href": "http://rwcbook12.herokuapp.com/1sv697h2yij",
        "data": [
          {"name": "id", "value": "1sv697h2yij", "prompt": "id"},
          {"name": "title", "value": "Marina", "prompt": "title"},
          {"name": "completed", "value": "false", "prompt": "completed"}
        ]
      },
      {
        "rel": "item",
        "href": "http://rwcbook12.herokuapp.com/25ogsjhqtk7",
        "data": [
          {"name": "id", "value": "25ogsjhqtk7", "prompt": "id"},
          {"name": "title", "value": "new stuff", "prompt": "title"},
          {"name": "completed", "value": "true", "prompt": "completed"}
        ]
      }
    ],
    "queries": [ 3
      {
        "rel": "search",
        "href": "http://rwcbook12.herokuapp.com/",
        "prompt": "Search tasks",
        "data": [
          {"name": "title", "value": "", "prompt": "Title"}
        ]
      }
    ],
    "template": { 4
      "prompt": "Add task",
      "rel": "create-form",
      "data": [
        {"name": "title", "value": "", "prompt": "Title"},
        {"name": "completed", "value": "false", "prompt": "Complete"}
      ]
    }
  }
}

Another important attribute of a Cj document is the root-level href (see 5). The value of href is used when adding a new record to the items collection. We’ll talk more about this property when we cover the template element (see “Template”).

Links

The links element in a Cj document is always a valid JSON array which contains one or more link objects. Important link properties include href, rel, and prompt. These work similarly to the way HTML <a>…</a> tags do—static URLs for HTTP GET actions:

"links": [
  {
    "href": "http://rwcbook12.herokuapp.com/home/",
    "rel": "home collection",
    "prompt": "Home"
  },
  {
    "href": "http://rwcbook12.herokuapp.com/task/",
    "rel": "self task collection",
    "prompt": "Tasks"
  },
  {
    "href": "http://rwcbook12.herokuapp.com/user/",
    "rel": "user collection",
    "prompt": "Users"
  }
]

In Cj, the links section typically holds links that are relevant for the current document or, in a human-centric UI, the current screen or web page. Along with important navigation links for the app (see in the previous example), the links section may include things like page-level navigation (first, previous, next, last) or other similar links.

Another handy property on Cj link objects is the render property. This tells consuming apps how to treat the link. For example, if the render value is set to "none", client apps are not expected to display the link. This is handy when passing link elements for things like CSS stylesheets, profile URLs, or other types of information:

"links": [
  {
    "href": "http://api.example.org/profiles/task-management",
    "rel": "profile",
    "render" : "none"
  }
]

Items

Probably the most unique element in Cj documents is the item sections. The items section is similar to HAL’s root-level properties and Siren’s properties object. Cj items contain the domain objects in the response, such as users, customers, products, and so forth. However, unlike the way HAL and Siren express domain objects, Cj has a highly structured approach. HAL and Siren express their domain objects as either simple name–value pairs or, in the case of Siren, as subentities. And both HAL and Siren support sending nested JSON objects as properties. But Cj doesn’t work like that and this can be a source of both frustration and freedom.

Here is an example of a user object expressed as a Cj item:

{
  "rel": "item http://api.example.org/rels/user",
  "href": "http://api.example.org/user/alice", 1
  "data": [ 2
    {"name": "id", "value": "alice", "prompt": "ID", "render":"none"},
    {"name": "nick", "value": "alice", "prompt": "Nickname"},
    {"name": "email", "value": "[email protected]", "prompt": "Email"},
    {"name": "name", "value": "Alice Teddington, Jr.", "prompt": "Full Name"}
  ],
  "links": [ 3
    {
      "prompt": "Change Password",
      "rel": "edit-form http://api.example.org/rels/changePW",
      "href": "http://api.example.org/user/pass/alice"
    },
    {
      "prompt": "Assigned Tasks",
      "rel": "collection http://api.example.org/rels/filterByUser",
      "href": "http://api.example.org/task/?assignedUser=alice"
    }
  ]
}

As you see in this example, a Cj item contains the rel and href (1), and a list of data elements (2), and may also contain one or more link elements for read-only actions (3) associated with the item. The way Cj expresses the item properties (id, nick, email, and name) is unique among the formats covered in this book. Cj documents return not just the property identifier and value (e.g., "id":"alice") but also a suggested prompt property. Cj also supports other attributes, including render to help clients decide whether to display the property on screen. This highly structured format makes it possible to send both the domain data and metadata about each property and object. As we’ll see when we start working on the Cj client app, this added data comes in handy when creating a human-centric interface.

The links collection within each Cj item contains one or more static safe (read-only) links, like those in the root-level links collection. This space can be used to pass item-level links within a Cj response. For example, in the preceding snippet, you can see a link that points to a form for updating the user password and a link that points to a filtered list of tasks related to this user object. The item-level links section is optional and any link that appears in the collection must be treated as safe (e.g., dereferenced using HTTP GET).

Queries

The queries element in Collection+JSON is meant to hold safe requests (e.g., HTTP GET) that have one or more parameters. These are similar to HTML forms with the method attribute set to GET. The queries section in a Cj document is an array with one or more query objects. They look similar to Cj link objects but can have an associated data array, too.

Here’s an example:

{
  "rel": "search",
  "name" : "usersByEmai",
  "href": "http://api.example.org/user/",
  "prompt": "Search By Email",
  "data": [
    {
      "name": "email",
      "value": "",
      "prompt": "Email",
      "required": "true"
    }
  ]
}

As you can see from the preceding example, a Cj query object has rel, name, href, and prompt attributes. There also may be one or more data elements. The data elements are similar to HTML input elements. Along with the name, value, and prompt attributes, data elements can have required and (not shown in the previous example) readOnly and pattern attributes. These last attributes help services send clients additional metadata about the arguments for a query.

Note that Cj query objects do not have an attribute to indicate which HTTP method to use when executing the query. That is because Cj queries always use the HTTP GET method.

There is another Cj element that is similar to HTTP FORM: the template element.

Template

Cj’s template element looks similar to the Cj queries element—but it’s even smaller. It only has a set of one or more data elements. These data elements represent the input arguments for a write action (e.g., HTTP POST or PUT). Let’s take a look at what a Cj template looks like:

"template": {
  "prompt": "Add Task",
  "data": [
    {"name": "title", "value": "", "prompt": "Title", "required": "true"},
    {"name": "tags", "value": "", "prompt": "Tags"},
    {"name": "completeFlag", "value": "false", "prompt": "Complete",
      "patttern": "true|false"}
  ]
}

The template element can have an optional prompt, but the most important part of the template is the data array that describes the possible input arguments for the write operation. Like the data elements that appear in Cj queries and items, the template’s data elements include name and value properties along with a prompt property. And, like the queries version of data elements, they can have additional metadata attributes, including readOnly, required, and pattern. The pattern element works the same way as the HTML pattern attribute.

There are two important aspects of write operations missing from the Cj template: (1) the target URL, and (2) the HTTP method. That’s because in Cj, the template applies to two different parts of the CRUD model: create and update. Just how the request is executed depends on what the client app wants to do.

Using Cj templates to create new resources

When used to create a new member of the collection, the client app fills out the template and then uses the HTTP POST for the method and the value of the Cj document’s href as the target URL.

For example, using the Cj document represented at the start of this chapter (see “The Collection+JSON Format”), a client application can collect inputs from a user and send a POST request to add a new task record.

The HTTP request would look like this:

*** REQUEST ***
POST / HTTP/1.1 1
Host: http://rwcbook12.herokuapp.com
Content-Type: application/vnd.collection+json
...

"template": {
  "data": [
    {"name": "title", "value": "adding a new record"},
    {"name": "tags", "value": "testing adding"},
    {"name": "completeFlag", "value": "false"}
  ]
}
Tip

The Cj specification says that clients can send the template block (as seen in the previous example) or just send an array of data objects, and servers should accept both. Also, servers should accept payloads with data objects that include prompts and other properties and just ignore them.

As you can see in the previous example, the URL from the Cj document href along with the HTTP POST method is used to add a new resource to the Cj collection.

Using Cj templates to update an existing resource

When client apps want to update an existing resource, they use the HTTP PUT method and the href property of the item to update. Typically, client apps will automatically fill in the template.data array with the values of the existing item, allow users to modify that data, and then execute the PUT request to send the update information to the server:

*** REQUEST ***
PUT /1sv697h2yij HTTP/1.1 1
Host: http://rwcbook12.herokuapp.com
Content-Type: application/vnd.collection+json
...
"template": {
  "data": [
    {"name": "id", "value": "1sv697h2yij"},
    {"name": "title", "value": "Marina Del Ray"},
    {"name": "completed", "value": "true"}
  ]
}

Note that (at 1), the URL from the item’s href property is used along with the HTTP PUT method. This is how Cj clients use the template to update an existing item.

So—one template, two ways to use it. That’s how Cj describes write operations.

Error

The Collection+JSON design also includes an error element. This is used to pass domain-specific error information from server to client. For example, if a resource cannot be found or an attempt to update an existing record failed, the server can use the error element to return more than HTTP 404 or 400. It can return a text description of the problem and even include advice on how to fix it.

For example, if someone attempted to assign a TPS task to a nonexistent user, the server might respond like this:

{
  "collection": {
    "version": "1.0",
    "href": "//rwcbook12.herokuapp.com/error/",
    "title": "TPS - Task Processing System",
    "error": {
      "code": 400,
      "title": "Error",
      "message": "Assigned user not found (filbert). Please try again.",
      "url": "http://rwcbook12.herokuapp.com/task/assign/1l9fz7bhaho"
    }
  }
}

As mentioned earlier, there are some additional elements and properties of Cj documents that I won’t cover here. You can check out the full specification at the online site listed in the Reference section at the end of this chapter (see “References”).

A Quick Summary

By now, we can see that the three featured hypermedia types (HAL, Siren, and Cj) have several things in common. Like HAL and Siren, Cj has an element (links) for communicating Addresses. And, like Siren, Cj’s queries and template elements communicate Action metadata in responses. And all three have a way to communicate domain-specific Objects (HAL’s root-level properties, Siren’s properties object and Cj’s items collection). Cj’s items collection is unique because it includes metadata about each property in the domain object (e.g., prompt, render, etc.). This elevates Cj’s ability to handle the Object aspect of the OAA Challenge. We’ll talk about this again when we build the Cj client app.

For now, we have enough background to review the Cj representor and then walk through our Cj client SPA code.

The Collection+JSON Representor

As with other formats, the process of coding a Cj representor is a matter of converting our internal resource representation (in the form of a WeSTL object) into a valid Collection+JSON document. And, like the other representors, it takes only about 300 lines of NodeJS to build up a fully functional module to produce valid Cj responses.

Tip

The source code for the Cj representor can be found in the associated GitHub repo. A running version of the Cj version of the TPS API described in this section can be found online.

Following is a quick walk-through of the Cj representor code with highlights.

The Top-Level Processing Loop

The top-level processing loop for my Cj representor is very simple. It starts by initializing an empty collection object (to represent a Cj document in JSON) and then populates this object with each of the major Cj elements:

  • Links

  • Items

  • Queries

  • Template

  • Error (if needed)

function cj(wstlObject, root) {
  var rtn;

  rtn = {};
  rtn.collection = {}; 1
  rtn.collection.version = "1.0";

  for(var segment in wstlObject) {
    rtn.collection.href = root+"/"+segment+"/"; 2
    rtn.collection.title = getTitle(wstlObject[segment]); 3
    rtn.collection.links = getLinks(wstlObject[segment].actions);
    rtn.collection.items = getItems(wstlObject[segment],root);
    rtn.collection.queries = getQueries(wstlObject[segment].actions);
    rtn.collection.template = getTemplate(wstlObject[segment].actions);

    // handle any error
    if(wstlObject.error) { 4
      rtn.collection.error = getError(wstlObject.error);
    }
  }
  // send results to caller
  return JSON.stringify(rtn, null, 2); 5
}

The code just shown has just a few interesting items. After initializing a collection document (1) and establishing the document-level href (2), the code walks through the passed-in WeSTL object tree (3) and constructs the Cj title, links, items, queries, and template elements. Then, if the current object is an error, the Cj error element is populated (4). Finally, the completed Cj document is returned (5) to the caller.

Now, let’s take a look at each of the major routines used to build up the Cj document.

Links

The links element in Cj holds all top-level links for the document. The Cj representor code scans the incoming WeSTL object for any action element that qualifies and, if needed, resolves any URI templates before adding the link to the collection.

Here’s the code:

// get top-level links
function getLinks(segment, root, tvars) {
  var link, rtn, i, x, tpl, url;

  rtn = [];
  if(Array.isArray(segment)!==false) {
    for(i=0,x=segment.length;i<x;i++) { 1
      link = segment[i];
      if(link.type==="safe" &&
        link.target.indexOf("app")!==-1 &&
        link.target.indexOf("cj")!==-1) 2
      {
        if(!link.inputs) {
          tpl = urit.parse(link.href);
          url = tpl.expand(tvars); 3
          rtn.push({ 4
            href: url,
            rel: link.rel.join(" ")||"",
            prompt: link.prompt||""
          });
        }
      }
    }
  }
  return rtn; 5
}

Here are the high points in the getLinks function:

1

If we have action objects, loop through them.

2

First, check to see if the current link meets the criteria for top-level links in a Cj document.

3

If it does, use the passed-in tvars collection (template variables) to resolve any URI Template.

4

Then add the results to the link collection.

5

Finally, return the populated collection to the caller.

Items

The next interesting function is the one that handles items. This is the most involved routine in the Cj representor. That’s because Cj does quite a bit to supply both data and metadata about each domain object it passes to the client app.

Here’s the code:

// get list of items
function getItems(segment, root) {
  var coll, temp, item, data, links, rtn, i, x, j, y;

  rtn = [];
  coll = segment.data;
  if(coll && Array.isArray(coll)!==false) {
    for(i=0,x=coll.length;i<x;i++) {
      temp = coll[i];

      // create item and link
      item = {}; 1
      link = getItemLink(segment.actions);
      if(link) {
        item.rel = (Array.isArray(link.rel)?link.rel.join(" "):link.rel);
        item.href = link.href;
        if(link.readOnly===true) {
          item.readOnly="true";
        }
      }

      // add item properties
      tvars = {}
      data = [];
      for(var d in temp) { 2
        data.push(
          {
            name : d,
            value : temp[d],
            prompt : (g.profile[d].prompt||d),
            render:(g.profile[d].display.toString()||"true")
          }
        );
        tvars[d] = temp[d];
      }
      item.data = data;

      // resolve URL template 3
      tpl = urit.parse(link.href);
      url = tpl.expand(tvars);
      item.href = url;

      // add any item-level links 4
      links = getItemLinks(segment.actions, tvars);
      if(Array.isArray(links) && links.length!==0) {
        item.links = links;
      }

      rtn.push(item); 5
    }
  }
  return rtn; 6
}

The getItems routine is the largest in the Cj representor. It actually handles three key things: the URL for the item, the item’s data properties, and any links associated with the item. Here’s the breakdown:

1

For each data item in the list, first set the href property.

2

Then loop through the properties of the domain object and construct Cj data elements.

3

After collecting the data values, use that collection to resolve any URL template in the item’s href.

4

Next, go collect up (and resolve) any Cj link objects for this single item.

5

Once all that is done, add the results to the internal item collection.

6

Finally, return the completed collection to the calling routine.

The resulting item collection looks like this:

"items": [
  {
    "rel": "item",
    "href": "http://rwcbook12.herokuapp.com/task/1l9fz7bhaho",
    "data": [
      {"name":"id","value":"1l9fz7bhaho","prompt":"ID","render":"true"},
      {"name":"title","value":"extensions","prompt":"Title","render":"true"},
      {"name":"tags","value":"forms testing","prompt":"Tags","render":"true"},
      {"name":"completeFlag","value":"true","prompt":"Complete Flag",
        "render":"true"},
      {"name":"assignedUser","value":"carol","prompt":"Asigned User",
        "render":"true"},
      {"name":"dateCreated","value":"2016-02-01T01:08:15.205Z",
        "prompt":"Created","render":"false"}
    ],
    "links": [
      {
        "prompt": "Assign User",
        "rel": "assignUser edit-form",
        "href": "http://rwcbook12.herokuapp.com/task/assign/1l9fz7bhaho"
      },
      {
        "prompt": "Mark Active",
        "rel": "markActive edit-form",
        "href": "http://rwcbook12.herokuapp.com/task/active/1l9fz7bhaho"
      }
    ]
  }
  ... more items here ...
]

Queries

The getQueries routine is the one that generates the “safe” parameterized queries—basically HTML GET forms. That means, along with a URL, there is a list of one or more argument descriptions. These would be the input elements of an HTML form. The code that generates Cj queries is very straightforward and looks like this:

// get query templates
function getQueries(segment) {
  var data, d, query, q, rtn, i, x, j, y;

  rtn = [];
  if(Array.isArray(segment)!==false) {
    for(i=0,x=segment.length;i<x;i++) { 1
      query = segment[i];
      if(query.type==="safe" &&  2
        query.target.indexOf("list")!==-1 &&
        query.target.indexOf("cj") !==-1)
      {
        q = {}; 3
        q.rel = query.rel.join(" ");
        q.href = query.href||"#";
        q.prompt = query.prompt||"";
        data = [];
        for(j=0,y=query.inputs.length;j<y;j++) { 4
          d = query.inputs[j];
          data.push(
            {
              name:d.name||"input"+j,
              value:d.value||"",
              prompt:d.prompt||d.name,
              required:d.required||false,
              readOnly:d.readOnly||false,
              patttern:d.pattern||""
            }
          );
        }
        q.data = data;
        rtn.push(q); 5
      }
    }
  }
  return rtn; 6
}

The walk-through is rather simple:

1

Loop through all the transitions in the WeSTL document.

2

Find the transitions that are valid for the Cj queries collection.

3

Start an empty query object and set the href and rel properties.

4

Loop through the WeSTL input elements to create Cj data elements for the query.

5

Add the completed query to the collection.

6

Finally, return that collection to the calling routine.

Again, there is no HTTP method supplied for each query since the spec says all Cj queries should be executed using HTTP GET.

That covers the read operations in Cj. Next is the work to handle the write operations—the ones handled by the Cj template.

Template

In Cj, write operations are represented in the template element. The getTemplate routine in our Cj representor handles generating the template element, and the code looks like this:

// get the add template
function getTemplate(segment) {
  var data, temp, field, rtn, tpl, url, d, i, x, j, y;

  rtn = {};
  data = [];
  if(Array.isArray(segment)!==false) {
    for(i=0,x=segment.length;i<x;i++) {
      if(segment[i].target.indexOf("cj-template")!==-1) { 1
        temp = segment[i];

        // emit data elements
        data = [];
        for(j=0,y=temp.inputs.length;j<y;j++) { 2
          d = temp.inputs[j];
          field = { 3
            name:d.name||"input"+j,
            value:(d.value||"",
            prompt:d.prompt||d.name,
            required:d.required||false,
            readOnly:d.readOnly||false,
            patttern:d.pattern||""
          };
          data.push(field); 4
        }
      }
    }
  }
  rtn.data = data;
  return rtn; 5
}

There is not much to the getTemplate routine, so the highlights are a bit boring:

1

Loop through the WeSTL transitions and find the one valid for Cj template.

2

Then loop through the transition’s input collection.

3

Use that information to build a Cj data element.

4

And add that to the collection of data elements for this template.

5

Finally, after adding the completed data collection to the template object, return the results to the caller.

As a reminder, there is no href property or HTTP method for Cj templates. The URL and method to use are determined by the client at runtime based on whether the client is attempting a Create or Update action.

That leaves just one small object to review: the Cj error element.

Error

Unlike HAL and Siren, Cj has a dedicated error element for responses. This makes it easy for clients to recognize and render any domain-specific error information in server responses. There are only four defined fields for the Cj error object: title, message, code, and url. The getError function is small and looks like this:

// get any error info
function getError(segment) {
  var rtn = {};

  rtn.title = "Error";
  rtn.message = (segment.message||"");
  rtn.code = (segment.code||"");
  rtn.url = (segment.url||"");

  return rtn;
}

There is really nothing to talk about here since the routine is so simple. It is worth pointing out that Cj responses can include both error information and content in the links, items, queries, and template elements. That makes it possible to return a fully populated Cj document along with some error information to help the user resolve any problems.

With the Cj representor walk-through completed, it’s time to review the Cj client SPA.

The Collection+JSON SPA Client

OK, now we can review the Collection+JSON single-page app (SPA). This Cj client supports all the major features of Cj, including links, items, queries, and template. It also supports other Cj elements, including title, content, and error elements.

Tip

The source code for the Cj SPA client can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

As we did for the SPAs described in Chapter 2 (JSON), Chapter 4 (HAL), and Chapter 6 (Siren), we’ll start with a review of the HTML container and then move on to review the top-level parsing routine, along with the major functions that parse the key Cj document sections, to build up the rest of the general Cj client.

The HTML Container

All the SPA apps in this book start with an HTML container, and this one is no different. Here is the static HTML that is used to host the Cj documents sent by the server:

<!DOCTYPE html>
<html>
  <head>
    <title>Cj</title>
    <link href="./semantic.min.css" rel="stylesheet" />
  </head>
  <body>
    <div id="links"></div> 1
    <div style="margin: 5em 1em">
      <h1 id="title" class="ui page header"></h1> 2
      <div id="content" style="margin-bottom: 1em"></div> 3
      <div class="ui mobile reversed two column stackable grid">
        <div class="column">
          <div id="items" class="ui segments"></div> 4
        </div>
        <div class="column">
          <div id="edit" class="ui green segment"></div>
          <div id="template" class="ui green segment"></div> 5
          <div id="error"></div> 6

          <div id="queries-wrapper">
            <h1 class="ui dividing header">
              Queries
            </h1>
            <div id="queries"></div> 7
          </div>
        </div>
      </div>

      <div>
        <pre id="dump"></pre>
      </div>

    </div>
  </body>
  <script src="dom-help.js">//na </script>
  <script src="cj-client.js">//na </script> 8
  <script>
    window.onload = function() {
      var pg = cj();
      pg.init("/", "TPS - Task Processing System"); 9
    }
  </script>
</html>

A lot of the HTML shown here supports the layout needs of the CSS library. But you can still find all the major Cj document elements represented by <div> tags in the page. They are:

1

The links collection

2

The title element

3

The content element

4

The items element

5

The template element

6

The error element

7

The queries element

The Cj parsing script is loaded at 8 and, after everything loads, the initial request starts at 9. That line calls into the top-level parse loop for the Cj library.

The Top-Level Parse Loop

In the Cj client, the top-level parse loop gets called each time a user makes a selection in the UI, which follows the Request, Parse, Wait (RPW) pattern I covered in Chapter 5. It turns out the parse loop for Cj is a bit simpler than the ones for the JSON, HAL, and Siren clients:

// init library and start
  function init(url) {
    if(!url || url==='') {
      alert('*** ERROR:

MUST pass starting URL to the Cj library');
    }
    else {
      global.url = url;
      req(global.url,"get"); 1
    }
  }

  // primary loop
  function parseCj() { 2
    dump();
    title();
    content();
    links();
    items();
    queries();
    template();
    error();
    cjClearEdit();
  }

This code set should look pretty familiar by now. After making the initial request (1), the parseCj routine (2) is called and it walks through all the major elements of a Collection+JSON document. The only other interesting elements in this code snippet are the internal routines. First, the call to the dump() method at the start of the loop—this is just for debugging help on screen—and second, the cjClearEdit() call at the end of the routine to handle cleaning up the HTML div used to display the UI’s current editing form.

I’ll skip talking about the title and content routines here—you can check them out in the source code. We’ll now walk through the other major routines to handle Cj responses.

Links

The routine that handles parsing and rendering Cj links is pretty simple. However, it has a bit of a twist. The code checks the domain-specific metadata about the link. For example, some links are not rendered on the screen (e.g., HTML stylesheets, IANA profile identifiers, etc.). Some other links should actually be rendered as embedded images instead of navigational links. The Cj design allows servers to indicate this level of link metadata in the message itself—something the HAL and Siren clients do not support in their current design.

Here’s the code for the links() function:

// handle link collection
function links() {
  var elm, coll, menu, item, a, img, head, lnk;

  elm = domHelp.find("links");
  domHelp.clear(elm);
  if(global.cj.collection.links) { 1
    coll = g.cj.collection.links;
    menu = domHelp.node("div");
    menu.className = "ui blue fixed top menu";
    menu.onclick = httpGet;

    for(var link of coll) { 2
      // stuff render=none Cj link elements in HTML.HEAD 3
      if(isHiddenLink(link)===true) {
        head = domHelp.tags("head")[0];
        lnk = domHelp.link({rel:link.rel,href:link.href,title:link.prompt});
        domHelp.push(lnk,head);
        continue;
      }
      // render embedded images, if asked 4
      if(isImage(link)===true) {
        item = domHelp.node("div");
        item.className = "item";
        img = domHelp.image({href:link.href,className:link.rel});
        domHelp.push(img, item, menu);
      }
      else {
        a = domHelp.anchor({rel:link.rel,href:link.href,text:link.prompt, 5
          className: "item"});
        v.push(a, menu);
      }
    }
    domHelp.push(menu, elm); 6
  }
}

While there is quite a bit of code here, it’s all straightforward. The highlights are:

1

After making sure there are Cj links to process, set up some layout to hold them.

2

Now start looping through the links collection.

3

If the link element should now be rendered, place it in the HTML <head> section of the page.

4

If the link element should be rendered as an image, process it properly.

5

Otherwise, treat is as a simple <a> tag and add it to the layout.

6

Finally, push the results to the viewable screen.

Figure 8-2 shows an example of rendering the Cj links at runtime.

rwcl 0802
Figure 8-2. Rendering Cj links at runtime

It turns out that the cases where the link element is not displayed (3) or the link is an image (4) takes more code than cases where the link element is just a navigational element (5). We’ll see some more of that kind of code when we parse the items collection.

Items

The items() function is the most involved routine in the Cj library. At 125 lines, it is also the longest. That’s because (as we saw when reviewing the items handling in the Cj representor) the items element is the most involved of all in the Cj document design. I won’t include all the lines of this routine but will show the key processing in the routine. You can find the full set of code in the source code repo associated with this chapter.

I’ll break up the code review for the items() routine into three parts:

  • Rendering Cj item editing links

  • Rendering Cj item links

  • Rendering Cj item data properties

First, the code that handles each item’s Read-Update-Delete links—the last three elements of the CRUD pattern. Each Cj item has an href property and, optionally, a readOnly property. Using this information as a guide, Cj clients are responsible for rendering appropriate support for the read, update, and delete links. You can see this in the code that follows. At 1, the read link is created. The update link is created at 2 and the delete link is created at 3. Note the checking of both the readOnly status of the client as well as whether the template can be found in the Cj document. These values are used to decide which links (update and delete) are rendered for the item:

// item link
a = domHelp.anchor( 1
  {
    href:item.href,
    rel:item.rel,
    className:"item link ui basic blue button",
    text:item.rel
  }
);
a.onclick = httpGet;
domHelp.push(a1,buttons);

// edit link
if(isReadOnly(item)===false && hasTemplate(g.cj.collection)===true) {
  a = domHelp.anchor( 2
    {
      href:item.href,
      rel:"edit",
      className:"item action ui positive button",
      text:"Edit"
    }
  );
  a.onclick = cjEdit;
  domHelp.push(a2, buttons);
}

// delete link
if(isReadOnly(item)===false) {
  a = domHelp.anchor( 3
    {
      href:item.href,
      className:"item action ui negative button",
      rel:"delete",
      text:"Delete"
    }
  );
  a.onclick = httpDelete;
  domHelp.push(a3,buttons);
}

The next important snippet in the items() routine is the one that handles any item-level links. In the code you can see (1) that, if there are links for this item, each link is checked to see if it should be rendered as an image (2) and, if not, if it can be rendered as a navigational link (3). Finally, after the links are processed, the results are added to the item display (4):

if(item.links) { 1
  for(var link of item.links) {
    // render as images, if asked
    if(isImage(link)===true) { 2
      p = domHelp.node("p");
      p.className = "ui basic button";
      img = domHelp.image(
        {
          className:"image "+link.rel,
          rel:link.rel,
          href:link.href
        }
      );
      domHelp.push(img, p, buttons);
    }
    else {
      a = domHelp.anchor( 3
        {
          className:"ui basic blue button",
          href:link.href,
          rel:link.rel,
          text:link.prompt
        }
      );
      a.onclick = httpGet;
      domHelp.push(a,buttons);
    }
  }
  domHelp.push(buttons,segment); 4
}

The last snippet to review in the items() routine is the one that handles all the actual data properties of the item. In this client, they are rendered one by one as part of a UI table display. As you can see, the code is not very complicated:

for(var data of item.data) {
  if(data.display==="true") {
    tr = domHelp.data_row(
      {
        className:"item "+data.name,
        text:data.prompt+"&nbsp;",
        value:data.value+"&nbsp;"
      }
    );
    domHelp.push(tr,table);
  }
}

That’s all for the items() routine. Figure 8-3 is an example of the generated UI for Cj items.

rwcl 0803
Figure 8-3. Generated Cj items

Next up is the routine that handles the queries element of the Cj document.

Queries

The queries() routine processes all the elements in the Cj queries collection and turns them into HTML GET forms. The code is not very complex, but it is a bit verbose. It takes quite a few lines to generate an HTML form! The code our Cj client uses for generating the UI for Cj queries follows:

// handle query collection
function queries() {
  var elm, coll;
  var segment;
  var form, fs, header, p, lbl, inp;

  elm = domHelp.find("queries");
  domHelp.clear(elm);
  if(global.cj.collection.queries) { 1
    coll = global.cj.collection.queries;
    for(var query of coll) { 2
      segment = domHelp.node("div");
      segment.className = "ui segment";
      form = domHelp.node("form"); 3
      form.action = query.href;
      form.className = query.rel;
      form.method = "get";
      form.onsubmit = httpQuery;
      fs = domHelp.node("div");
      fs.className = "ui form";
      header = domHelp.node("div");
      header.innerHTML = query.prompt + "&nbsp;";
      header.className = "ui dividing header";
      domHelp.push(header,fs);
      for(var data of query.data) { 4
        p = domHelp.input({prompt:data.prompt,name:data.name,value:data.value});
        domHelp.push(p,fs);
      }
      p = domHelp.node("p"); 5
      inp = domHelp.node("input");
      inp.type = "submit";
      inp.className = "ui mini submit button";
      domHelp.push(inp,p,fs,form,segement,elm); 6
    }
  }
}

The queries routine has just a few interesting points to cover:

1

First, see if there are any queries in this response to process.

2

If so, loop through each of them to build up a query form.

3

Create the HTML <form> element and populate it with the proper details.

4

Walk through each data element to create the HTML <inputs> that are needed.

5

Then add the submit button to the form.

6

Finally, add the resulting markup to the UI for rendering on the page.

That’s how the Cj client handles generating all the safe query forms (e.g., HTTP GET). There are a few parts that deal with the HTML layout that are left out here, but you can see the important aspects of the queries() routine. Figure 8-4 shows an example of the generated query forms in our Cj client app.

rwcl 0804
Figure 8-4. The generated Cj query forms

Template

Just as Cj queries describe safe actions (e.g., HTTP GET), the Cj template describes the unsafe actions (e.g., HTTP POST and PUT). The code looks very similar to the code for generating Cj queries:

// handle template object
function template() {
  var elm, coll;
  var form, fs, header, p, lbl, inp;

  elm = domHelp.find("template");
  domHelp.clear(elm);
  if(hasTemplate(global.cj.collection)===true) { 1
    coll = global.cj.collection.template.data;
    form = domHelp.node("form"); 2
    form.action = global.cj.collection.href;
    form.method = "post";
    form.className = "add";
    form.onsubmit = httpPost;
    fs = domHelp.node("div");
    fs.className = "ui form";
    header = domHelp.node("div");
    header.className = "ui dividing header";
    header.innerHTML = global.cj.collection.template.prompt||"Add";
    domHelp.push(header,fs);
    for(var data of coll) { 3
      p = domHelp.input(
        {
          prompt:data.prompt+"&nbsp;",
          name:data.name,
          value:data.value,
          required:data.required,
          readOnly:data.readOnly,
          pattern:data.pattern
        }
      );
      domHelp.push(p,fs);
    }
    p = domHelp.node("p"); 4
    inp = domHelp.node("input");
    inp.className = "ui positive mini submit button";
    inp.type = "submit";
    d.push(inp,p,fs,form,elm); 5
  }
}

Here are the highlights for the template routine:

1

Confirm there is a template element in the loaded Cj document.

2

If there is, start building and populating an HTML <form>.

3

Using the template’s data properties, create one or more HTML <input> elements.

4

After all the inputs are created, add an HTML submit button.

5

Finally, add the completed HTML form to the UI.

You will also notice in the preceding code that the HTML <form> element is set to use the POST method. This takes care of the create use case for Cj template. For the update use case, there is a shadow routine in the Cj client called cjEdit(). This is invoked when the user presses the Edit button generated for each item. I won’t review the code for cjEdit() here (you can check out the source yourself) but will just mention that it looks almost identical except for a few changes related to the HTTP PUT use case.

Figure 8-5 is an example of the Cj template rendered for the create use case.

rwcl 0805
Figure 8-5. Generating the Cj create UI

The only code left to review for the Cj client is the code that handles any error elements in the response.

Error

Cj is the only hypermedia design featured in this book that has built-in support for sending domain-specific error information. The Cj error element is very simple. It has only four properties: title, message, code, and url. So the client routine for rendering errors is simple, too.

The following code shows that the Cj client app just echoes the properties of the error element in Cj responses directly to the screen:

// handle error object
function error() {
  var elm, obj;

  elm = domHelp.find("error");
  domHelp.clear(elm);
  if(global.cj.collection.error) {
    obj = global.cj.collection.error;

    p = d.para({className:"title",text:obj.title});
    domHelp.push(p,elm);

    p = d.para({className:"message",text:obj.message});
    domHelp.push(p,elm);

    p = d.para({className:"code",text:obj.code});
    domHelp.push(p,elm);

    p = d.para({className:"url",text:obj.url});
    domHelp.push(p,elm);
  }
}

Quick Summary

The Cj client differs from the HAL and Siren clients reviewed earlier in the book in a number of respects, most significantly in the way domain objects are handled in Cj. Instead of just echoing a set of name–value pairs or even a nested JSON object graph, Collection+JSON only supports returning flat lists of items. Each item represents more than just a domain object’s properties. It also includes metadata about the domain object (prompt and render information) and a collection of one or more link elements associated with the domain object.

The way safe and unsafe actions are expressed in Cj is also unique. Instead of leaving it up to source code (as in HAL) or relying on a general model for all actions (as in Siren), the Cj design supports two different action elements: the queries and template elements. Cj queries are for safe actions (e.g., HTML GET) and the template is for unafe actions (e.g., HTTP POST and PUT).

The other main element in Cj documents is the links collection, which is very similar to the way both HAL and Siren express links, too.

Now that we have a fully functional Cj general client, let’s introduce some modifications to the backend TPS API and see how it deals with backward-compatible changes.

Dealing with Change

In previous chapters covering the JSON (Chapter 2), HAL (Chapter 4), and Siren (Chapter 6) SPA clients, I introduced various backward-compatible changes to the TPS API in order to explore the runtime adaptability of the client. The changes all dealt with one or more changes that the three key aspects web API clients need to deal with: Objects, Addresses, and Actions. How the client apps reacted to the changes gave us an indication of their adaptability using our OAA Challenge.

Tip

The source code for the updated Cj representor with Note support can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

For the Cj client, I’ll introduce an entirely new Object (Notes) along with a full set of Actions and Addresses. This level of change represents examples of all the kinds of changes we’ve introduced to the other SPA clients before. This will test the Cj client’s ability to recognize and deal with domain objects and operations that were introduced long after the initial production release of the API and client implementations.

Adding the Note Object to the TPS API

Let’s assume that the TPS team decides to add support for attaching comments or Notes to Task records in the TPS API. That means defining a Note object’s fields and adding support for the basic CRUD operations on Note objects along with some other NOTE-specific actions like filters, etc.

In this section, I’ll review the API design elements (internal Note object and public API) and the resulting WeSTL document, and take a look at a bit of the server code. Then, after completing the backend changes, we’ll fire up the Cj client and see what happens.

The note API design

Our Note object will have a small set of fields, support the basic CRUD operations, a couple of filters, and a custom NoteAssignTask operation. Table 8-1 shows the Note object properties.

Table 8-1. Note Object Properties
Property Type Status Default

id

string

required

none

title

string

required

none

text

string

optional

none

assignedTask

taskID

required

none

Along with the Create-Read-Update-Delete (CRUD) actions, we’ll need a couple of filters (NoteListByTitle and NoteListByText) that allow users to enter partial strings and find all the Note records that contain that string. We’ll also add a special operation to assign a Note to a Task (NoteAssignTask) that takes id values (a Note id and a Task id). Table 8-2 lists all the operations, arguments, and HTTP protocol details.

Table 8-2. TPS Note Object API
Operation URL Method Returns Inputs

NoteList

/note/

GET

NoteList

none

NoteAdd

/note/

POST

NoteList

id, title, text, asssignedTask

NoteItem

/note/{id}

GET

NoteItem

none

NoteUpdate

/note/{id}

PUT

NoteList

id, title, text, assignedTask

NoteRemove

/note/{id}

DELETE

NoteList

none

NoteAssignTask

/note/assign/{id}

POST

NoteList

id, assignedTask

NoteListByTitle

/note/

GET

NoteList

title

NoteListByText

/note/

GET

NoteList

text

That’s all we need on the design side. Let’s look at how we’ll turn this design into a working API in our TPS service.

The note API service implementation

I won’t go into the details of the internal server-side code (data and object manipulation) for implementing the Notes object support in the TPS API. However, it is worth pointing out a few things on the interface side since they affect how we’ll set up the Cj responses sent to the existing client.

The first thing to add is the component code that defines the object described in Table 8-1. This code also validates inputs and enforces relationship rules (e.g., making sure users don’t assign Note records to nonexistent Task records). In the TPS API service, the Note object definition looks like this:

  props = ["id","title","text","assignedTask","dateCreated","dateUpdated"]; 1
  elm = 'note';

  // shared profile info for this object 2
  profile = {
    "id" : {"prompt" : "ID", "display" : true},
    "title" : {"prompt" : "Title", "display" : true},
    "text" : {"prompt" : "Text", "display" : true},
    "assignedTask" : {"prompt" : "Assigned Task", "display" : true},
    "dateCreated" :  {"prompt" : "Created", "display" : false},
    "dateUpdated" :  {"prompt" : "Updated", "display" : false}
  };

Notice that the props array (1) defines valid fields for a NOTE and the profile object (2) contains the rules for displaying objects to users (e.g., the prompt and display flags).

Following is the addTask routine for the note-component.js server-side code. It shows how the component builds up a new Note record to store (1) and validates the inputs (2), including checking for the existence of the supplied assignedTask ID (3). Then, as long as there are no errors found, the code sends the new Note record off for storage (4):

function addNote(elm, note, props) {
  var rtn, item, error;

  error = "";

  item = {} 1
  item.title = (note.title||"");
  item.text = (note.text||"");
  item.assignedTask = (note.assignedTask||"");

  if(item.title === "") { 2
    error += "Missing Title ";
  }
  if(item.assignedTask==="") {
    error += "Missing Assigned Task ";
  }
  if(component.task('exists', item.assignedTask)===false) { 3
    error += "Task ID not found. ";
  }

  if(error.length!==0) {
    rtn = utils.exception(error);
  }
  else {
    storage(elm, 'add', utils.setProps(item,props)); 4
  }

  return rtn;
}

That’s enough of the internals. Now let’s look at the interface code—the WeSTL entries that define the transitions for manipulating Note objects, and the resource code that handles the HTTP protocol requests exposed via the API.

Here are some of the transition descriptions for Note objects. I’ve included the noteFormAdd transition that will populate the Cj template (1) and the two transitions for the “Assign Task” action: the one that offers a link to the form (2) and the template for making the assignment (3):

  // add task 1
  trans.push({
    name : "noteFormAdd",
    type : "unsafe",
    action : "append",
    kind : "note",
    target : "list add hal siren cj-template",
    prompt : "Add Note",
    inputs : [
      {name : "title", prompt : "Title", required : true},
      {name : "assignedTask", prompt : "Assigned Task", required : true},
      {name : "text", prompt : "Text"}
    ]
  });

  ...

  trans.push({ 2
    name : "noteAssignLink",
    type : "safe",
    action : "read",
    kind : "note",
    target : "item cj read",
    prompt : "Assign Task",
  });
  trans.push({ 3
    name : "noteAssignForm",
    type : "unsafe",
    action : "append",
    kind : "note",
    target : "item assign edit post form hal siren cj-template",
    prompt : "Assign Task",
    inputs : [
      {name: "id", prompt:"ID", readOnly:true, required:true},
      {name: "assignedTask", prompt:"Task ID", value:"", required : true}
    ]
  });

Because the Cj media type design relies heavily on the CRUD pattern, unsafe operations that don’t easily fall into the CRUD model (in this case, the noteAssignForm operation) need to be handled differently. In Cj, these nonstandard CRUD actions are offered as templates and executed with an HTTP POST—the way you’d create a new object in a standard CRUD pattern.

To support this, I need two transitions: one that returns the “assign template” (noteAssignLink) and the other that accepts the POST call to commit the assign arguments to storage (noteAssignForm). Since WeSTL doesn’t supply URLs, the source code (in the /connectors/note.js file on the server) does that at runtime.

Here’s what that snippet of code looks like:

// add the item-level link
wstl.append({name:"noteAssignLink",href:"/note/assign/{id}",
  rel:["edit-form","/rels/noteAssignTask"],root:root},coll);

// add the assign-page template
wstl.append({name:"noteFormAdd",href:"/note/",
  rel:["create-form","/rels/noteAdd"],root:root},coll);

Finally, I’ll need to account for this when handling HTTP requests, too. Here is the code that responds to the HTTP GET for the “assign page” (e.g., /note/assign/1qw2w3e):

case 'GET':
  if(flag===false && parts[1]==="assign" && parts[2]) {
    flag=true;
    sendAssignPage(req, res, respond, parts[2]);
  }

And here’s the snippet of code that responds to the HTTP POST request that commits the assignment:

case 'POST':
  if(parts[1] && parts[1].indexOf('?')===-1) {
    switch(parts[1].toLowerCase()) {
      case "assign":
        assignTask(req, res, respond, parts[2]);
      break;
    }
  }

There is more in the server-side code (e.g., adding the page-level link to the new Notes API, etc.) that you can check out yourself. The point here is that Cj forces API designers to explicitly account for the non-CRUD unsafe actions (via POST) right up front. This is a bit more work for API designers (well, you’d have to do it eventually anyway) but it makes support in the Cj client much easier. In fact, that support is already there.

So let’s see what happens when we fire up our existing, unchanged, Cj client against this updated TPS API.

Testing the note API with the existing Cj client

In a real-life scenario, the TPS API would be updated into production without prior warning to all the existing Cj clients. Then at some point, one of the clients might make an initial request to the TPS API, just as in previous days, and automatically see the new Notes option at the top of the page (see Figure 8-6).

rwcl 0806
Figure 8-6. New Notes option in the Cj client

When the user clicks on the Notes link, the fully populated interface comes up with all the display and input constraints enforced. Along with the expected Read, Edit, and Delete buttons for each item plus the Add Note form, users will also see (in Figure 8-7) the special Assign Task link that appears for each Note in the list.

rwcl 0807
Figure 8-7. The Notes page in the Cj client

Finally, clicking the Assign Task button will bring up the screen that prompts users to enter the id value of the Task to which this note should be attached (see Figure 8-8).

rwcl 0808
Figure 8-8. Assigning a Note to a Task

So, the existing Cj client was able to support all the new API functionality (the new Object, Addresses, and Actions) without any new coding or configuration. At this point, the TPS web API team has quite a bit of freedom to modify the existing production API. As long as the changes are backward-compatible, not only will the Cj client not break when new changes appear, but the new changes will be fully supported (not just safely ignored) for now and into the future.

Cj and the OAA Challenge

The OAA Challenge tests whether the format was designed to support dynamically changing the API’s Objects, Addresses, and Actions and have a standard client application automatically adapt to those changes at run time without the need for any custom coding. The Collection+JSON format is the only format we’ve covered in this book that was designed to meet the OAA Challenge without the need for private extensions or customization of the format.

That means that web APIs that can output responses in Cj can assume that there is a client that is able to adapt to changes in things like:

  • Adding new links and forms

  • Changing the URLs of existing links and forms

  • Changing the input rules for forms

  • Adding new resource objects

  • Changing the rules on whether users can edit or delete resource objects

This gives API providers a wide range of choices for updating existing APIs without breaking any client apps using that same API. And that means client app developers do not have to constantly monitor the change notices from a service provider in order to head off a fatal crash or unexpected loss of functionality.

There are trade-offs for this ability to adapt, too. The standard Cj client applications lose some aspect of control—they end up showing the UI that the API wants them to see, they follow the workflow provided by the API, and they show the links and forms the API thinks are important. The layout is even controlled, to some extent, by the API provider. These can be hard things to accept for those who spend most of their time crafting pixel-perfect bespoke user interfaces for web APIs.

However, there is room here for both API provider and client developer to each take control of the things that matter. If client-side developers decide they want full control of the user experience, Cj client apps can be crafted to continue to ignore new functionality. Some of the techniques for making this possible were covered in “Client Implementors”. And, as long as API providers follow the rules outlined in “Server Implementors”, client apps will not experience unexpected breakage even if they ignore the providers’ new features.

Quick Summary

So, working with Cj is pretty nice. But things are not perfect with our Cj client. As you might have noticed on those last couple screens, entering the text of a note is a problem—the text box is too small. It should be rendered as an HTML <textarea> control to support longer text entry and even scroll bars. Even more problematic is the Assign Task data entry. There, users are expected to supply two rather opaque record identifiers (NoteID and TaskID) in order to complete the Assign Task action. That’s not very user-friendly and it is likely to be rejected by any team responsible for building quality user experiences.

To fix this shortcoming, we’ll need to extend the Cj design to do a better job of describing (and supporting) user-friendly input experiences. There are lots of options for improvement and I’ll just focus on two of them for now: (1) improving client-side support for input types, and (2) supporting a drop-down or suggested list of possible values for input.

Extending Collection+JSON

Cj has powerful support for passing metadata on links (Addresses), forms (Actions), and domain objects (Objects). However, it has relatively weak support for passing metadata about user-supplied inputs. The ability to indicate input properties such as required, readOnly, and pattern (all directly from HTML5) is a start, but more is needed. For example, Swiber’s Siren (see Chapter 6) has much stronger support for input metadata.

The good news is that Cj has a clear option for creating extensions to fill in gaps in the design. And that’s what I’ll do here. This section outlines an extension for supporting a type attribute for Cj data elements (à la HTML5’s type attribute) and a suggest attribute to provide input metadata similar to that of the HTML <select> input control.

Tip

The source code for the updated Cj representor with Note support can be found in the associated GitHub repo. A running version of the app described in this section can be found online.

These extensions will improve the ability of Cj client apps to provide a solid user experience.

Supporting Improved Input with Cj-Types

Adding support for HTML5-style input types (e.g., email, number, url, etc.) is a pretty simple extension for Cj. It’s just a matter of adding the type property to the Cj output and honoring it on the client side. There are also a series of associated properties like min, max, size, maxlength, etc. that should also be supported.

The Cj media type has an option for adding backward-compatible extensions to allow new features to emerge without the need for advanced approval and modification of the media type specification itself.

Extending Cj with cj-types

Here is an example of an extension I’ll call cj-types to add support for improved client-side input validation. First, let’s see what that would look like in the Cj representation. Note the use of the pattern (1 and 3) and "type":"email" (2):

"template": {
  "prompt": "Add User",
  "rel": "create-form userAdd create-form",00
  "data": [
    {"name": "nick","value": "","prompt": "Nickname","type": "text",
      "required": "true","pattern": "[a-zA-Z0-9]+"}, 1
    {"name": "email","value": "","prompt": "Email","type": "email"}, 2
    {"name": "name","value": "","prompt": "Full Name","type": "text",
        "required": "true"},
    {"name": "password","value": "","prompt": "Password","type":"text",
        "required": "true","pattern": "[a-zA-Z0-9!@#$%^&*-]+"} 3
  ]
}
Tip

The specification for the cj-types extension can be found in the associated GitHub repo. You can also view the completed spec online.

Another handy HTML UI element is the <textarea> element. Adding support for textarea in Cj is also pretty simple. The Cj template would look like this (see 1):

"template": {
  "prompt": "Add Note",
  "rel": "create-form //localhost:8181/rels/noteAdd",
  "data": [
    {"name": "title","value": "","prompt": "Title",
      "type": "text","required": "true"},
    {"name": "assignedTask","value": "","prompt": "Assigned Task",
      "type": "text","required": "true"},
    {"name": "text","value": "","prompt": "Text",
      "type": "area","cols": 40,"rows": 5} 1
  ]
}

Updating the representor

Updating our Cj representor (see “The Collection+JSON Representor”) module to support the new types extension happens in two places:

  • The getQueries() routine

  • The getTemplate() routine

To save space, I’ll just include a snippet from the getTemplate() implementation of our Cj representor here:

// emit data elements
data = [];
for(j=0,y=temp.inputs.length;j<y;j++) {
  d = temp.inputs[j];
  field = {
    name:d.name||"input"+j,
    value:(isAdd===true?d.value:g.tvars[d.name])||"",
    prompt:d.prompt||d.name,
    type:d.type||"text" 1
  };
  if(d.required){field.required=d.required.toString();} 2
  if(d.readOnly){field.readOnly=d.readOnly.toString();}
  if(d.pattern){field.pattern=d.pattern;}
  if(d.min){field.min=d.min;}
  if(d.max){field.max=d.max;}
  if(d.maxlength){field.maxlength=d.maxlength;}
  if(d.size){field.size=d.size;}
  if(d.step){field.step=d.step;}
  if(d.cols){field.cols=d.cols;}
  if(d.rows){field.rows=d.rows;}
  data.push(field);
}

This is really just a set of added property checks against the internal WeSTL document (see “The WeSTL Format”) to see if any of the new type values exist and, if they do, they are passed along in the Cj representation. The highlights are:

1

If the type property exists in the WeSTL document, add that to the representation; otherwise, use the default "text" type then…

2

Go through the list of all the other possible cj-type properites and, if they are in the WeSTL, add them to the Cj representation.

Note that, if the new properties don’t exist, they are not sent at all. This keeps the representor from emitting confusing properties with some kind of default value for every Cj template or query element.

Next we can take a look at the client-side implementation of the cj-types extension.

Updating the Cj client library

Like the code for the Cj representor, the update for the Cj client library is pretty simple. We just need to check for the existence of the new cj-types properties on the data elements and, if they are there, emit them as valid HTML DOM elements.

Here’s a code snippet from the template() function in the cj-client.js library:

for(var data of coll) {
  p = domHelp.input(
    {
      prompt:data.prompt+"&nbsp;",
      name:data.name,
      value:data.value,
      required:data.required,
      readOnly:data.readOnly,
      pattern:data.pattern,
      type:data.type,
      max:data.max,
      min:data.min,
      maxlength:data.maxlength,
      size:data.size,
      step:data.step,
      cols:data.cols,
      rows:data.rows,
      suggest:data.suggest
    }
  );

You can see that all this code does is pass the properties from the Cj representation on to the domHelp library for conversion into value HTML DOM elements. The real magic happens in my dom-help.js library and that looks like this:

inp.name = args.name||"";
inp.className = inp.className + "value "+ (args.className||"");
inp.required = (args.required||false);
inp.readOnly = (args.readOnly||false);
if(args.pattern) {inp.pattern = args.pattern;}
if(args.max) {inp.max = args.max;}
if(args.min) {inp.min = args.min;}
if(args.maxlength) {inp.maxlength = args.maxlength;}
if(args.size) {inp.size = args.size;}
if(args.step) {inp.step = args.step;}
if(args.cols) {inp.cols = args.cols;}
if(args.rows) {inp.rows = args.rows;}

In the preceding code, the args collection was passed in from the call to domHelp.input() and the inp variable holds an instance of an HTML <input … /> control. There are a few minor details for handling the <area>…</area> but I’ll skip those for now.

Once it’s all put together and in place, Figure 8-9 shows how the CJ client screen looks with support for the area input type added.

rwcl 0809
Figure 8-9. Adding Cj support for textarea input

So, adding support for most HTML5-style inputs was rather easy. But there are a couple of HTML5-style inputs that take a bit more effort, and one of them is badly needed for the TPS user experience—the <select> or drop-down list.

The Cj-Suggest Extension

Supporting HTML-style drop-down lists takes a bit of planning. I’ll need to make modifications to the Cj document design, representor, and client library. I won’t go through a lot of detail here—just the highlights.

Tip

Check out the cj-suggest extension specification in the associated GitHub repo and compeleted spec document online.

Extending Cj with cj-suggest

First, we’ll need a way to communicate the drop-down input within the Cj data elements. The design I chose allows for two types of implementation: direct content and related content. I’ll explain the differences as we go along.

Here is a sample suggest element that uses the direct content approach:

data :
  [
    {
    "name": "completeFlag",
    "value": "false",
    "prompt": "Complete",
    "type": "select", 1
    "suggest": [ 2
      {"value": "false", "text": "No"},
      {"value": "true", "text": "Yes"}
    ]
  }
]

At 1, the new type attribute is set to "select" and, at 2, the suggest element is an array with an object that holds both the value and the text for HTML <select> elements.

The other type of suggest implementation I want to support is what I call the related model. It uses related data in the response as content for the drop-down list. That means I need to add a new element to the Cj document’s root: related. This Cj root element is a named object with one or more named JSON arrays that hold content for a drop-down list. Here’s what that looks like (see 1):

{
  "collection": {
    "version": "1.0",
    "href": "//localhost:8181/task/assign/1l9fz7bhaho",
    "links": [...],
    "items": [...],
    "queries": [...],
    "template": {...},
    "related": { 1
      "userlist": [
        {"nick": "alice"},
        {"nick": "bob"},
        {"nick": "carol"},
        {"nick": "fred"},
        {"nick": "mamund"},
        {"nick": "mook"},
        {"nick": "ted"}
      ]
    }
  }
}

And here’s the matching implementation for the suggest attribute (1) for Cj data elements:

data: [
  {
    "name": "assignedUser",
    "value": "mamund",
    "prompt": "User Nickname",
    "type": "select",
    "required": "true",
    "suggest": {"related": "userlist","value": "nick","text": "nick"} 1
  }
]

Now, Cj client applications can find the related data in the response (by the value of related) and use the property names listed in the suggest element to populate the drop-down list.

Updating the Cj representor

We need to include the related property in the output of the Cj representor. That’s pretty easy. We just create a small function to pull any related content into the response (1) and add that to the top-level routine that builds up the Cj response document (2):

// handle any related content in the response
function getRelated(obj) {
  var rtn;

  if(obj.related) {
    rtn = obj.related; 1
  }
  else {
    rtn = {};
  }
  return rtn;
}

...

// building the Cj response document
rtn.collection.title = getTitle(wstlObject[segment]);
rtn.collection.content = getContent(wstlObject[segment]);
rtn.collection.links = getLinks(wstlObject[segment].actions);
rtn.collection.items = getItems(wstlObject[segment],root);
rtn.collection.queries = getQueries(wstlObject[segment].actions);
rtn.collection.template = getTemplate(wstlObject[segment].actions);
rtn.collection.related = getRelated(wstlObject[segment]); 2

Updating the Cj client library

The Cj client library has a handful of things to deal with now, including:

  • Recognizing the new suggest attribute in responses

  • Locating any possible related content in the responses

  • Parsing the suggest element into a valid <select> element in the UI

  • Processing the value of the <select> element and including it in the POST and PUT actions

Most of this work will happen in my dom-help.js routine—that’s where the request to create an input element in the UI takes place. Here is a snippet of code I added to the input(args,related) routine:

....
if(args.type==="select" || args.suggest) { 1
  inp = node("select");
  inp.value = args.value.toString()||"";
  inp.className = "ui drop-down ";
  if(Array.isArray(args.suggest)) { 2
    for(var ch of args.suggest) {
      opt = option(ch);
      push(opt,inp);
    }
  }
  if(related) { 3
    lst = related[args.suggest.related];
    if(Array.isArray(lst)) { 4
      val = args.suggest.value;
      txt = args.suggest.text;
      for(var ch of lst) { 5
        opt = option({text:ch[txt],value:ch[val]});
        push(opt,inp);
      }
    }
  }
}
....

In the preceding code:

1

See if this is a suggest control.

2

If there is an array of values, use that to build the <option> elements for the HTML <select> control.

3

Check to see if a pointer to the related content in the response was passed.

4

If it was, and it returns a valid array of data…

5

Use that content to build up the <option> elements.

There are some additional client-side library changes to manage the details and collect and send selected values back to the service. You can check out the source code for details.

Once all this is in place, the UI for screens like Assign Task look much more inviting (Figure 8-10).

rwcl 0810
Figure 8-10. Adding drop-down support to Cj

Now, with the suggest extension and added support for improved input metadata, Cj offers not only fully functional support for adding new Objects to the backend API, but it also has better user interface support. It is worth pointing out that most of the input support I added to Cj as extensions already exists as part of the design for Siren (see “Actions”).

Reader Challenge

My suggest implementation has two modes: direct and related. There is at least one more mode I didn’t implement that I’d really like to see: web mode. In web mode, the suggest.related value is a valid URL pointing to an API response that returns the list of choices. It could be used to create a simple drop-down or it could be used to implement a key-stroke experience that performs a search on each key press and returns suggested results. I’ll leave the details to my intrepid readers to work out on their own—and submit as an update to the online GitHub repo.

Quick Summary

In this section, I added two Cj extensions:

cj-types

For enriching the way client-side inputs are displayed and validated

cj-suggest

To add support for drop-down list inputs

The good news is that adding these kinds of extensions to Cj is pretty straight-forward. It is also worth noting that these were backward-compatible extensions. If a standard Cj client that doesn’t have support for cj-types and cj-suggest gets an API response that contains these values, that client can safely ignore them and continue to work without breaking. This approach is actually clearly outlined in the Collection+JSON spec:

Any extensions to the Collection+JSON vocabulary MUST not redefine any objects (or their properties), arrays, properties, link relations, or data types defined in this document. Clients that do not recognize extensions to the Collection+JSON vocabulary SHOULD ignore them.

Now that we’ve explored the Cj format, implemented the server-side representor, and the extensible Cj client, it’s time to wrap this up and move on to our last challenge.

Summary

In previous chapters covering the JSON (Chapter 2), HAL (Chapter 4), and Siren (Chapter 6) SPA clients, I introduced various backward-compatible changes to the TPS API in order to explore the runtime adaptability of the client. The changes all dealt with one or more changes that the three key aspects web API clients need to deal with: Objects, Addresses, and Actions. How the client apps reacted to the changes gave us an indication of their adaptability using our OAA Challenge.

Here’s a quick summary of our experience so far:

JSON client

Changes to URLs, adding fields, or actions all were ignored by the client app. That gave us no “wins” on the OAA Challenge.

HAL client

Changes to URLs did not adversely affect the client. However, changes to domain objects and actions were ignored. That’s one “win” for HAL: Addresses.

Siren client

Changes to URLs and actions were all recognized and supported in the Siren client. However, changes to domain objects were not picked up by the client. That’s two “wins” for Siren: Addresses and Actions.

Cj client

As we saw in this chapter, adding an entirely new domain object (NOTES) was automatically picked up by the Cj client. All the URLs, fields, and operations appeared in the UI without the need to make any changes to the client code. That gives Cj all three “wins” in the OAA Challenge (Objects, Addresses, and Actions).

Of course, along the way, each client was enhanced through the application of extensions such as HAL-FORMS (see “The HAL-FORMS Extension”), POD documents (see “The POD Specification”), and cj-types and cj-suggest (from this chapter). And there are other possible ways in which to improve both the adaptability and the user experience of all of the client implementations reviewed in the book. But the Collection+JSON design offers clients the widest possible range of adaptability for the kinds of API changes that are most commonly experienced by API client apps.

While the Cj format seems to solve most of our problems, there is one more challenge worth exploring: the challenge of implementing a single SPA application that supports more than one hypermedia format—and can automatically switch between formats at runtime based on the service response.

And that’s the challenge we’ll take up in the next—and final—chapter of the book.

References

  1. The Atom Syndication Format is documented in RFC4287 and the Atom Publishing Protocol is covered in RFC5023.

  2. The most up-to-date specification docs for Cj can be found here.

  3. You’ll find various Cj examples and extensions in the online GitHub repository.

  4. The Google discussion group for Cj is archived here.

  5. The HTML <input> element has quite a few options. Check out the docs online at the W3C website.

  6. The specification for the HTML <textarea> element can be found at the W3C website.

Image Credits

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.42.129