UI Code

Skuid’s default components can be used to great effect to build UX-focused applications. However, it’s not uncommon to have truly unique needs that these default components may not cover. In instances like these, extending your available UI options with custom code can be helpful.

However, before diving in to the customization options available in Skuid, it’s helpful to know some basic concepts.

Rendering strategies

Web pages are collections of HTML elements, and these elements are often dynamically updated, or even generated, through JavaScript. This dynamic UI pattern is an essential concept in Skuid, as UI elements often must update to reflect model data. Since that dynamic rendering can happen a few different ways, let’s cover the two basic strategies most relevant to Skuid.

Direct DOM Manipulation

The document object model (aka the DOM) provides a way to structure and interact with pages of HTML or XML content. And thus, through direct manipulation of the DOM through JavaScript, it’s possible to change the appearance of the page. Within Skuid, this usually happens via APIs like document.createElement or through libraries like jQuery (accessible through the Skuid API).

Direct DOM manipulation is a common pattern in web development, as well as earlier versions of Skuid components. You can still utilize it when building a custom field renderer using the element strategy, but using virtual rendering instead is considered best practice.

Virtual rendering

Virtual rendering in Skuid refers to the product’s use of the virtual DOM. This rendering paradigm determines the virtual representation of the DOM, keeps track of data changes, and updates the DOM dynamically only when necessary. This paradigm was popularized by the React framework, but sees common use among other UI frameworks as well.

Virtual rendering is the basis for all v2 components, and writing custom UI code utilizing it is considered best practice.

Skuid and maquette

Maquette is the JavaScript library used by Skuid’s v2 API to render and rerender HyperScript syntax using the virtual DOM. So how does this work?

Maquette’s projector processes HyperScript syntax to render DOM nodes when there are differences between the virtual DOM and the actual DOM—resulting in more efficient render times since they only occur as needed.

A more technical breakdown of the projector’s processes can be seen in maquette documentation.

HyperScript Syntax

Skuid’s implementation of the virtual DOM requires the use of HyperScript syntax. We recommend becoming very familiar with its syntax. For new users, Maquette’s HyperScript tutorials provide an interactive introduction.

This syntax involves invoking a HyperScript function, also known as an h() function, and passing in three parameters:

  • A string of the HTML element type to generate
  • An object of key-value pairs representing the HTML attributes of the element
  • An array of child nodes

This function call then returns a virtual DOM node. Skuid and maquette can then track the virtual DOM and render DOM elements appropriately from there.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
h("div#an-id.class1.class2",
  {
    htmlElementAttributes: "These are listed as key object pairs"
  },
  [
    "Child nodes are stored in this array",
    "They can be strings or...",
    h("a", { href: "www.example.com" }, ["... other HyperScript functions!"]),
    orVariablesAsLongAsTheyAreStringsOrHyperScriptInstances
  ]
);

This may be different than the way you are used to working with elements, so let’s compare two different ways to create an div node:

1
<div style="color:red">This is a div node.</div>

First, we’ll create an actual element using standard web APIs:

1
2
3
let divElement = document.createElement('div');
divElement.setAttribute("style","color:red");
divElement.innerText = "This is a div node.";

And now let’s compare it to an equivalent element created via Hyperscript:

1
2
3
4
5
6
7
8
h("div",
  {
    style: {
      color: "red"
    }
  },
  [ "This is a div node." ]
);

The ability to use variables in these HyperScript constructs is key to the construction of v2 UI elements in Skuid. Additionally, the ability to handle events is crucial for updating the component in response to user interaction.

Ensure child nodes are distinguishable

If creating a VNode that will have multiple nodes, then each node must have a unique key. This is one of maquette’s “two rules” to ensure proper rendering and performance.

For example, this rule applies if the function returns multiple VNode elements based on the context, like this field renderer read function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
read: function (fieldComponent) {
  let h = fieldComponent.h,
    context = fieldComponent.cpi.getContext();
  const codes = context.row.code_item_id_foreign.records.map(item => {
    return h("a",
      {
        key: item.id // This key value must be unique.
        styles: {},
        href: "/catalog/codes/" + item.id,
      },
      []
    );
  });
}

