Handling Product Customization

This topic dwells on how to handle product customization using the Customer's Canvas IFrame API. Here we discuss such tasks as saving product, approving design, and rendering hi-res output. Customer's Canvas IFrame API provides three methods to perform these tasks: Editor.saveProduct, Editor.getProofImages, and Editor.finishProductDesign.

In the last section of the topic you can find the sample demonstrating how to handle product loading, to save product, to display proof images, to reload product for further editing, and to provide links to hi-res output.

After the API is initialized, you can save the product, get proof images for displaying them to the user in the approval process, and render the hi-res output, like it is described below.

Saving Product

The Editor.saveProduct method allows saving a product current state. You may call this method while a user is customizing the product, for example, by adding the save button to the design page. Also you can implement the autosave feature by calling the method after equal periods of time using a timer. In the sample below a product is saved after a user has finished product customization.

If you want to allow your users to name saved products in the user interface, it can be done with the saveProduct method as well. However a part of the feature is to be implemented in your system. The idea is quite simple. Customer's Canvas returns the product state id and the return-to-edit URL. After that your system links them with the user account and the name entered by the user. Having all these details you can easily reload the product by its name.

The saveProduct method returns a Promise, whose onFulfilled callback function accepts an object implementing the ISaveProductResult interface. The interface has the following properties:

  • stateId: a product state identifier; it appears as xxxxxxxx-yyyy-xxxx-yyyy-xxxxxxxxxxxx. You can use this value to load the product into the editor, as it is described in the Loading Product State Into Editor section below.
  • userId: a user identifier. If you specified a user identifier in the editor configuration, using the IConfiguration.userId property, then you get the identifier presented here. Otherwise, you get the default user identifier.
  • returnToEditUrl: a return-to-edit URL. This property is provided for backward compatibility only; it utilizes the old API based on query string. To learn on how to perform the same functionality using IFrame API see the Loading Product State Into Editor section below.
JavaScript
//Saving a product.
editor.saveProduct()
    //If the product is saved correctly.
    .then(function (result) {
        stateId = result.stateId;
        returnToEditUrl = result.returnToEditUrl;
        ...
})
    //If there was an error thrown when saving the product.
    .catch(function (error) {
        console.error("Saving product failed with exception: ", error);
});

Getting Proof Images

As a part of the process when users approve results of their work (or return to the editor to make some last minute changes), you need to show them proof images displaying customized products. At this stage we do not need hi-res output as the product is not finalized yet. So, just call the getProofImages method after a user has finished customizing a product. This method returns a Promise, whose onFulfilled callback function accepts an object implementing the IProofResult interface. The interface has the following property:

  • proofImageUrls: an array of links to proof images.
JavaScript
//Getting links to proof images.
editor.getProofImages()
    //If the links proof images are generated successfully.
    .then(function (result) {
        stateId = result.stateId;
        proofImageUrls = result.proofImageUrls;
        ...
})
    //If there was an error thrown while getting links to proof images.
    .catch(function (error) {
        console.error("Getting proof images failed with exception: ", error);
});

Also the getProofImages method accepts an object containing maximum width and height of proof images as the optional argument. If you pass this argument, then proof images will be proportionally resized accordingly to the given values. For example, if maximum width and height are set to 640 pixels both, then 1280*960px image will be resized to 640*480px. The following snippet sets width and height of a proof image to 640 pixels:

JavaScript
//Specifying maximum height and width of proof images.
editor.getProofImages({maxHeight: 640, maxWidth: 640})
    .then(function (result) { ... })
    .catch(function (error) { ... });

Rendering Hi-res Output

The last step, after a user has approved the design, is rendering hi-res output. To perform this call the finishProductDesign method. This method saves the current product state and returns links to hi-res output. The finishProductDesign method returns a Promise, whose onFulfilled callback function accepts an object implementing the IFinishDesignResult interface. The interface has the following properties:

  • hiResOutputUrls: an array of links to hi-res output.
  • returnToEditUrl: a return-to-edit URL. This property is provided for backward compatibility only; it utilizes the old API based on query string. To learn on how to perform the same functionality using IFrame API see the Loading Product State Into Editor section below.
  • stateId: a product state identifier; it appears as xxxxxxxx-yyyy-xxxx-yyyy-xxxxxxxxxxxx. You can use this value to load the product into the editor, as it is described in the Loading Product State Into Editor section below.
  • userId: a user identifier. If you specified a user identifier in the editor configuration, using the IConfiguration.userId property, then you get the identifier presented here. Otherwise, you get the default user identifier.
  • userChanges: an object containing information about all changes of the product text elements made by a user.
  • boundsData: a bounding rectangle of all items in the product, in points. For more information see the Measuring Products topic.
