TrophyCustomer's Canvas is honored with a 2020 InterTech Technology Award! Learn more 

Creating Parametrized Configs

Typically, you want to allow your users to edit multiple products in a similar way. For example, you have an editor for invitations and multiple templates. You certainly don't want to create a copy of the config that has only one difference - the template name.

So, you need a way to parametrize the config, i.e. pass the name of a template (or any other piece of data) outside this config. There are several approaches to do so:

  1. Using the product attributes passed through the e-commerce driver.
  2. Using the vars section of the config.
  3. A combination of these two.

Let's learn about these approaches in more detail.

Using Attributes

Initializing attributes through the driver

The e-commerce driver is a JavaScript module, which encapsulates all operations with the e-commerce system. Each e-commerce system has its own driver. For all systems that are not integrated with the UI Framework, a default driver may be used.

One of the important operations is reading all the information about the product that is being ordered. It is exposed through the Product object, which has such fields as sku, price, and, in particular, attributes - an array of key-value pairs containing some product metadata.

For example:

  • In nopCommerce, attributes are known as Specification Attributes (available through the control panel).
  • In WooCommerce, they are just called Attributes or Product Attributes (available through the control panel).
  • In Shopify, they are stored as metafields created by Aurigma's back end.

E-commerce plugins for these systems extract this data automatically, so you don't need to do anything special to pass them to the editor.

If you are using a default driver, you just create a JSON structure like this:

const product = {
  id: 0,
  sku: "PRODUCT-001",
  title: "My Product",
  description: "This is a test product.",
  options: [],
  price: 1,
  attributes: [{
     id: 1,
     title: "Template",
     value: "my-template"
  }]
};

and pass it to driver.init.

It is assumed that you read it from your database and initialized this object with the appropriate values.

Reading attributes in the config

All important e-commerce driver objects are available in the scope of the dynamic expressions, in particular, the product object. You may refer to attribute values in the config, like this:

{
    ...,
    "foo": "{{ product.attributes.find(a=>a.title === 'Template').value }}"
}

This way, you may reuse the same config with all products that have the attribute called Template.

Handling missing attributes

What will happen if you are trying to access a non-existing attribute? The find method would return undefined and you will try to get the value of the undefined variable. This results in a JavaScript error.

If you expect that you may load the config with a product that does not support this attribute, you need to add some checks. The shortest one would be using the "short circuit" operations like this:

{
    ...
    "foo": "{{ (product.attributes.find(a=>a.title === 'Template') || {}).value || '' }}"
}

In this code, if no attribute is found, we will try to read a value from an empty object. The result is also undefined, and in this case the second || operator will return an empty string.

You may use it in more complicated constructions, for example, using #if blocks:

{
    ...,
    "foo": {
        "{{#if !product.attributes.find(a=>a.title === 'Radius')}}": 0,
    	"{{#else}}": "{{product.attributes.find(a=>a.title === 'Radius').value}}" 
    }
}

Using the vars Block

Using attributes is not always possible or reasonable. Attributes are something the end user edits through the admin panel of the e-commerce system. However, in some scenarios, you don't want them editing these parameters. Some examples:

  • When values are calculated dynamically.
  • URLs to some APIs, tokens, paths, etc - various tech information.
  • Objects, arrays, or other types of data that cannot be edited through your control panel.

In this case, instead of using attributes, the following approach is recommended:

  1. Declare your data that you plan to edit dynamically in the vars section. Set some default value for that there (useful for testing purposes or if you forget to alter it).
  2. Refer to the data as {{ vars.MyVariableName }} inside the config.
  3. In the integration code, modify the vars section of the config through your code. Since it is a JSON object, you can easily do it as shown in the code example (see below).

Example 1 - variable URL for prod and dev environments

For example, let's assume you want to send the AJAX requests to a back end and different URLs are used in dev and prod environments. You may do so as follows:

Config:

