Target configuration

HUB Client

Target structure

HUB Client Target should have the following structure, which can vary depending on complexity.

/src — folder with target source code
  /assets — static assets (fonts, images etc)
  /components — YAML reusable components
  /layouts — visual layouts
  /pages — configuration files for specific pages. By convention, names of these files should be the same as route names in flow
  /styles — LESS styles
    index.less — list of imports used in target and global styles
    fonts.less — font styles (or import from CDN)
    overstyle.less — style overrides for UI components
    variables.less — CSS variables for UI components theming
    studio-variables.less — CSS variables for Design Studio
  /translates — translations for target
    {LANG}.yml — list of translations for {LANG} locale
  index.yml — project configuration file
package.json — metadata information about the target and its dependencies
package-lock.json — dependencies tree with locked versions

Project settings

Project settings can be set in the root index.yml file. The values for these settings can be individually set for any particular environment. (Environment-specific entry points)

List of available parameters

Parameter Required Default Value Description
analytics false Analytics configuration
analyticsMapper false Analytics configuration
analyticsParams false Analytics configuration
apiVersion false 'v0' API version ('v0' or 'v1')
authorizationTimeout false 10 Authorization cookie expiration timeout (in minutes)
backDisabledAlert false Message to be displayed in case of disabled back action
coreLocale false List of translates for Core messages (more in localization)
defaultLocale false Default locale code (more in localization)
devTools false true Toggles developer tools (open with ctrl + shift + D hotkey)
errorPage false Error page configuration
favicon false Path to favicon
flowExport false Mocked flow export
flowIdName false Flow ID name in Backend instance
flowIdRevision false Flow ID revision in Backend instance
flowName true Flow name in Backend instance
flowStartParameters false List of parameter to be passed to flowStart action
formats false Global formats settings
globals false Extending expression context
indexPageInit false true Specifies if application initialization should start from flowStart action
loadingComponent false Component to be didsplayed during application initialization
mockData false Mocked input data for development tools
og false List of meta og tags
pages true Page Configuration
serverUrl true URL of Backend instance server
studioSettings false Studio settings
styles false LESS files includes
title true Meta title of an application, shown in browser tab header
translates false List of translates for specific languages (more in localization)
url false Application URL settings

Application URL settings

