Exposed variables

In the previous sections, you learned how to create highly dynamic UI behavior by using JavaScript expressions in the component property configurations. You also learned how to write actions, and how to define state variables to encapsulate computations and maintain application state in a single place. Such pieces of code are however only as useful as the variables, functions and libraries that you have available. In this section, you will learn more about the exposed variables and objects that you have at your disposal in any place you can use JavaScript in.

UI components

Every component, that exists anywhere on the canvas, is available in JavaScript code as variable with the same name. These variables reference to objects that expose a selected list of getters and setters for their particular properties. These component objects do not expose every single property that is available in the properties panel, but only a selective list that are considered most useful.

For example, a TextInput component exposes a getter for the value in the input called value, and a setter called setValue to change the input value.

// Using the input value
const inputValue = myInput.value;
console.log(`Input value: ${inputValue}`);

// Changing the input value
myInput.setValue('');

Please check out the component reference to see a full list of all exposed properties and functions of each component type.

Actions

Equivalent to components, every action of an app is exposed as variable with the same name in any JavaScript snippet. These action objects allow to trigger the underlying action and expose properties of its most recent execution:

interface Action {
    // Internal action type name, for example 'JS'
    readonly type: string;
    // Name of the action, as managed by the user in the action editor
    readonly name: string;
    // Whether the most recent execution was successful
    readonly success?: boolean;
    // Error message of the most recent execution in case it was not successful.
    // null otherwise.
    readonly error?: string | null;
    // Execution time in milliseconds of the most recent execution,
    // regardless of success
    readonly executionTime?: number;
    // Returned data of the most recent execution, after applying js-transformations
    readonly data?: any;
    // Like data, but before applying js-transformations
    // Does not apply to JS actions
    readonly dataBeforeTransform?: any;
    // Runs the action and returns a promise that resolves to its return value,
    // or is rejected in case the action run fails due to an error
    readonly trigger(value?: { args: any }): Promise<any>;
}

success, error, executionTime, data and dataBeforeTransform will be undefined in case the particular action has not been executed yet.

Please refer to the actions section to learn more about actions, their execution and how to pass arguments into an action run.

State variables

All state variables and computed values are exposed in code via the state object. This object contains a property for each state variable and computed value, with the same name as those entities are defined. These properties will provide the current value of the particular variable. On top of that, it offers a setter for for each state variable to mutate its value. These setters are named by pattern state.set{variable-name}, e.g. setProductName for a state variable called productName:

Computed values do not need a setter, because their value should always be the outcome of the provided JavaScript snippet. Please refer to the state variables section to learn more about state variables and computed values.

Renaming entities

Since every component, action and state variable is exposed as variable in JavaScript snippets by its name, you might wonder whether renaming such entities will break your logic. Fortunately, the app editor takes care of such renaming operations automatically and updates all usages as well. You can see this demonstrated in the following example:

You can rename any entity with the confidence that no JavaScript snippet in properties, actions or state variables will break due to that operation.

Deleting entities

The user may delete a component, action or state variable at any moment. That deleted entity may however be used in code snippets anywhere across the application. Unfortunately, deleting such entity anyway will break all usages of the related variable in JavaScript code snippets. The user is notified about existing usages to prevent accidentally breaking the application, this might however be ignored and she might proceed with deleting anyway.

In that case, existing usages will be replaced with a question mark (?), which will lead to runtime errors when these code snippets will be evaluated:

In case entities with usages are deleted despite the provided protections, there is no alternative to checking all snippets in the application for broken code. Fortunately, undoing the harmful deletion will fix all usages automatically.

Authenticated user

The currently authenticated user is exposed as a variable called currentUser. It provides basic details about the user:

interface CurrentUser {
    readonly id: string;
    readonly email: string;
    readonly organization: string;
    readonly organizationId: number;
    readonly firstName: string;
    readonly lastName: string;
    readonly initials: string;
    readonly photoName?: string;
    readonly role: 'admin' | 'editor' | 'viewer';
    readonly createdAt: string;
    readonly updatedAt: string;
    readonly active: boolean;
}

You can use this object like any other variable in your JavaScript snippets:

Libraries

The open-source community provides many useful libraries to solve common problems in software engineering. While developing applications in Uify, you may benefit from the following libraries by default:

nanoid: Create unique IDs with the default behavior or a custom alphabet and length
// Default ID with 21 characters
const id = nanoid();
// ID with a custom length of 12
const id = nanoid(12);
// ID with a custom alphabet
const id = nanoid.customAlphabet('1234567890abcdef', 10);