JavaScript
//Completing product customization.
editor.finishProductDesign()
    //If product customization is completed successfully.
    .then(function (result) {
        hiResOutputUrls = result.hiResOutputUrls;
        proofImageUrls = result.proofImageUrls;
        stateId = result.stateId;
        returnToEditUrl = result.returnToEditUrl;
        boundsData = result.boundsData;
        userChanges = result.userChanges;
        ...
})
    //If there was an error thrown when completing product customization.
    .catch(function (error) {
        console.error("Completing product customization failed with exception: ", error);
});

The userChanges object is not as simple and straightforward as the other data fields returned by the callback, so it needs clarification. As pointed before, this object contains text entered by the user in text elements. That means you can get all text input on the server side, which allows for providing the user with custom default values for text layers, such as name, phone, address, etc. To implement it your system should store values entered by the user in a database and pass them as defaults to other templates loaded by the user. For example, if one template has an address field, you can store the entered value and pre-populate address fields in other templates loaded by the user.

The userChanges object implements the IUserChanges interface and has the following structure:

JavaScript
{ 
    texts:
        [
            {
                name: "layer_name",
                usersValue: "layer_text"
            },
            ...
        ],
    inStringPlaceholders:
        [
            {
                name: "placeholder_name",
                usersValue: "placeholder_value"
            },
            ...
]}

texts and inStringPlaceholders are arrays of objects having identical structure. The arrays contain text entered by the user in text elements. For each text element, which was changed by the user, corresponding object contains its name and new value. Text layers are discussed in the Point and Rich (Paragraph) Text topic; for information about text placeholders see the In-String Placeholders and Text Validation topic.

The finishProductDesign method accepts optional file name argument to customize the resulting file name. This name is only used when you allow the end-user directly downloading hi-res files using the links returned by the finishProductDesign method. If you omit the argument and try downloading a hi-res file, it will always have result.ext name where .ext stands for actual file extension for configured output format (it will be result.pdf for the PDF format). This behavior can be changed using the argument and result will be replaced with whatever is passed in it. If Customer's Canvas is set up to produce multiple hi-res files, the same file name will be used for all of them, and the user will have the option to rename them on the client side when saving in the browser. The information on how to set a type of hi-res output you can find in Configuring Hi-res Output topic.

Also the finishProductDesign method accepts maximum width and height of proof images as optional second and third arguments. If you set these arguments, then proof images will be proportionally resized accordingly to the given values. For example, if maximum width and height are set to 640 pixels both, then 1280*960px image will be resized to 640*480px. The following snippet sets width and height of a proof image to 640 pixels.

JavaScript
editor.finishProductDesign({fileName: "envelope", proofMaxHeight: 640, proofMaxWidth: 640})
    .then(function (result) { ... })
    .catch(function (error) { ... });
Note

All the API functions discussed above should be called in a page context where the Customer's Canvas editor is hosted.

Loading Product State Into Editor

State files are internal Customer's Canvas files, which are created when products are saved via the Editor.saveProduct or the Editor.finishProductDesign method.

State files are located in user subfolders and are named as follows: aaaaaaaa-bbbb-cccc-xxxx-yyyyyyyyyyyy.st, where aaaaaaaa-bbbb-cccc-xxxx-yyyyyyyyyyyy is the value of the stateId property returned by any of the methods mentioned above. So, to load a saved product into the editor you should pass a state file name without extension and user identifier to the loadEditor method, like it is shown below:

JavaScript
//Finish product customization.
editor.finishProductDesign()
    .then(function (result) {
        ...
        //Save state identifier and user identifier.
        stateId = result.stateId;
        userId = result.userId;
        ...
})
    .catch(function (error) {
        ...
});
...
//Load a previously saved product.
CustomersCanvas.IframeApi.loadEditor(iframe, stateId, { userId: userId });

The Sample

The code sample in this section represents a three-step wizard that contains the design page, the approval page, and the finish page. Its workflow includes the following steps:

  1. The system loads a product template.
  2. A user creates a design.
  3. The user initializes transfer to the approval page. The system saves the product and displays proof images.
  4. The user can return to editing the product. In this case the system restores the product using the state ID got on the previous step.
  5. The user approves the design. The system displays the finish page containing links to hi-res output and return-to-edit URL.

To make this sample work do the following:

  1. Copy and paste the code to the index.html file.
  2. Replace example.com with the name of the site where your Customer's Canvas instance is hosted and save the file.
  3. Open index.html in a browser.