url: {
  persistHash?: boolean // defines if hash should be persistent on page change, default value is TRUE (default hash is page URI)
  persistQuery?: boolean // defines if query should be persistent on page change, default value is FALSE
  persistPathname?: boolean // defines if pathname should be persistent on page change, default value is FALSE

Example configuration

Here's an example of various settings in index.yml:

title: "Zenoo Demo Project"
serverUrl: ""
flowName: "zenoo"
favicon: "/assets/favicon.ico"
indexPageInit: true
mockData: !include ./mockdata.json
  - !include ./styles/index.less
  gtm: "GTM-ID001"
authorizationTimeout: 60
  en: !include ./translates/en.yml
  cz: !include ./translates/cz.yml
defaultLocale: "en"
  name: ZenooBank
  logo: /assets/logo.png
  country: Mexico
  index: !include ./pages/index.yml
  otp: !include ./pages/otp.yml
  loan-overview: !include ./pages/loan-overview.yml
  thanks: !include ./pages/thanks.yml
  rejected: !include ./pages/rejected.yml

Page settings

The entire application consists of pages. Each view that is presentable to a user must be implemented as page. There are two predefined user positions, the index page and the error page. The index must be inside index property in pages. In root of your yaml (typically index.yml), you can specify the value of the property errorPage. This property is name of page to which the user will be redirected when an error occurs (such as a network failure).

List of available parameters

Parameter Required Description
analytics false Analytics configuration for specific page
defaultAction false Default form submit action name
defaultActionParams false Default form submit action params
defaults false Default values for form fields
fadeAnimationBack false Use "fade" animation on back action
fadeAnimationSubmit false Use "fade" animation on submit action
items false Elements tree of specific page
og true List of meta og tags (will be merged with the ones coming from project configuration)
schema false Validation rules as a JSON schema
title false Page meta title

Example configuration

  formLayout: !include @common/layouts/form-layout.yml
  formGroup: !include @common/components/form-group.yml
  header: !include @common/components/header.yml
  pinInput: !include @common/components/pin-input.yml
fadeAnimationBack: true
    - code
      type: string
      minLength: 4
      maxLength: 4
    _: "{field} - Required field"
  mobile: !expression ""
  - !ref components: formLayout
      - !ref components: header
        progress:  \%-((3 / 8) * 100)\% 
      - !component as: div
        className: "content main-content"
          - !component as: h1
            items: "Enter your phone number"
          - !component as: p
            items: "Please enter a valid mobile phone number to where we can text a confirmation code to."
          - !ref components: formGroup
              - !ref components: pinInput
                field: code
                label: "Enter your confirmation code"
                length: 4

Error page

The error page can be specified as an errorPage parameter in application configuration.

errorPage: "error-page"
  error-page: !include ./pages/error.yml # Include error page to a list of pages

If an error page is not specified, the auth cookie will be deleted and application will be reloaded.

You can create more dynamic error page that provides useful features, such as a button to continue or reattempt the previous action (flowContinue). This button will automaticaly fetch the last stored data from the server and redirect user to correct screen.

Another useful error management feature is to provide a button that reloads the flow. If the problem is not easily resolved, have the user click a button to redirect to start of the flow in case with the form action flowReload.

If you are on error page, there are also available page parameters that contain the reason for the error. For example, query the value of page.params.error to get the raw output from the error catch.


You are able to initialize different analytics providers when application starts and call specific action when certain event occurs.

List of currently available providers:

  • ga — Google Analytics
  • gtm — Google Tag Manager
  • hotjar — HotJar ID
  • mixpanel — MixPanel ID

Example integration

# index.yml
  mixpanel: "a78gc206fb0a9d85edb622d10ec74b5d"
  gtm: "GTM-XXXXXX"

User identification

To identify current user for different analytics providers analytics.authorizationToken configuration key can be used, e.g.:

# index.yml
  authorizationToken: !expression "url.query.do_authorization"

In order to identify user not on initial page load, but by some event, analytics.authorization action from Expression context can be used:

- !component as: div
  onClick: !function "analytics.authorization(flow.export.identityId)"

Events management

Analytics events can be dispatched manually or using analytics event management.

By defining analytics in page configuration built-in analytics event management will be involved, some UI components are dispatching basic default events, e.g. form fields have click, change, blur, focus, etc.

Analytics page configuration

Analytics configuration structure is coresponding to an event you want to handle and can be placed on every page.

There are 3 ways you can set event configuration: "string", "object" or "function" annotation:

      # String annotation
      change: "firstNameChanged"
      # Object annotation
        eventName: "middleNameChanged"
          page: !expression ""
          device: !expression: "device.deviceType"
      # Function annotation
      change: !function "analytics.event('lastNameChanged')"

You can also define this event configuration inside of parent structure, for example this function will be triggered on any field change:

    change: !function "analytics.event('someFieldChanged')"
Existing events

Form fields events:

Event name Description
click Triggers when user clicks on field
change Triggers when user change value of field
focus Triggers when user focus on field
blur Triggers when user unfocus from field

File upload events:

Event name Description
click Triggers when user clicks on field
change Triggers when user change value of field
accepted File was accepted to field
rejected File was rejected, it can be caused by prevalidations or livness detection

Path for these events is in this format fields.{FieldName}.{EventName}.

Application lifecycle events

Path Event name Description
page enter Triggers when page is entered
page leave Triggers when page is leaved
form initialized Triggers when execution is initialized

Path for these events is in this format {Path}.{EventName}.

Analytics storage

Expression context has support for dispatching analytics events and for storing some values.

Analytics storage is a simple key/value storage, that can contain any value. It has some utils to make its usage simpler: for numeric values there are increment and get. increment will augment value by 1, if value does not exist, it will set it to 1.


This will sends event with name Click with parameter count: 1 for first call, 2 for second call, etc.

!function "analytics.event('Click', { count:'timesClicked') })

This will sends event with name Click with parameter count with value from storage. If this value does not exist, count will be set to 0 (default value).

!function "analytics.event('Click', { count:'timesClicked', 0) })

Global analytics params

There is a way to set global analytics params which will be sent with every single event. This injection works only when input params in event call is an object or was not provided. Global params has lower priority, so if you redefine same field in event params, it will overwrite it.


# index.yml
  ip: !expression "flow.export.ip"
  page: !expression ""

Dispatch events manually

To manually fire analytics event, use analytics.event method from expression context

- !component as: div
  onClick: !function "analytics.event(eventName, eventParams)"

Formats settings

Global formats should be defined under formats parameter. Later all formats are available as helpers in global application state (expression context)

Example configuration

    format: "DD/MM/YYYY"
    decimalSeparator: "."
    thousandsSeparator: ","
    precision: 2
    format: "%u%n"
    unit: "£"
    countryCode: "+44"
    mask: "9999 999999"

Global application state and methods


Expressions are a simple way to access data from the app runtime, or the response from server. Data is accessed through an object that is internally known as Core context or Expression context. If expression fails, it will return undefined. If you specify the default parameter, it will be returned when expression fails.

Examples of expressions:

property: !expression flow.export.value

  !expression eval: flow.export.value
  default: Nothing

# Multiline expression
  !expression: |
    const variable = 1;
    // More lines of JavaScript
    console.log('Hello', variable);


A function is another type of expression. It's useful to add some callbacks, such as a button onClick event.

- !component as: div
  onClick: !function "console.log('Click')"
  items: "Click me"

Extending Expression context

To extend expression context with custom values or methods, globals or utils configuration keys can be used:

# index.yml
  test: "I am a global variable"
  sum: !expression "function (a, b) { return a + b; }"

Then in page configuration:

- !component as: Heading
  items: !expression "globals.test"
- !component as: Heading
  items: !expression "utils.sum(1, 2)"

Expression context

Expression context is a global object, which is accessible from YAML expression only.

  • analytics - functions for trigger analytics events

analytics: {
  authorization: (token) => void, // Trigger mixpanel.identify(token), GA.set({ userId: token }) and GTM dataLayer event "authorization" with parameter token (string)
  event: (name: string, params?: object) => void, // Trigger event with given event name and params
  storage: { // More info in "Analytics storage" section
    set: (name: string, value: any) => void
    get: (name: string) => any
    increment: (name: string) => void
  • api - information about API

api: {
  authToken: string,
  progress: {
    [field-name]: number // Percentage of progress in file uploading
  • app - information about app

app: {
  locale: string, // Current locale
  targetId: string, // Current target name
  waiting: {
    [tag]: boolean, // App waiting tags
  wrapByLoading: (promise: Promise) => Promise

Example usage of wrapByLoading

# Element with click handler as async operation
- !component as: div
  items: "Run simple async operation"
  onClick: !function "app.wrapByLoading(simple_async_operation, 'SIMPLE_TAG')"

# Element with click handler as async operation with complex structure
- !component as: div
  items: "Run complex async operation"
  onClick: !function |
    app.wrapByLoading((async () => {
      await complex_async_operation();
    })(), 'COMPLEX_TAG')

# Displaying loader during async operation
- !component as: VisibilityWrapper
  visible: !expression "app.waiting.SIMPLE_TAG || app.waiting.COMPLEX_TAG"
  items: "Loading..."
  • configuration - complete configuration of your target in json. This is an output from target-builder module, which parses all files inside target folder and produces large JSON that contains all settings, configurations, pages structures, etc.
  • constants - list of constants, the most important ones are: COUNTRIES_FULL, COUNTRIES, COUNTRY_CODES, LANGUAGES
  • cookie - methods exported from js-cookie module (cookie.get, cookie.set, cookie.remove)
  • cx - method exported from classnames module
  • device - information about device

device: {
  ... //
  hasWebcam: boolean, // If device has webcamera physically
  hasWebcamPermission: boolean, // If user already granted webcamera permission to current website
  • flow - data from server about flow and flow/route functions from server

flow: {
  backEnabled: boolean, // value of backEnabled from API for current page
  execution: { // information about current flow execution
    uuid: string
    token: string
  export: ...any-data-from-server, // this is exported data for page in flow from server
  function: {
    [function-name]: (payload?: any, resultKey?: string) => void, // - call (in !fuction) any flow/route function by call function name (like `'something')`), you can also set output resultKey (default function-name)
    results: {
      [function-name or result-key]: ...any-data-from-server-function, // - here will be data from server under function-name or result-key property name (like ``)
  goToErrorPage: (message: string, logout?: boolean) => void // redirect user to error page (if one is specified) with some message put into `page.params.error`. Optionally logout can be performed
  refresh: () => void // refresh workflow based on current workflow status
  reload: () => void // removes authentication cookie and reloads flow
  • form - data about form, including states of fields

form: {
  changeValue: (fieldName: string, value: any, callback?: () => void) => void, // change value of some field, you can use callback that will be called after data set, for example if you need to submit form
  data: {
    [field-name]:, // - data can be string, file, etc.
  field: {
    [field-name]: {
      isValid: boolean,           // is field valid
      validationError: string,    // only validation erros generated by page schema
      error: string,              // all field errors including validation errors and server errors
      isFilled: boolean,          // is there any data
  recompileSchema: () => void, // recompile form validation schema
  addTags: (tags: string[]) => void, // add tags to form
  removeTags: (tags: string[]) => void, // regexp as string can be also used to identify more tags
  hasTags: (tags: string | string[]) => boolean // checks if all passed tags are present
  tag: {
    [tag-name]: boolean, // form visual tags
  submit: (actionName: string, params: string[]) => void, // submit form
  valid: boolean, // is form valid
  visited: {
    [field-name]: boolean // indicated if field was visited
  • format - global formats used in application

format: {
  formatDate: (date: string) => string
  formatCurrency: (value: number, options?: NumberFormat) => string
  formatNumber: (value: number, options?: NumberFormat) => string
  roundNumber: (value: number) => number
  dateFormat: string
  currencyUnit: string
  phoneCountryCode: string
  phoneMask: string
  • globals - custom constants/variables, see more on how to extend expression context
  • helper - Helper functions and 3rd party libraries

helper: {
  dayjs, //
  getFileHolder: (file: File | Blob) => Promise // Get FileHolder compatible with HUB client
  • locals - local page variables and functions

# page.yml
  test: "I am a local variable"
  sum: !expression "function (a, b) { return a + b; }"
- !component as: Heading
  items: !expression "globals.test"
- !component as: Heading
  items: !expression "globals.sum(1, 2)"
  • page - parameters of page, which may be an error. This data is set only from the local application, not the server

page: {
  params: any // for example page.params.error contains informations, why you are on error page
  name: string // current page name (route URI)
  storage: { // local page storage, gets cleared on page change
    get: (name: string, defaultValue?: any) => any
    set: (name: string, value: any) => void
  • translates - function to translate a string of text, change current locale

changeLocale: (locale: string) => void
t: (string, params) => string
te: (string, params) => string
  • url - information about locations, query params, etc.

url: {
  ... // -
  • utils - custom methods, see more on how to extend expression context


HUB Client has built-in support for multiple locales and an easy way to manage translations.

All translation keys are being stored in src/translates folder under appropriate YAML files: {LANG}.yml and should be described in index.yml project configuration file:

defaultLocale: "en"
  en: !include ./translates/en.yml

Translations can be stored under nested keys, e.g.

# translates/en.yml
  text: "Automated real-time identity authentication & decisioning."
  button: "Lets get started"

  title: "Enter your confirmation code"
  text: "We've sent a confirmation code to your phone number"


# Page configuraion
- !component as: Heading
  items: !t "welcome.text"
- !component as: SubmitButton
  text: !t "welcome.button"

In order to use translation key with some parameter, the following notation can be used:

# translates/en.yml
  text: "Some text with {param}"


# Page configuraion
!t text: "welcome.text"
param: "Zenoo"

# Expression can be used as well
!t text: "welcome.text"
param: !expression "flow.export.param"

There are two ways to use translations in YAML:

  • Use the !t function. It can value for any property in any object.
  • Use !expression and call the function t.

To change locale, use the action changeLocale, in which the first parameter is target locale name.


# Evaluate translation for given translation key
- !t translation_key

# Evaluate translation for dymanic translation key (e.g. error coming from Backend)
- !expression t(flow.export.translation_key)

Markdown and HTML content in translations

Translation key value can have string, HTML or Markdown as a value:

  string: "Welcome"
  text1: !html |

Welcome to our website

Please provide some information text2: !markdown | # Welcome to our **website** Please provide some information

In order to use Markdown/HTML you need to use !te tag instead of !t:

# String
- !component as: Paragraph
  items: !t "welcome.string"

# Markdown
- !component as: Paragraph
  items: !te "welcome.text1"

- !component as: Paragraph
  items: !te "welcome.text2"

Built in components

Using components in YAML page configuration

Each component must have an "as" parameter that specifies the component element name. You can use the provided component name, or the standard HTML DOM element.

Each component has also $reference property, which can create a named reference to DOM element. This reference is accessible through a $reference object inside appDataContext.

Examples of $reference:

# Referenceable div
!component as: div
$reference: myDiv

# Some component that uses this reference
property: !expression #reference.myDiv

UI components

List of UI components can be viewed in Zenoo Storybook.

Logical components


Fragment wrapper - this is a dummy component without any effect on the rendering. You can wrap a set of components into it to create a single component with children.


Wrapper for hiding and showing elements. Elements (items) inside it will be shown only if property visible is set to true, or property invisible is set to false.


visible?: boolean;
invisible?: boolean;

This component permits you to track changes on some value and retrieve that value with an expression. On loading (mounting) of this component, the load callback will be called with current value. If this value was changed, the change callback will be called with old and new values. Value variables are accessible in scope. For load function there is value, that referes to current value of the trigger. In a change callback, there is an oldValue and newValue-which contain the values before and after the change, respectively.


- !component as: Trigger
   value: !expression
   load: !function form.changeValue('otherFieldName', '08123456789')
   change: !function console.log("Data was changed", oldValue, newValue)


value: any;
load?: (value: any) => void;
change?: (oldValue: any, newValue: any) => void;
hysterezis?: number; // timeout for on change callback

A script component that runs a script, which is defined by the onEnter and onLeave property (use a !function expression). The onEnter script will be run immediately if the trigger property is not defined—or the value of the trigger property changes from false to true. The onLeave script will be run when a component unmounts (in case trigger has not been set), or when trigger changes from true to false.

You can also specify runOnUpdate and the script will run whenever trigger changes. If runOnUpdate is false or not set, both the onEnter and onLeave scripts will be run only once. You can also provide a URL to an external script.


attributes?: {[name: string]: string} // - html attributes, can by used only in combination with url
onCreate?: () => void
onEnter?: () => void
onError?: () => void
onLeave?: () => void
url?: string;

EJS partials

It is possible to extend initial HTML content of application. By creating/filling the following files in /ejs folder in target source you can extend content of head element and add HTML code at the beginning/end of body tag:

  • head.ejs — head element content
  • index.ejs — beginning of body element content
  • body.ejs — end of body element content

Remote application start

In order to "pause" application initialization prior to perform some asynchorous task, the following approach can be used with the help of EJS partials:


< script >
  function startApplication() {
    if (!window.runApplication) {
      window.onApplicationPrepared = function() {
    } else {

  (function() {
    window.DISABLE_AUTOLOAD = true;

    // Performing some request needed
    return fetch('')
      .then(response => response.json())
      .then(data => {
        // Make something with data, e.g. put to global variables

      .catch(() => {
< /script >