# Examples

In this article, you will go through some common use cases of Custom JS in your embedded solutions.

## Introduction

In this article, you will go through some common use cases of Custom JS in your embedded solutions. Alternatively, You can check out our [Custom JS repository](https://github.com/trayio/embedded-customjs-public) with use cases and walkthrough videos.
For examples 1, 2 and 3, we have set up an Embedded project which you can download and install to test yourself.
It is a data mapping integration between Salesforce and a fictional CRM called Acme:
![main-config-wizard-screen](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-b47fc695_main-config-wizard-screen.png)
Salesforce is a real world CRM while Acme is another mock CRM system which is a placeholder for the service you want to build a custom integration with.

## Prerequisites for Example 1-3

> **Info:** You have to do these steps if you want to see the examples 1,2 and 3 in action. You may skip this and navigate to standalone examples in Use Cases (standalone) section.

### Create Salesforce account (optional)

### Create Salesforce account

In order to test this solution end-to-end you will need to set up a [free Salesforce developer account](https://developer.salesforce.com/signup) (optional if you have one already) and follow [our documentation on how to set up your Salesforce permissions](https://tray.ai/documentation/connectors/service/salesforce/#authentication) to authenticate with Tray - taking particular notice of the section on the Salesforce trigger which says that you must a use a Salesforce 'permission set' with the the 'Modify all Data' and 'Customize Application' permissions.

### Set up the project

### Import the mock API workflow

### Import Acme API service workflow

Download and import [this workflow](#) (separate from your project). This workflow mocks the Acme API service and it will:

* respond with details of the Acme objects and their fields when requested by the Config Wizard
* create an object and respond with '200 - object created' when a new contact/account etc. has been created in Salesforce
  Note that you can change the fields associated with each Acme object as you please:
  ![change-acme-object-fields](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-d946aa49_change-acme-object-fields.png)
  You will also need to grab the webhook url for this workflow:
  ![acme-service-get-webhook-url](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-fd7cb179_acme-service-get-webhook-url.png)

### Edit webhook URLs

You will then need to edit the webhook urls in the following screens to match that of your Acme API service workflow:

### Screen 5

Click the pencil icon to edit the Custom JS for 'external\_destination\_object':
![screen-5-edit](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-40af156b_screen-5-edit.png)
Then:
![screen-5-edit-url](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-3fa9a52f_screen-5-edit-url.png)

### Screen 6

Click the pencil icon to edit the Custom JS for 'external\_acme-fields':
![screen-6-edit](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-505f0f72_screen-6-edit.png)
Then:
![screen-6-edit-url](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-57a1b2f3_screen-6-edit-url.png)

### Screen 7

Click the pencil icon to edit the Custom JS for 'external\_data\_mapping':
![screen-7-edit](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-50b90f9d_screen-7-edit.png)
Then:
![screen-7-edit-url](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-00bd65da_screen-7-edit-url.png)

### Test using the Embedded demo app

You will then need to:

1. Publish your solution
2. Test your solution either by [creating instance from the UI](https://tray.io/documentation/embedded/building-integrations/solution-instances/creating-instances/) or through the [marketplace app](https://tray.io/documentation/embedded/integration-walkthrough/integration-marketplace-app/).
3. Once you have created a Solution Instance for an End User test by adding a new contact/account etc. to Salesforce and you will then see a successful run for that End User:

### 1 - Select User

![embedded-user](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-cda91d4e_Advanced%20Config%20Wizard-Examples-Test%20using%20the%20Embedded%20demo%20app.png)

### 2 - view user's workflow instance

![debug-object-added](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-45deebb2_debug-object-added.png)

> **Info:** In all the Custom JS below, the headers, parameters, url etc. entered are specific to the mock Acme API service we have created. When using your own service, you will need to adjust these accordingly

## Examples (using ACME service)

The examples shown below can also be found at our Github repository.

### 1 - Generating dropdown lists

Normally when you want to display a dynamic list in the config wizard, this is quite intuitive - you create the config slot on a field that is a dropdown list in the workflow (see [Config and drop-down lists](https://tray.ai/documentation/platform/embedded/key-concepts/config-slots)).
**However, this only works when we have the correct connector operations and dropdown lists**.
There may be times when you want to define a **new list**. You can achieve this using custom javascript.
In this example, when pushing records to Acme, we want to prompt the end user to select what object type they want to push new Salesforce records to:
![acme-object-drop-down-list](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-ff1e7f1e_acme-object-drop-down-list.png)
This is done with the following steps:

### 1 - Set object type as a config variable

In the solution workflow, in step http-client-1 you can see that in the payload to create the record in Acme, there is a body parameter called **destination\_object**:
The value of this has been set to a config variable:
![destination-object-config-variable](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-bbf275e4_destination-object-config-variable.png)

### 2 - Check config slot

In turn this has generated a config slot in our solution with external ID **external\_destination\_object** on screen 4:
![external-destination-object-v2](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-6198dd10_external-destination-object-v2.png)

### 3a - Edit Custom JS

The Acme API has an endpoint to query what objects are available to write to (GET /objects ).
Using custom javascript we can define a call to the Http Client, using the callConnector function, that then calls the Acme API endpoint.
In this solution, the slot is on its own screen so we can simply listen to the CONFIG\_SLOT\_MOUNT event. The key to showing a dropdown is specifying the enum property in the JSON schema, which tells the string field that it can take one of the values specified in the list.
You can edit the code which renders the slot by clicking on the pencil icon:
![destination-object-edit-slot](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-00c19fe7_destination-object-edit-slot.png)
The code editor will then open:
![acme-destination-object-code-editor](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-d80d79b8_acme-destination-object-code-editor.png)

### 3b - Code breakdown

### CONFIG\_SLOT\_MOUNT

We use the CONFIG\_SLOT\_MOUNT event to load the dropdown schema when the screen has loaded.
The only properties we need to amend are:

* status
* jsonSchema
  So we assign these to the existing slot state in **event.data**:

```js
tray.on('CONFIG_SLOT_MOUNT', async(\{
  event, previousWizardState
\}) => ({
  ...event.data,
    status: 'VISIBLE',
    jsonSchema: await getJsonSchema({
      previousWizardState
    }),
}));
```

### Call the Connector API

When calling the connector (http client in this case as there is no pre-built 'Acme' connector) we have to specify:

* connector
* version
* operation
  And we also:
* use `authId: previousWizardState.values.external_acme_authentication` to grab the `authId` from the auth slot.
* interpolate the API key into the request using `{$.auth.api_key}`
* specify the correct url for the Acme API service (edit this to match the public url of your Acme service workflow)

> **Warning:** When connecting to an actual API service, you will need to adjust all of this according to the exact headers, parameters etc. required by the service

```js
async function getJsonSchema({
  previousWizardState
}) {
  // use the Http Client connector to call the Acme API
  const response = await tray.callConnector({
      connector: 'http-client',
      version: '5.5',
      operation: 'get_request',
      // grab the authId from the Acme API auth slot
      authId: previousWizardState.values.external_acme_authentication,
      input: {
        "status_code": {
          "range": \{
            "from": 200,
            "to": 299
          \}
        },
        "follow_redirect": false,
        "case_sensitive_headers": false,
        "auth": {
          "basic_auth_username": {
            // interpolate the Acme API key into the request
            "username": "{$.auth.api_key}"
          }
        },
        "parse_response": "true",
        "follow_keep_method": false,
        "follow_authorization_header": false,
        "url": "https://6d7f402d-xxx-xxx-xxx-d759ff1b0d19.trayapp.io/objects",
        "reject_unauthorized": true
      }
    })
```

### Parse response

The 'enum' property renders the slot as a dropdown.
enum' takes an array of objects with properties 'text' and 'value'.

* 'text' defines the human-readable value to show to the user in the config wizard dropdown
* 'value' defines the underlying value to set for the config slot which is then used in the workflow
  If 'text' and 'value' are the same then instead of specifying the enum value as an object \&#123; text, value \&#125; you can just specify the va e.g. enum: \[ 1, 2, 3 ] will render a dropdown list where the user can pick from the 3 options 1, 2 or 3.
  This is identical to specifying enum: \[ \&#123; text: 1, value: 1 \&#125;, ... ] in this scenario

```js
.then(response => JSON.parse(JSON.parse(response).response.body));
  return {
    // the allowed values for this slot are of type 'string'
    type: 'string',
    enum: response.results.map((\{
      name, typename
    \}) => (\{
      text: name,
      value: typename
    \}))
  };
}
```

### 2 - Customising the data mapping component

As in the example of generating dropdown lists, there are times when you might want to use the [Data Mapping component](https://tray.io/documentation/embedded/advanced-topics/data-mapping/intro/) with [dynamic lookups](https://tray.ai/documentation/platform/embedded/advanced-features/data-mapping/introduction) but we don't support the connector operation you need to set this up.
In this case when an End User has selected e.g. 'Contacts' as the Acme object to sync to in step 1 above, we want the available fields to dynamically respond to the fields available for a 'contact'.
On the **right hand side**, we want the user to be able to **select from the list of available Acme fields for the object they have chosen**:
![sfdc-to-acme-data-mapping-config-wizard](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-53125160_sfdc-to-acme-data-mapping-config-wizard.png)
In order to achieve the above functionality, we do the following:

### 1 - Create 'acme-fields' and 'simple-data-mapping' config slots

An '**array**' type config slot for '**acme-fields**' and an '**object**' type config slot for '**simple-data-mapping**' are added to the **Configuration slots (script-1)** step in the workflow:
![script-1-acme-fields-v2](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-24510278_script-1-acme-fields-v2.png)
Note that, although these configuration slots are **not needed logically in the workflow**, since solutions **inherit config slots from the workflows**, we must create the config values somewhere in the workflow first.

### 2 - write custom JS for 'external\_acme-fields' slot

In order for this to work, we must write javascript code in the slot 'external\_acme-fields' to output a value, when the screen loads, that will feed into the data mapping:
![acme-fields-edit-custom-js](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-d7b89b32_Untitled.png)
Two key parts of the code here are:

* `const acmeObject = previousWizardState.values.external_destination_object;`
* `"url": 'https://6d7f402d-xxx-xxx-xxx-d759ff1b0d19.trayapp.io/objects/${acmeObject}/fields',`

```js
tray.on('CONFIG_SLOT_MOUNT', async(\{
  event, previousWizardState
\}) => {
  if (event.data.externalId !== tray.env.slotExternalId) return;
  // this call is dependent on the object selected on the previous screen of the wizard
  const acmeObject = previousWizardState.values.external_destination_object;
  const acmeAuth = previousWizardState.values.external_acme_authentication;
  // use the Http Client connector to call the Acme API
  const response = await tray.callConnector({
      connector: 'http-client',
      version: '5.5',
      operation: 'get_request',
      // grab the authId from the Acme API auth slot
      authId: acmeAuth
      input: {
        "status_code": {
          "range": \{
            "from": 200,
            "to": 299
          \}
        },
        "follow_redirect": false,
        "case_sensitive_headers": false,
        "auth": {
          "basic_auth_username": {
            // interpolate the Acme API key into the request
            "username": "{$.auth.api_key}"
          }
        },
        "parse_response": "true",
        "follow_keep_method": false,
        "follow_authorization_header": false,
        // interpolate the Acme object into the request path
        "url": https://6d7f402d-xxx-xxx-xxx-d759ff1b0d19.trayapp.io/objects/${acmeObject}/fields,
        "reject_unauthorized": true
      }
    })
    .then(response => JSON.parse(JSON.parse(response).response.body));
  return {
    ...event.data,
      // hide the slot from view in the wizard
      status: 'HIDDEN',
      // output the list of fields to pick from in the data mapping component
      value: response.results.map((\{
        name, typename
      \}) => (\{
        text: name,
        value: typename
      \}))
  };
});
```

### 3 - Edit 'external\_simple-data-mapping' slot

If we click the pencil icon to edit the **external\_simple\_data\_mapping** slot:
![edit-simple-data-mapping-slot](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-6fb9a5f4_edit-simple-data-mapping-slot.png)
We can see that service 1 (Salesforce) uses a simple 'dynamic lookup' as the 'Find Records' operation provides the list of options we need:
![simple-data-mapping-salesforce-dynamic-lookup](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-25517bcf_simple-data-mapping-salesforce-dynamic-lookup.png)
For Service 2 (Acme) we use the "Config slot value" method for retrieving the field metadata, and set the config slot to use as the 'acme-fields' slot created using the custom JS in the previous step:
![simple-data-mapping-acme-config-slot-lookup](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-337da4b6_simple-data-mapping-acme-config-slot-lookup.png)

### 3 - Showing / hiding slots based on dependencies

In order to improve the user experience of the wizard you may wish to show or hide slots based on the value of another. In this solution, on the final screen we present the option to the user of either:

* using the default mapping if the user so chooses
* making use of a user-defined mapping
  ![show-hide-slots](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-edafec65_show-hide-slots.png)
  The native Data Mapper feature doesn't support the ability to be hidden or visible based on the value of another slot.
  So we set up the **external\_use\_default\_mapping** slot in the following manner:

### 1 - Create 'use-default-mapping' config slot

The slot **use\_default\_mapping** is created as a **boolean config slot** in the step **Configuration slots (script-1) of the workflow**:
![use-default-mapping-in-workflow](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-c3d85fb2_use-default-mapping-in-workflow.png)

### 2 - Create custom JS for 'external\_use-default-mapping'

We then click to edit the **external\_data\_mapping** code:
![edit-external-data-mapping](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/8b12039c-a8f6834f_edit-external-data-mapping.png)
In this slot we have then defined a script that listens to the value of the boolean config by using both the CONFIG\_SLOT\_MOUNT and CONFIG\_SLOT\_VALUE\_CHANGED events:

* If the boolean option is set to **true (use the default mapping)** we don't want to show the user the mapping component so we set the slot status to **"HIDDEN"**.
* However if the option is set to **false** then we **render the schema** and change the status to **"VISIBLE"**.
  Note that when the CONFIG\_SLOT\_MOUNT event fires, the slot's status is "LOADING" - which shows the user that the wizard is rendering new schema.
  In order to also show the user that the wizard is loading new schema when the CONFIG\_SLOT\_VALUE\_CHANGED event fires, we first of all change the slot status to "LOADING", and then using the CONFIG\_SLOT\_STATUS\_CHANGED event we then render the schema for the slot.
  Here is the code implemented for the slot **external\_data\_mapping**:

```js
const getJsonSchema = async (previousWizardState) => {
    const acmeObject = previousWizardState.values.external_destination_object;
    const acmeAuth = previousWizardState.values.external_acme_authentication;
    const salesforceAuth = previousWizardState.values.external_salesforce_authentication;
    const salesforceObject = previousWizardState.values.external_salesforce_entity;
    // use Promise.all in order to parallelise requests
    const [ acmeFields, salesforceFields ] = await Promise.all([
        await tray.callConnector({
            connector: 'http-client',
            version: '5.5',
            operation: 'get_request',
            // grab the authId from the Acme API auth slot
            authId: acmeAuth,
            input: {
                "status_code": {
                    "range": \{
                        "from": 200,
                        "to": 299
                    \}
                },
                "follow_redirect": false,
                "case_sensitive_headers": false,
                "auth": {
                    "basic_auth_username": {
                        // interpolate the Acme API key into the request
                        "username": "{$.auth.api_key}"
                    }
                },
                "parse_response": "true",
                "follow_keep_method": false,
                "follow_authorization_header": false,
                "url": https://6d7f402d-xxx-xxx-xxx-d759ff1b0d19.trayapp.io/objects/${acmeObject}/fields,
                "reject_unauthorized": true
            }
            })
            .then(response => JSON.parse(JSON.parse(response).response.body))
            .then(result => result.results.map((\{ name, typename \}) => (\{ text: name, value: typename \}))),
        await tray.callConnector({
            connector: 'salesforce',
            version: '8.4',
            operation: 'so_object_describe',
            authId: salesforceAuth,
            input: {
                object: salesforceObject
            }
        })
        .then(res => JSON.parse(res))
        .then(result => result.fields.map((\{ label, name \}) => (\{ text: label, value: name \})))
    ]);
    return {
    /* note when using custom javascript, we define the mapping dictionary as an array type
    instead of the object type used in the native data mapping feature
    this is because the array type supports the 'table' property which allows us to present a UX-friendly component
    the 'table' property defines the column titles to show in the wizard
    the key names inside the 'properties' property define the keys set in the config slot when the user has entered their mappings
    */
        type: 'array',
        table: \{ lhs: 'Salesforce field', rhs: 'Acme field' \},
        items: {
            title: 'mapping',
            type: 'object',
            properties: {
                lhs: \{
                    title: 'Salesforce field',
                    type: 'string',
                    // display the dropdown of options to the user
                    enum: salesforceFields
                \},
                rhs: \{
                    title: 'Acme field',
                    type: 'string',
                    enum: acmeFields
                \}
            },
            // limit the user to only the specified columns
            additionalProperties: false
        }
    };
}
tray.on('CONFIG_SLOT_MOUNT', async (\{ event, previousWizardState \}) => {
    if (previousWizardState.values.external_use_default_mapping === true) {
        // hide the slot if the user has chosen to use the default mapping
        return \{
            ...event.data,
            status: 'HIDDEN'
        \};
    } else {
        // render the slot as a table if the user has chosen to define their own mapping
        return \{
            ...event.data,
            status: 'VISIBLE',
            jsonSchema: await getJsonSchema(previousWizardState)
        \};
    }
});
tray.on('CONFIG_SLOT_VALUE_CHANGED', (\{ event, previousWizardState, previousSlotState \}) => {
    if (event.data.externalId === 'external_use_default_mapping' && event.data.value === false) {
        return \{
            ...previousSlotState,
            status: 'LOADING'
        \};
    } else if (event.data.externalId === 'external_use_default_mapping' && event.data.value === true) {
        return \{
            ...previousSlotState,
            status: 'HIDDEN'
        \};
    }
});
tray.on('CONFIG_SLOT_STATUS_CHANGED', async (\{ event, previousWizardState \}) => {
    if (event.data.externalId === tray.env.slotExternalId && event.data.status === 'LOADING') {
        return \{
            ...event.data,
            status: 'VISIBLE',
            jsonSchema: await getJsonSchema(previousWizardState),
        \}
    }
})
```

## More Examples (standalone)

### 1 - Validating the auth or input data

> **Info:** This example illustrates another use case of Custom JS and does not employ the Acme-Salesforce integration use case we used to illustrate examples 1, 2 and 3.

Data validation is a very common use case where you might want to use Custom JS. You can perform a sanity check on the data entered by the user in the config wizard and display an error message if data entered does not pass the tests given by you.

#### 1a - Use case for data type validation

You can download the project from [here](#) and import to test this example end to end.
If you are taking a config value like 'Email' or 'phone number' from the user in the config wizard, you can use custom JS to check for a valid email address/phone number.
Here we show you an example where end user has to enter a valid email address for the field 'username'.
![customjs : examples : customJs datavalidation configwizard1](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/17xbMUVGSgv6CFaOqM8Jp1_customJs_datavalidation_configwizard1.png)
If the entered email is not a valid email, config wizard throws an error and the user can' move to the next step and has to go back to the previous step to fix the error.
![customjs : examples : customJs datavalidation configwizard2](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/3C8U5hyms82R2mcUEYUZG5_customJs_datavalidation_configwizard2.png)
Below are steps how you can achieve this:

### 1 - Create data validation slot

It is important that you set a config slot in your workflow that can be used for showing the output of data validation. You can do this with the script connector.
![custom js : data validation : workflow config property](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/3PtHy4y7fZadGNpz7zW92j_customjs_validation_workflow.png)

### 2 - Edit custom js

Move the screen which contains the dummy config slot you created in step 1 where you want to show the output of validation.
![custom js : examples : data validation : data type : config wizard](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/7xR9Ppf2hL2XEl3zPqw3Gl_customjs_validation_configwizard.png)
Here to show the output of validation of data entered in screen 1 we move the screen with the dummy slot to screen 2. Now edit the custom JS for the dummy slot with the code provided in the next step.

### 3 - Sample Code

See the code sample below for reference:

```js
function validateEmail(email) {
    let mail_format = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]\{2,\}))$/;
    if(email.match(mail_format))
        return undefined;
    else {
        console.log("validated");
        return \{
            status: 'ERROR',
            message: 'Invalid Email address.'
        \};
    }    
}
tray.on('CONFIG_SLOT_MOUNT', async (\{ event, previousWizardState \}) => {

    //If the event triggering slot is not the validation slot, return undefined
    if (event.data.externalId !== "external_validation_slot") return;
  
    const email = previousWizardState.values["external_username"];

    //If the email has not been provided, do not change the validation slot
    if (email === undefined) return;

    //Get the validation
    const validation = await validateEmail(email);

    //Change status to visible
    //Set validation to an error or undefined (success)
    let returned = {
        ...event.data,
        status: 'VISIBLE',
        value: {},
        validation,
        jsonSchema: \{
            type: 'object',
            title:  "All fields validated. Please fix errors if any",
            additionalProperties: false
        \},
        className: validation ? "validation_error" : "validation_successful"
    };
    console.log(validation)
    return returned 
});
```

#### 1b - Use case with a connector operation

You can download the project from [here](#) and import to test this example end to end.
This example shows how you can use Call Connector function to validate user supplied data in the config wizard.
![customjs:datavalidation\_googleanalytics1](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/P05gKnXw3p7kLTcPxjQaY_imageedit_4_2453835353.png)
This integration runs the google analytics report for a website from a start date to an end date and feeds the data to a storage (like Amazon S3).
![customjs:datavalidation\_googleanalytics2](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/eZjM1h3eQSkH1ONaLOlyx_datavalidation_2.png)
The connector's 'Generate report' operation expects the end date to be greater than the start date and so if a user goes ahead and chooses a start date later than end date, the config wizard throws the error returned by the connector, instead of running the integration end to end. User can go back and correct the date order and proceed as usual.
![customjs:datavalidation\_googleanalytics3](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/38XCE8WJpkY8HU4tBZtPkx_datavalidation_3.png)
Here's how you can achieve this with custom JS:

### 2 - Showing / hiding slots based on dependencies

Here's another example of this use case.
You can download the project from [here](#) and import to test this example end to end.
You want to send a slack notification for workflow success and failure to the end user's configurable slack channel.
![customJS hideSlotBasedOnDependency](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/1DyfGEYdIv49bWRIvv1Ut4_customJS_hideSlotBasedOnDependency.png)
The default option sends notification to the same channel but if user unselects the default check box, you could show a slot to configure the channel for error notification.
![customJS 2 hideSlotBasedOnDependency](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/2Q75iW7U678HjttaF5OPhv_customJS_2_hideSlotBasedOnDependency.png)
It is up to you to decide how you want to provide best user experience.

### 3 - Conditional display for solution screens

You can show / hide config wizard screens.
You set screen visibility dynamically in CustomJS by emitting a `TOGGLE_SCREENS_VISIBILITY` event. You will need to pass which screens you want to show or hide as shown below:

```js
tray.emit("TOGGLE_SCREENS_VISIBILITY", [
  \{
    screenId: "b3146e55-1134-45cf-8c62-6991a6b5855f", // screen id you want to toggle visibility for
    isVisible: true, 
	\},
  \{
    screenId: "978c8df3-3043-4acd-a1a7-504d33ab9506",
    isVisible: false,
  \}
]);
```

You can get screen ids from the `solutionInfo` object, that is passed as part of the callback argument of all solution events, such as "CONFIG\_SLOT\_MOUNT" or "CONFIG\_SLOT\_VALUE\_CHANGED".
You can hardcode the ids or use the `solutionInfo.screens[index]` but if the screens order changes, the code would have to be updated.

```js
tray.on("CONFIG_SLOT_MOUNT", (\{ event, previousWizardState, solutionInfo \}) => \{ 
  /* ... 
  ...
  console.log(solutionInfo.screens);
  ...
  ...
  */
\});
tray.on("CONFIG_SLOT_VALUE_CHANGED", (\{ event, solutionInfo \}) => \{ 
  /* ...
  ...
  console.log(solutionInfo.screens);
  ...
  ...
  */
\});

solutionInfo.screens[0].id // The "Not visible to users" screeen
solutionInfo.screens[1].id // Screen 1
solutionInfo.screens[2].id // Screen 2 etc.
```

index - 0 refers to the "Not visible to users" screen, index - 1 refers to the first screen and so on as shown in the code block above.
When a screen is hidden, an icon will show up in the screens list next to the corresponding screen.
![hiddenScreens config wizard](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/3e2lSDUK0XBsSp6M1d06cG_hiddenScreens.png)

#### Example implementation

Here is a simple example that shows how to set screens visibility dynamically based on config variables (e.g. checkboxes). Download the project file from [here](#).
**1. Create checkboxes** to control the visibility of chosen screens (boolean config slots)
![conditonal display screens prop panel](https://tray.ai/documentation/images/platform/embedded/advanced-features/custom-js/examples/LD1AnDPEVu2QVAP7EyXNH_conditonal%20display%20screens%20prop%20panel.png)
2\. **Set the default visibility** in the "CONFIG\_SLOT\_MOUNT" callback.

```js
// On the earliest slot mount, set default 
tray.on("CONFIG_SLOT_MOUNT", (\{ event, previousWizardState, solutionInfo \}) => {
  if (event.data.externalId === tray.env.slotExternalId) {
		tray.emit("TOGGLE_SCREENS_VISIBILITY", [
		  \{
		    screenId: "978c8df3-3043-4acd-a1a7-504d33ab9506", // solutionInfo.screens[2].id,
		    isVisible: previousWizardState.values["external_screen_2"] || false,
			\},
		  \{
		    screenId: "b3146e55-1134-45cf-8c62-6991a6b5855f", // solutionInfo.screens[5].id,
		    isVisible: previousWizardState.values["external_screen_5"] || false,
		  \}
		]);
	}
});
```

3. **On checkbox change, set screen visibility** for each checkbox by editing CustomJS for each config slot