HTML
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta charset="utf-8" />
    <title>Business cards - Sample host page - Customer's Canvas</title>
    
    <script type="text/javascript" src="http://code.jquery.com/jquery-2.1.1.min.js">
    </script>
    <script type="text/javascript" src="http://example.com/Resources/Libs/jquery/loadmask/jquery.loadmask.min.js">
    </script>
    <link href="http://example.com/Resources/Libs/jquery/loadmask/jquery.loadmask.css" rel="stylesheet" type="text/css" />
    <link href="index.css" rel="stylesheet" /> 
    <!-- The IFrame API script. IMPORTANT! Do not remove or change the id. -->
    <script id="CcIframeApiScript" type="text/javascript" src="http://example.com/Resources/SPEditor/Scripts/IFrame/IframeApi.js">
    </script>

    <script>
        $(function () {
            //Defining the product.
            productDefinition = {surfaces: ["stamp"]};
            
            //Defining the editor configuration.
            configuration = { widgets: {FinishButton: {mode: "Disabled"}}};
            
            var editorFrame = $("#editorFrame");
            
            var editor = null;
            
            var frameParent = editorFrame.parent();
            
            function handleError(error, message) { 
                frameParent.mask("Server error.");
                console.error(message, error);
                return error;
            };
            
            var loadData = null;

            //Loading a product into the editor.
            function loadProduct(product) {
                frameParent.mask("Loading...");
                //Loading the editor.
                CustomersCanvas.IframeApi.loadEditor(editorFrame[0], product, configuration, 
                                                    function () { frameParent.mask("The product is being configured for the first launch. It may take a while.") })
                    //If the editor has been successfully loaded.
                    .then(function (e) {
                    editor = e;
                    frameParent.unmask();
                })    
                    //If there was an error thrown when loading the editor.
                    .catch(function (error) {
                        loadData = handleError(error, "Load failed with exception: ");
                });
            }

            //Loading the defined above product into the editor.
            loadProduct(productDefinition);
            
            var currentPage = "design";

            //Object containing data generated by getProofImages.
            var approveData = null;
            
            //Object containing data generated by finishProductDesign.
            var renderData = null;

            //Object containing data generated by saveProduct.
            var saveData = null;

            //Saving product and getting links to proof images.
            $("#editorPage #nextButton").click(function () {
                frameParent.mask("Loading...");
                //Saving a product.
                editor.saveProduct()
                    //If the product has been successfully saved.
                    .then(function (result) {
                        //Saving product state info.
                        saveData = result;
                })
                    //If there was an error thrown when saving the product
                    .catch(function (error) {
                        saveData = handleError(error, "Save failed with exception: ");
                });
                //Getting proof images.
                editor.getProofImages()
                    //If proof images has been successfully got.
                    .then(function (result) {
                        //Saving proof images info.
                        approveData = result;

                        //Go to the approval page.
                        goToPage("approve");

                        frameParent.unmask();
                })
                    //If there was an error thrown when getting proof images.
                    .catch(function (error) {
                        approveData = handleError(error, "Getting proof images failed with exception: ");
                });
            });

            //Completing product customization.
            $("#approvePage #approveButton").click(function () {
                frameParent.mask("Loading...");
                //Completing product customization.
                editor.finishProductDesign()
                    //If product customization has been successfully completed.
                    .then(function (result) {
                        //Saving hi-res output info.
                        renderData = result;

                        //Go to the finish page.
                        goToPage("finish");
                        frameParent.unmask();
                })
                    //If there was an error thrown when completing product customization.
                    .catch(function (error) {
                        renderData = handleError(error, "Product customization completion failed with exception: ");
                });
            });

            //Opening a new product in the designer.
            $("#finishOrderPage #newDesign").click(function () {
                goToPage("design");
            });
            
            //Reopening the product in the designer.
            $("#approvePage #lnkEditAgain").click(function () {
                loadProduct(saveData.stateId);
                goToPage("design");
            });
            
            //Initializing the approval and the finish pages with the product info.

            //Initializing the approval page with links to the proof images.
            function setApprovePageData() {
                var previewElements = $("#approvePage .previewImg").attr("src", "");
                for (var i = 0; i < approveData.proofImageUrls.length && i < previewElements.length; i++) {
                    previewElements[i].setAttribute("src", approveData.proofImageUrls[i]);
                }
            };

            //Initializing the finish page with print-ready output URL and a link for reopening the product in the designer for further editing.
            function setFinishPageData() {
                $("#finishOrderPage #hiResLink").attr("href", renderData.hiResOutputUrls[0]);
                $("#finishOrderPage #linkForFeatureEdit").attr("href", renderData.returnToEditUrl);
            };

            function setEditorPageData() {
                loadProduct(productDefinition);
            };
            
            //Wizard navigation.
            
            function goToPage(to) {
                setupPage(to);
            }
            window.onpopstate = function (e) { setupPage(e.state); }

            var workflowPages = {
                "design": { elements: $("#editorPage"), setData: setEditorPageData },
                "approve": { elements: $("#approvePage"), setData: setApprovePageData },
                "finish": { elements: $("#finishOrderPage"), setData: setFinishPageData }
            }

            function setupPage(page) {
                var destPageData = workflowPages[page]

                if (typeof destPageData.setData === "function")
                    destPageData.setData();

                workflowPages[currentPage].elements.fadeOut(function () {
                    destPageData.elements.show();

                    currentPage = page;
                });
            }
        })
    </script>