This rule also applies to the child nodes within the HyperScript child node array, per this example from maquette’s documentation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
h("ul",
  [
    items.map(function(item) {
      return h("li",
        { key: item.id },
        [ item.text ]
      );
    })
  ]
);

HyperScript style

While HyperScript written in functional syntax will render in Skuid, it can be helpful to add a line break before each parameter in the function and nest appropriately.

Because HyperScript functions accept objects and arrays as parameters, often with many nested elements for complex implementations, telling where different parts of a HyperScript function begin and end can be problematic.

Compare this more standard styling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
h("div", {
    style: {
      color: "red"
    }
  },
  [
    "This is a div.",
    h("input", {
      type: "text",
      placeholder: "This is an input field within that div."
    })
  ]
);

To this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
h("div",
  {
    style: {
      color: "red"
    }
  },
  [
    "This is a div.",
    h('input',
      {
        type: 'text',
        placeholder: "This is an input field within that div."
      }
    )
  ]
);

While the first example is more akin to typical JavaScript styling, it can be more difficult to read. Because the attribute object’s bracket is on the same line as the start of the function (line 1), the indentation of its closing bracket can appear to be the end of the statement (line 4) since the relationship between the brackets isn’t as clear.

Contrast this with the second example, where each bracket and parenthesis has the same level of indentation. By being more verbose with line breaks and indentation, the beginning and end of each parameter is clearer, making the function as a whole more readable.

Important APIs and Functions

While powerful, building custom UI elements requires a nuanced understanding of Skuid’s own APIs and libraries. You’ll often need to code behaviors that are pre-baked into Skuid’s own component set. We recommend familiarizing yourself with the tools listed in the sections below.

Working with the DOM

  • jQuery: Skuid’s v1 components heavily utilized jQuery, and jQuery 3 is accessible from via skuid.$. While virtual rendering is best practice, you’ll likely use jQuery often if working with the element rendering strategy, and you may even use it with virtually rendered elements as well.
  • Maquette and its VNode afterCreate() attribute: Maquette, the library Skuid uses for its implementation of virtual rendering, provides a function that allows for adjustments to be made to the DOM element after it’s been rendered virtually. If you’re having trouble with a virtually rendered element, check if adding logic to its afterCreate() function solves the problem.

Working with data

One of the most important structural features in Skuid is the connection of the UI element to which ever model it represents. While some UI elements are only meant to illustrate data, and changes to model data will require some API know-how.

  • Models: You’ll often be working with modes through context. For example, fieldComponent.cpi.getContext() returns a skuid.model.Model object, which is the model that the component is attached to. Because of this, all of the model’s properties and APIs are available for use within the custom UI element. If you need to access data from a different model, try using the skuid.model.getModel() API.
  • Fields: Fields within Skuid have a certain attributes. When writing field renderers, you may need to utilize that field data. For more information, see Skuid’s field metadata object documentation.

Event publishing

Skuid UI elements communicate through an event publishing system. By publishing and subscribing to events, it’s possible to have custom UI elements that interface with each other, including Skuid’s own component set.

For more information, see the Skuid events reference, as well as the API docs for skuid.events.subscribe() and skuid.events.publish()

Generating unique IDs

Working with DOM elements can require unique IDs, either for DOM elements or data records. The skuid.utils.generateUniqueId() API creates a globally unique ID in the context of the current Skuid page.

Dynamic logic

When working with UI elements, it may be necessary to dynamically create models or components or other XML nodes. This can be done using skuid.utils.makeXMLDoc.

Component code and CPIs

v2 components store their functions within their component programming interface, commonly shortened to CPI in general use or cpi when referring to the component property. You can think of CPIs as the component-specific JavaScript APIs. Field renderers, for example, utilize the fieldComponent, with it’s most useful CPI being getContext().