date-fns: Selected functions for dealing with dates
interface DateFns {
    isPast(date: Date | number): boolean;
    isFuture(date: Date | number): boolean;
    isAfter(date: Date | number, dateToCompare: Date | number): boolean;
    isBefore(date: Date | number, dateToCompare: Date | number): boolean;
    isDate(value: any): boolean;
    isValid(date: any): boolean;
    endOfWeek(date: Date | number, options: { weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 }): Date;
    format(
        date: Date | number,
        format: string,
        options?: {
            weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
            firstWeekContainsDate?: number;
            useAdditionalWeekYearTokens?: boolean;
            useAdditionalDayOfYearTokens?: boolean;
        },
    ): string;
    add(date: Date | number, duration: Duration): Date;
    addBusinessDays(date: Date | number, amount: number): Date;
    addDays(date: Date | number, amount: number): Date;
    addHours(date: Date | number, amount: number): Date;
    addISOWeekYears(date: Date | number, amount: number): Date;
    addMilliseconds(date: Date | number, amount: number): Date;
    addMinutes(date: Date | number, amount: number): Date;
    addMonths(date: Date | number, amount: number): Date;
    addQuarters(date: Date | number, amount: number): Date;
    addSeconds(date: Date | number, amount: number): Date;
    addWeeks(date: Date | number, amount: number): Date;
    addYears(date: Date | number, amount: number): Date;
    differenceInMilliseconds(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInSeconds(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInMinutes(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInHours(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInDays(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInCalendarWeeks(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInWeeks(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInCalendarISOWeeks(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInCalendarMonths(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInMonths(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInCalendarQuarters(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInQuarters(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInCalendarYears(dateLeft: Date | number, dateRight: Date | number): number;
    differenceInYears(dateLeft: Date | number, dateRight: Date | number): number;
    parse(
        dateString: string,
        format: string,
        referenceDate: Date | number,
        options?: {
            weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
            firstWeekContainsDate?: number;
            useAdditionalWeekYearTokens?: boolean;
            useAdditionalDayOfYearTokens?: boolean;
        },
    );
    parseISO(
        argument: string,
        options?: {
            additionalDigits?: 0 | 1 | 2;
        },
    ): Date;
    startOfWeek(date: Date | number, options: { weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 }): Date;
    sub(date: Date | number, duration: Duration): Date;
    subBusinessDays(date: Date | number, amount: number): Date;
    subDays(date: Date | number, amount: number): Date;
    subHours(date: Date | number, amount: number): Date;
    subISOWeekYears(date: Date | number, amount: number): Date;
    subMilliseconds(date: Date | number, amount: number): Date;
    subMinutes(date: Date | number, amount: number): Date;
    subMonths(date: Date | number, amount: number): Date;
    subQuarters(date: Date | number, amount: number): Date;
    subSeconds(date: Date | number, amount: number): Date;
    subWeeks(date: Date | number, amount: number): Date;
    subYears(date: Date | number, amount: number): Date;
};

All these functions can be accessed via the dateFns variable in any code snippet:

const inputDate = dateFns.parseISO(myInput.value);
const threeDaysLater = dateFns.addDays(inputDate, 3);

We are aware that this restricted set of default libraries is not at all sufficient to address all possible use cases and that there are other essential libraries out there (e.g. lodash, validator, papaparse, ...). The ability to explicitly load and use any npm package in any app is on the feature roadmap and will hopefully be released in the near future.

Inbox notifications

The Inbox is an essential part of the workspace. You may send notifications programmatically to any member of your workspace to, for example, notify about events, request an approval or communicate errors. This is possible by using the sendNotification function of the exposed inbox object. It currently has a fairly basic signature:

interface Inbox {
    readonly sendNotification: (
        recipientEmail: string,
        message: string,
        destinationAppId?: string
    ) => Promise<void>
}

The recipient will receive the notification to her or his inbox immediately. When clicking on this notification, the recipient will be navigated to the destination app. If no destinationAppId was provided, the destination app will be the same from which the notification was sent. Otherwise, the user will be navigated to the app with the provided destinationAppId. In case a notification was sent from edit-mode, the user will end up in edit mode as well when clicking on the notification. This makes it easier to test programmatic notifications while building a new application before releasing it to production.

Sending a notification is an asynchronous operation, which is why it returns a Promise. Due to that, the sendNotification function is usually called with await:

await inbox.sendNotification(
    'jane.doe@corp.com',
    'A new client has signed up and requires your approval'
);

Trying to send a notification to an email that is not part of your workspace, or using a destinationAppId that does not exist, will result in an error. In case you await the result as shown above, this means that the action will fail. Otherwise, the action will succeed without error, which usually is not what you want.

Console

The browser console is a useful tool for ad-hoc debugging during application development. This is possible with the object exposed as console variable, which allows you to log messages to the console:

On-screen notifications

You may want to display notifications to the user to communicate successful completion of an operation or to issue a warning or error message. This is possible by using the notify object, which exposes different functions to dispatch notifications with different severity. The amount of time, in milliseconds, for which the particular notification will be displayed must be provided as second argument:

Sleep

In JS actions, you can access the sleep function, which returns a Promise that will resolve in the given amount of milliseconds. Usually, this function is triggered with await, to delay further execution of an action. The signature of this function is as follows:

function sleep(ms: number): Promise<void>;

Exposed functions in component properties

The previous sections presented many different functions that are exposed to the user in JavaScript snippets. Some examples of such functions are:

  • Setters for component properties

  • Triggering an action via the trigger function

  • Changing the value of a state variable via the state.setVariableName function

  • Showing an error notification via the notify.error function

In theory, these exposed functions may be used in the code that you may place in js properties, or that you embedded in stringWithJs properties, in the properties panel of a component. Due to the reactive nature of these properties, these code snippets will be executed whenever the runtime sees a necessity for such re-evaluation, including all associated effects like running an action, changing a component property value or displaying a notification. This might be not exactly deterministic, or may change over time.

For that reason, no functions of the exposed objects are available in such code snippets of component properties. The operations which are performed in such code snippets should be limited to reading values. This implies, that exposed objects that exclusively offer functions (like console or inbox) are not available at all for code snippets in the properties panel.

Last updated