Creating Parametrized Configs

Typically, you want to allow your users editing 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 which has the only different - the template name.

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

  1. Using the product attributes passed through the ecommerce driver.
  2. Use the vars section of the config.
  3. A combination of these two.

Let's learn about them in more details.

Using Attributes

Initializing attributes through the driver

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

One of the important operations is reading all the information about the product which is being ordered. It is exposes through the Product object which has fields like 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.

Ecommerce plugins for these system extracts these data automatically, so you don't need doing 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 supposed that you read it from your database and initialize this object with the appropriate values.

Reading attributes in the config

All important ecommerce driver objects are available in the scope of the dynamic expressions, in particular, the product object. You may refer 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 which has the attribute called Template.

Handing 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 undefined variable. So you will get an error in JavaScript.

So if you expect that you may load the config with a product which 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 it this case the second || operator will return an empty string.

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

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

Using vars Block

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

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

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

  1. Declare your data 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 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 doing 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 backend and different URLs are using in dev and prod environments. You may do it 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"));

Of course, you may edit the widget directly like config.widgets[0].params.url = '...', but it is much more fragile approach and it may get really difficult if you are using this value in several places. So it is highly recommended using 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 - list of designs

Now let's see 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 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 on a 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 doing 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 to complicated to generate it on a 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 ecommerce driver or vars) which 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 a server in another widget.