{
    "vars": {
        "backendUrl": "http://example.com/api"
    },
    "widgets": [
        {
            "name": "my-request",
            "type": "ajax",
            "params": {
            	"url": "{{vars.backendUrl}}",
                ...
        	}
        }
    ]
}

Integration code:

let config = await (await fetch("https://url-to-config/1234")).json();

/*  Replace the vars here */ 
config.vars.backendUrl = "https://my-production-url.com/api";

let driver = (await moduleLoader.dynamicImport("ecommerceDriver", "lib/ui-framework-4.0.13/dist/drivers/default-driver.js")).ecommerceDriver;
let editor = (await moduleLoader.dynamicImportDefault("editor", "lib/ui-framework-4.0.13/dist/editor.js")).editor;

let ecommerce = await driver.init(product, editor, config, {customersCanvasBaseUrl: ""}, null, 1, {id: 'test-user'});
ecommerce.products.current.renderEditor(document.getElementById("editor-container"));

You can also edit the widget directly like config.widgets[0].params.url = '...', but it is a much more fragile approach and it may become very difficult if you are using this value in several places. So, it is highly recommended to use the vars section.

Tip: A good practice is to use vars for all data, not only the one you are going to parametrize. It will make the config much easier to maintain.

Example 2 - a list of designs

Now, let's look at a more complicated example. Here, we want to display a list of designs in the gallery and pass a selected design to the editor. It will require you to pass an array of items to the editor.

let config = await (await fetch("https://url-to-config/1234")).json();

/*  Replace the vars here */ 
config.vars.items = ["design1", "design2", "design3"].map(x=>({
  "title": x,
  "designFile": `root/folder/${x}`, 
  "previewUrl": `https://example.com/api/getpreview?name=${x}`
}));
...

Tip: Instead of setting it up on the client side, you may modify it on the server.

The config may look like this:

{
    "vars": {
        "items": []
    },
    "widgets": [{
     	"name": "design-list",
        "type": "gallery",
        "params": {
	        "items": "{{vars.items}}"
        }
    }, 
    {
        "name": "editor",
        "type": "canvas",
        "params": {
            ...
            "setPrintArea": {
                "data": {
                    "designFile": "{{$['design-list']._.designFile}}"
                }
            } 
        }
    }]
}

Example 3 - modifying vars after loading the editor

Typically, you don't need to do anything on your page after the editor is loaded until the user finishes editing. However, sometimes it may be necessary - for example, you are waiting for the asynchronous operation results or, in addition to the editor, you have some other user interface elements on the same page.

In this case, you need to do the following:

  1. Get an instance of the editor object returned after you call renderEditor.
  2. Use the appendVars(name, value) method to update or add a variable.

Like this:

...

const ecommerce = await driver.init(product, editor, config, {customersCanvasBaseUrl: ""}, null, 1, {id: 'test-user'});

const editorInstance = await ecommerce.products.current.renderEditor(document.getElementById("editor-container"));

...

/* 
   Before you execute this line, {{vars.foo}} returns undefined, so you need to add some checks.
   After you call it, it will become "bar" and the config is automatically recalculated.
*/
editorInstance.appendVars({foo: "bar"});

Fetching data remotely

There is no need to pass all the data as a parameter. The data may be too big or too complicated to generate it on the client side. In this case, you may get it from a remote server using the ajax widget.

You still need to create a parameter (either through an e-commerce driver or vars) that would pass all the information required to make a request, like this:

Integration code:

let config = await (await fetch("https://url-to-config/1234")).json();

/*  Replace the vars here */ 
config.vars.query = "foo*";

... 

Config:

{
    "vars": {
        "id": 0
    },
    "widgets": [{
        "name": "get-data",
        "type": "ajax",
        "params": {
            "url": "http://example.com/api/search",
            "method": "POST",
            "responseType": "json",
            "request": {
                "query": "{{vars.query}}"
            }
        }
    }, {
        "name": "items",
        "type": "gallery",
        "params": {
            "items": "{{$['get-data'].response}}"
        }
    }]
}

Here, we make a request to the server based on the variable passed from the integration code and use the output from the server in another widget.