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
index.yml
...
/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: "https://zenoo.onboardapp.io/api"
flowName: "zenoo"
favicon: "/assets/favicon.ico"
indexPageInit: true
mockData: !include ./mockdata.json
styles:
- !include ./styles/index.less
analytics:
gtm: "GTM-ID001"
authorizationTimeout: 60
translates:
en: !include ./translates/en.yml
cz: !include ./translates/cz.yml
defaultLocale: "en"
studioSettings:
name: ZenooBank
logo: /assets/logo.png
country: Mexico
previewUrl: https://onboarding.zenoo.com/
pages:
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
components:
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
schema:
required:
- code
properties:
code:
type: string
minLength: 4
maxLength: 4
errorMessage:
_: "{field} - Required field"
defaults:
mobile: !expression "flow.export.mobile"
items:
- !ref components: formLayout
items:
- !ref components: header
progress: \%-((3 / 8) * 100)\%
- !component as: div
className: "content main-content"
items:
- !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
items:
- !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"
---
pages:
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.
Analytics
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
analytics:
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
analytics:
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:
fields:
firstName:
# String annotation
change: "firstNameChanged"
middleName:
# Object annotation
change:
eventName: "middleNameChanged"
data:
page: !expression "page.name"
device: !expression: "device.deviceType"
lastName:
# 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:
analytics:
fields:
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.
Example:
This will sends event with name Click with parameter count: 1 for first call, 2 for second call, etc.
!function "analytics.event('Click', { count: analytics.storage.increment('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: analytics.storage.get('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.
Example:
# index.yml
analyticsParams:
ip: !expression "flow.export.ip"
page: !expression "page.name"
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
formats:
date:
format: "DD/MM/YYYY"
number:
decimalSeparator: "."
thousandsSeparator: ","
precision: 2
currency:
format: "%u%n"
unit: "£"
phone:
countryCode: "+44"
mask: "9999 999999"
Global application state and methods
Expressions
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
property:
!expression eval: flow.export.value
default: Nothing
# Multiline expression
property:
!expression: |
const variable = 1;
// More lines of JavaScript
console.log('Hello', variable);
Functions
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
globals:
test: "I am a global variable"
utils:
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: {
... // https://github.com/duskload/react-device-detect#selectors
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 `flow.function.search('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 `flow.function.results.search`)
}
}
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-inside-field, // - 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, // https://github.com/iamkun/dayjs
getFileHolder: (file: File | Blob) => Promise // Get FileHolder compatible with HUB client
}
- locals - local page variables and functions
# page.yml
locals:
test: "I am a local variable"
sum: !expression "function (a, b) { return a + b; }"
items:
- !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: {
... // - https://github.com/unshiftio/url-parse
}
- utils - custom methods, see more on how to extend expression context
Localization
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"
translates:
en: !include ./translates/en.yml
Translations can be stored under nested keys, e.g.
# translates/en.yml
welcome:
text: "Automated real-time identity authentication & decisioning."
button: "Lets get started"
otp:
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
welcome:
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.
Examples:
# 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:
welcome:
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"
# HTML
- !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
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.
VisibilityWrapper
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.
Properties:
visible?: boolean;
invisible?: boolean;
Trigger
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.
Example
- !component as: Trigger
value: !expression form.data.mobile
load: !function form.changeValue('otherFieldName', '08123456789')
change: !function console.log("Data was changed", oldValue, newValue)
Properties
value: any;
load?: (value: any) => void;
change?: (oldValue: any, newValue: any) => void;
hysterezis?: number; // timeout for on change callback
Script
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.
Properties
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:
head.ejs
< script >
function startApplication() {
if (!window.runApplication) {
window.onApplicationPrepared = function() {
window.runApplication();
}
} else {
window.runApplication();
}
}
(function() {
window.DISABLE_AUTOLOAD = true;
// Performing some request needed
return fetch('https://example.com')
.then(response => response.json())
.then(data => {
// Make something with data, e.g. put to global variables
startApplication();
})
.catch(() => {
startApplication();
});
})();
< /script >