</head>
    
<body>
    <div id="wrapper">
        <div id="content">
            <!-- Design page -->
            <div id="editorPage" class="area">
                <div id="iframeWrapper">
                    <iframe id="editorFrame" width="100%" height="800px"></iframe>
                </div>
                <div id="saveAndNextButtonsWrapper">
                    <!-- Finish design button -->
                    <input id="nextButton" type="button" class="btn btn-success btn-lg" value="Finish design >" />
                </div>
            </div>
            <!-- Approval page -->
            <div id="approvePage" class="area" style="display: none">
                <div class="container-fluid">
                    <h1>Approve Your Product</h1>
                        <!-- proof images -->
                        <img class="previewImg" id="preview" />
                        <img class="previewImg" id="previewPage2" />
                </div>
                <p>
                    <div id="approveButtonWrapper">
                    <input id="approveButton" type="button" class="btn btn-success btn-lg" value="Approve >" />
                    </div>
                </p>
                <div class="return">
                    <a id="lnkEditAgain">< I want to make some changes</a>
                </div>
            </div>

            <!-- Finish page -->
            <div id="finishOrderPage" class="area" style="display: none">
                <h1 class="">Your Product is Ready</h1>
                    <ul>
                        <!-- a link for downloading hi-res output -->
                        <li>The print-ready file can be downloaded from <a id="hiResLink">this link</a></li>
                        <!-- a link to reopen the product in the web-to-print designer -->
                        <li>You can return to designing the product by initializing the designer using <a id="linkForFeatureEdit">this link</a></li>
                    </ul>
                <div class="right">
                    <input id="newDesign" type="button" class="btn btn-info btn-lg" value="< New design" />
                </div>
            </div>
        </div>
    </div>
</body>
</html>

Additionally, you can copy the following CSS snippet and save it to the index.css file in the same folder where the index.html is placed. The styles will give your page a more user-friendly interface, however, it is not mandatory.

CSS
body {
    font: 12px/18px Arial, "Helvetica CY", "Nimbus Sans L", sans-serif;
    height: 100%;
    min-width: 960px;
}

.area,
.fluid {
    position: relative;
}

.area {
    width: 940px;
    margin: 0 auto;
}

#editorFrame {
    display: block;
    border: 0;
}

#content {
    padding: 0px 0px 100px;
}

#saveAndNextButtonsWrapper {
    text-align: right;
    margin-top: 10px;
}

#approvePage {
    font: 14px Tahoma,Arial,Helvetica,sans-serif;
}

#approvePage .previewImg {
    max-width: 600px;
    max-height: 600px;
    box-shadow: rgba(100, 100, 100, 0.8) 0 0 3px;
    margin-top: 1em;
}

#approvePage .container-fluid {
    text-align: center;
}

#approvePage .agree {
    border: dashed 2px #f00;
    text-align: left;
    padding: 4px 16px 16px;
    margin-top: 15px;
}

#approvePage #approveButtonWrapper {
    text-align: right;
}

#approvePage .return{
    margin-top: 20px;
    font-size: 14pt;
}

#approvePage .return a, a:active, a:focus {
    border-bottom: 1px solid rgb(169, 227, 149);
    color: rgb(100, 188, 70);;
}

#approvePage .return a:hover {
    color: rgb(43, 121, 16);
    text-decoration: none;
    cursor: pointer;
}

#finishOrderPage h3 {
    text-align: justify;
}

#finishOrderPage h1 {
    text-align: center;
}

#finishOrderPage .right {
    text-align: right;
}

#finishOrderPage ul {
    list-style-type: disc;
}

See Also

Manual

IFrame API Reference