Skip to content
David Jensen edited this page Mar 5, 2016 · 5 revisions

ASF architecture (work in progress)

ASF consists of the directive sfSchema (src/directives/schema-form.js) and a couple of services + some helping directives used internally to render the form.

Conceptually you can split it up in three parts:

  1. The sfSchema directive. This is what the user uses and is the starting point.
  2. JSON Schema and ASF form definition parsing and mangling. This is mainly the schemaForm service (src/services/schema-form.js).
  3. The builder that churns out the HTML from a canonical form definition. It's the sfBuilder

Basically it is a pipe line:

  1. It starts off by the sfSchema directive gets a form and a JSON schema from the user.
  2. It merges the schema and the form definition to one canonical form definition, basically a normalized form definition with references back to the schema for each field.
  3. sfSchema then calls the sfBuilder with a decorator and the canical form. It spews out DOM Nodes. We plop it into the form and apply angulars $compile to hook it all up.

The form definitions

There are roughly speaking two kinds of form definition: the one we get from the user, and the parsed and enhanced version we call the canonical form definition. The latter is what we loop over in the builder to build the DOM Nodes.

It's easier to explain with some examples:

So this is a simple form definition:

var form = [
  "name",
  {
    "key": "description",
    "type": "textarea"
  }
];  

There are a couple of things to notice:

  1. It's a list
  2. It's has objects and strings in it. The string 'name' is a shorthand for "include the field 'name' here with default settings"

A matching schema could look like this:

{
  "type": "object",
  "properties": {
    "name": {
      "title": "Item name",
      "type": "string"
    },
    "description": {
      "type": "string",
      "title": "Item description"
    },
    "deleted": {
      "type": "boolean"
    }
  },
  "required": ["name", "deleted"]
}

Example: http://schemaform.io/examples/bootstrap-example.html#/3231d278b6236fedbf3b

So if we pass these to the schemaForm.merge function we would get a canonical form back.

A canonical form:

  1. Always a list of objects
  2. Keys are always arrays of strings, not just string. This is so we can support complex characters in keys like space, -, " etc.
  3. It has default options inferred from the JSON Schema, for example a form type depending on the json schema type.
  4. It has defaults from global options
  5. It has a reference to the part of the json schema that it matches.

Here is an example of a merge between the schema and the form above:

[
  {
    "key": ["name"],
    "required": true,
    "schema": {
      "title": "Item name",
      "type": "string"
    }
    "title": "Item name"
    "type": "text"
  },
  {
    "key": ["description"]
    "schema": {
      "title": "Item description",
      "type": "string"
    },
    "title": "Item description",
    "type": "textarea"
  }
]

Notice that what in the user supplied form was just the string "name" now is an entire object, and that all their own part of the schema embedded.

Also notice that we've extracted if the field is required or not. This is useful to know on each field.

And even though there where a third property in the JSON Schema, "deleted", it didn't pop up in the final form. This is very much design. We like to support having form that only describe a part of a larger schema. This is often very useful, for instance you can have one schema that covers the validation of a "user" object but display it in a series of forms in a wizard or on several tabs etc.

As you can see this is a much nicer format for our builder to loop over and build a form from.

The sfSchema.merge method

Let's take a look at the actual merging, how it works and how we will need to change it. I will leave a lot of details out to simplify the examples, global options for instance.

This is psuedo code for what the merge does:

function merge(schema, form) {
  // First generate a "standard" form defintion from the JSON Schema alone. There are a couple of
  // extensible rules that define what JSON Schema type maps against what field type and defaults.
  // Also, very importantly, each field object has a reference back to its schema under the
  // property "schema". We later use it for validation.
  var result = defaults(schema);

  // The defaults method results in two things, a form definition with just defaults and an object,
  // "lookup", that acts as a mapping between "key" and field object for that key.
  var standardForm = result.form;
  var lookup = result.lookup;

  // Then we loop over the user supplied form definition and merge in the defaults
  // (this is simplified since a form definition isn't flat when using nested types like array,
  // fieldsets etc, but you get the idea)
  var canonical = [];
  angular.forEach(form, function(fieldObj) {
    // Handle the shorthand version of just a string instead of an object.
    if (typeof fieldObj === 'string') {
      fieldObj = { key: fieldObj };
    }

    // Only fields with a key get defaults from the schema. But there are lot's of other fields
    // that don't need it, like buttons etc.
    if (fieldObj.key) {
      var defaultFieldObj = lookup[fieldObj];

      // Copy defaults, unless the user already has set that property.
      // This gives us title from the schema, a reference to the actual schema etc
      angular.forEach(defaultFieldObj, function(value, attr) {
        if (fieldObj[attr] === undefined) {
          fieldObj[attr] = defaultFieldObj[attr];
        }
      });
    }
    canonical.push(fieldObj);
  });

  return canonical;
}

Form keys and the lookup

One key part here is the lookup object. It maps from the form key (in string form), to the default options from the schema.

Small digression into key format

Keys are in their simple form "dot notated", i.e. as you would wite in javascript. For example 'user.address.street'. To support keys that have hyphen, period, etc (AKA complex keys), the sfPath service uses ObjectPath lib (https://github.com/mike-marcacci/objectpath) to transform it into a form with brackets. So 'user.address.street' becomes '["user"]["address"]["street"]'.

Keys in lookup

Keys in the lookup mapping object are built while recursively traversing the schema. This has several ramifications

You might have realized that we have a problem here: $ref. To properly support $ref's in the schema we can't keep merging like this. The problem is that $ref's can make a schema circular and therefor we end up in an infinite loop!

This means we need to not create a default form definition and instead only look up in the schema each part as we loop over and normalize the user supplied form definition.

(At the same time we probably should rewrite it in vanilla js so it can be reused outside of angular.)

Clone this wiki locally