Restoring the editor
- 8 minutes to read
After the user has created a design and added a personalized product to the shopping cart, they may want to return to editing this design to fix any problems or use it for a new order. In other words, you need to be able to open the same config file but initialize it differently to restore the state of the editor at the moment when the user left off.
In simple cases, it may be a straightforward task. In most cases, all you need to do is to open a user design instead of an original template. However, if the config includes not only the editor but also some options, it may get complicated. You will need to restore user selections that may cause recalculation of the dynamic expressions in the config. Predicting side effects of that may be challenging.
To be able to restore the editor and maintain both the product options and design changes, Customer's Canvas offers the concept of snapshots and a special restoration
section of the config. This article explains how to use them.
Snapshots
A snapshot is basically a state of all widgets in the editor represented by a base64-encoded string. UI Framework allows for applying this string later to restore the state of widgets. When it happens, it temporarily turns off the dynamic expression auto update to prevent any side effects and inconsistencies.
UI Framework allows for reading this string at any time. As usual, you want to do it when a user finishes personalization and leaves the editor. It is supposed that you store this string somewhere, for example, in local storage or a database that temporarily stores unfinished shopping carts. When a user decides to return to the editor, you need to pass a snapshot to the editor page and initialize the editor using this string.
All standard e-commerce plugins implement this idea. As long as a config file sets up a snapshot and a restoration
section (see further), the "return to edit" functionality works out of the box. However, you can implement it in any custom integration as well.
Creating a snapshot
The simplest way to read a snapshot and pass it outside of the editor is to define the snapshot
property in the data
object of the order
or cart
widget as follows:
{
"name": "order",
"type": "order",
"params": {
"images": "{{ $['design-editor'].proofImageUrls.map(item=>item[0]) }}",
"downloadUrls": "{{$['design-editor'].hiResUrls}}",
"data": {
"stateId": "{{ $['design-editor'].stateId}}",
"snapshot": "{{ main.editorState }}"
}
}
}
Tip
All our standard e-commerce plugins read the snapshot from this snapshot
property. In custom integrations, it is necessary to implement the ability to store this value, as explained below.
As you may notice, we're just reading the editorState
property of the editor component (au-wizard HTML element), which is accessed through the main
object of a scope.
You can also create a snapshot outside of the config in your JavaScript code. To make a snapshot, you can call the auWizard.createEditorSnapshot()
method in your script.
const snapshot = await auWizard.createEditorSnapshot(true);
If you pass true
as the parameter, then this method will also have all Design Editors used in the config to save the design by using the saveProduct
method of the Customer's Canvas IFrame API.
Storing a snapshot
It is up to you where and how you store a snapshot. For example, you may use:
- Local storage
- Database
- For the online store platforms like Shopify, cart item meta fields
- ... and any other way to store temporary data
Note that a user may order more than one line item, or the same item may be ordered several times with different personalization. It may be important to take this into consideration, for example, if you are using local storage and select a key name for a snapshot entry.
It is not recommended to store this data too long. If you change the config after the snapshot is created, UI Framework may fail to restore the updated config with a snapshot.
Restoring the editor state
As usual, to allow the user to restore the editor, you are adding a button, for example, "Return to edit", on each line item in a shopping cart. When a user clicks it, you are opening the product page again and passing the snapshot through a query string or another method.
As usual, you add some code that detects whether a new personalization instance began or the previous one needs to be restored. In the latter case, you need to initialize the e-commerce driver in the following way:
let ecommerce = await driver.init(product, editor, config,
{ editorUrl: "https://{your-site-url}/design-editor/" },
/* restore data */ { snapshot: "N4I...." },
/* quantity */ 1,
/* user info*/ { id: "JohnWood" });
Another way to do so is to call auWizard.restoreEditor()
:
await auWizard.restoreEditor(snapshot);
Using the restoration section
For most config files, the restoration process described above is sufficient. However, when the config logic is complex or relies on data obtained from the backend through the ajax
widget, you may need to have finer control over this process.
This is where the restoration
section of a config comes into play. It looks like this:
{
"restoration": {
"widgets": [...],
"steps": [...],
"vars": {...}
}
}
When this section is presented, UI Framework will use values of widgets
, steps
, and vars
specified inside restoration
instead of the original ones. You may use different amounts of widgets or steps. If any of these values are omitted, they are taken from the original config.
Inside this section, you may use dynamic expressions with one additional object in a scope - widgetData
. It allows for referring the widget data stored in a snapshot instead of the widget itself.
Example - using widgetData
For a better understanding of how it works, let's consider an example.
Imagine that you are building a personalization process where you want a user to select a vehicle using three dropdown elements (option
widget) - vehicle year, make, and model. Depending on these values, a user selects a design with an appropriate mockup and creates artwork for a particular vehicle.
So, your original config will consist of two steps: 1) choosing a vehicle and 2) personalizing a design that corresponds to a selected vehicle.
These dropdowns are populated from the server, and a list of available values depends on other dropdowns. For example, if you choose "Ford" as a make and "2021" as a year, you would like to see only Ford models that are available in the 2021 model line.
This behavior works great when the user just started the personalization process and did not choose any values yet. However, when you return to the editor from the cart, a vehicle is already selected, and the only thing you need to do is restore the design editor tool. Reusing the same config and avoiding reloading the design without losing progress is quite a challenging task.
The restoration
section comes to the rescue. It allows for simplifying a config loaded after the restoration and excluding widgets that may prevent the editor from restoring its state correctly.
The original config may look like this:
{
"widgets": [
{
"name": "vehicleYear",
"type": "ajax",
"params": { /* omitted for brevity */ }
},
{
"name": "vehicleMake",
"type": "ajax",
"params": { /* omitted for brevity */ }
},
{
"name": "vehicleModel",
"type": "ajax",
"params": { /* omitted for brevity */ }
},
{
"name": "order",
"type": "order",
"params": {
"data": {
"Year": "{{ $['vehicleYear']._.title }}",
"Make": "{{ $['vehicleMake']._.title }}",
"Model": "{{ $['vehicleModel']._.title }}"
},
"images": "{{ $['editor'].proofImageUrls.map(x=> x[0]) }}",
"downloadUrls": "{{ $['editor'].hiResUrls }}"
},
// ...
}
],
// ... omitted for brevity
}
In the restoration.widgets
, we need to get rid of the ajax widgets and refer the widgetData
instead of these widgets:
{
"widgets": [ /* the same */ ],
"restoration": {
"widgets": [
{
"name": "order",
"type": "order",
"params": {
"data": {
"Year": "{{ widgetsData.vehicleYear.selected.optionValue.title }}",
"Make": "{{ widgetsData.vehicleMake.selected.optionValue.title }}",
"Model": "{{ widgetsData.vehicleModel.selected.optionValue.title }}"
},
"images": "{{ $['editor'].proofImageUrls.map(x=> x[0]) }}",
"downloadUrls": "{{ $['editor'].hiResUrls }}"
}
},
// ...
],
// ...
},
// ...
}
Accessing properties in widgetData
To access widget values in widgetData
, you may need to use slightly different syntax. In the following table, you can learn how the basic properties of widgets match their representations in widgetsData
.
Widget | Property in config | Property in widgetData |
---|---|---|
ajax | response | response |
ajax | responses | responses |
ajax | statusCode | status |
asset-storage-ajax | apiResponse | apiResponse |
asset-storage-ajax | response | response |
canvas | stateId | stateId |
checkbox | ._ | value |
color-picker | color | color |
color-selector | color | color |
data-driven-editor | stateId | design.designId |
data-driven-editor | userId | access.userId |
datasheet | tableService.getData() | tableData |
datasheet | columnSchemes | columnSchemes |
design-editor | stateId | stateId |
gallery | ._ | value |
html | $.container.innerHTML (полный HTML) | html |
input-text | ._ | text |
option | ._ | selected.optionValue |
preflight | stateId | stateId |
preflight | page | page |
static-text | text | text |
Design Editor in the restoration section
The Design Editor widget is processed in a special way. As you know from the Working with Design Editor article, its config has an initial
section, which describes how the editor is loaded. It includes two parts: productDefinition
and editorConfig
.
When we are restoring the editor, we first execute the productDefinition
, then load the design from the snapshot. So, it does not matter what productDefinition
you specify - it can be any valid value, for example, a blank 1x1 design. It will be replaced by the saved design anyway.
As for the editorConfig
, you might just copy the config from the original widget. However, it is almost always very large. To avoid copying a bulky config, you may just replace it with an object { "loadConfigFromState": true }
. In this case, it will take this config from the saved state file.
Here is an example:
{
"name": "editor",
"type": "design-editor",
"params": {
"initial": {
"productDefinition": {
"surfaces": [
{
"width": 1,
"height": 1
}
]
},
"editorConfig": {
"loadConfigFromState": true
}
}
}
}