This topic dwells on how to handle product customization using the Customer's Canvas IFrame API. Here, we discuss such tasks as saving products, approving designs, and rendering the hi-res output. The Customer's Canvas IFrame API provides three methods to perform these tasks: Editor.saveProduct, Editor.getProofImages, and Editor.finishProductDesign.
In this topic, you can find the sample demonstrating how to handle product loading, save products, display proof images, reload products for further editing, and provide links for the 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.
The Editor.saveProduct method allows for saving a product's 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 as the sample shows. Also, you can implement the autosave feature by calling this method after equal periods of time using a timer.
If you want to allow your users to name saved products or rewrite products, you can use the stateId optional parameter of saveProduct. When you omit this parameter, saveProduct creates file names using unique product state identifiers (GUID). As a result, this method returns the product state ID, user ID, and the return-to-edit URL. Having these details, you can reload the product by its name.
The saveProduct method returns a Promise, whose the onFulfilled callback function accepts an object implementing the ISaveProductResult interface. This interface has the following properties:
//Saving a product to the StateFileName.st file. editor.saveProduct("StateFileName") //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); });
All the API functions discussed in this topic should be called in a page context where the Customer's Canvas editor is hosted.
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.
Customer's Canvas allows you to render three types of proof images: a page thumbnail, a real-size page preview, and product thumbnails as a multi-page PDF. All of these proof images are 72 DPI. To configure proof images, you can pass the following options to getProofImages:
All these properties are optional. By default, this method renders a page thumbnail. You can pass these properties as follows:
//Getting links to proof images. editor.getProofImages({maxHeight: 640, maxWidth: 640, pregeneratePreviewImages: true}) //Handling the results. .then(function (result) { ... }) .catch(function (error) { ... });
To specify the required size, you can pass the maximum width and height. If you omit these parameters, the default value of 500
will be applied. This method proportionally resizes proof images according to maxHeight and maxWidth. For example, if the maximum width and height are both set to 640
pixels, then a 1280 x 960
px image will be resized to 640 x 480
px. The previous example sets the width and height of a proof image to 640
pixels.
You can also pass the pregeneratePreviewImages option to start generating proof images in the background when running this method. By default, getProofImages only returns links to proof images and starts generating these images when a request to download them is received.
This method returns a Promise, whose the onFulfilled callback function accepts an object implementing the IProofResult interface. This interface has the only property:
//Getting links to proof images. editor.getProofImages() //If the links to proof images were generated successfully. .then(function (result) { 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); });
When you generate product previews in PDF files and allow your users to directly download these files using the links returned by this method, you can pass the filename query parameter that enables the download and customizes the resulting file name. For example, to download a booklet.pdf
file:
https://example.com/api/rendering/GetProofImage/JohnWood/06047be9-78fb-4b89-98c5-8b1712ba1b51/-1_-1.pdf?filename=booklet
The last step, after a user has approved the design, is rendering the hi-res output. To perform this, call the finishProductDesign method. This method saves the current product state and returns links to the hi-res output. The finishProductDesign method returns a Promise, whose the onFulfilled callback function accepts an object implementing the IFinishDesignResult interface. This interface has the following properties:
~\userdata\<someUserId>\states\
folder, so these proof images can be regenerated if a cache cleanup occurs.default
user identifier.//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 this callback, so it needs clarification. As pointed out 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 this, your system should store values entered by the user in a database and pass them off 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:
{ inStringPlaceholders: [ { name: "placeholder_name", usersValue: "placeholder_value" }, ... ], mockups: [ { mockup: "mockup_name", surfaceName: "surface_name" }, ... ], texts: [ { name: "layer_name", usersValue: "layer_text", isNewItem: "new_or_changed", isRemovedItem: "removed_or_changed" }, ... ], surfaces: { surface_0: { inStringPlaceholders: [ { name: "placeholder_name", usersValue: "placeholder_value" }, ... ], texts: [ { name: "layer_name", usersValue: "layer_text", isNewItem: "new_or_changed", isRemovedItem: "removed_or_changed" }, ... ] } } }
Here, the inStringPlaceholders and texts arrays contain text entered by the user in text layers. For each text layer that was changed by the user, the corresponding object contains its name and new value. Elements in texts contain the isNewItem property, which is true
for new layers and false
for changed layers, and the isRemovedItem property, which becomes true
if the user deleted the text layer. Customer's Canvas returns these arrays twice: at the top level of IUserChanges and in the surfaces object containing changes on a per-surface basis. In the previous example, surface_0 is the surface name. Elements of the mockups array contain mockup names that the user set up for the surfaces. For information about text placeholders, see the In-String Placeholders and Text Validation topic. Text layers and mockups are discussed in the Point and Paragraph Text and Mockups topics, correspondingly.
As an optional parameter, you can pass the following object to finishProductDesign.
editor.finishProductDesign({fileName: "envelope", proofMaxHeight: 640, proofMaxWidth: 640, stateId: "StateFileName", pregeneratePreviewImages: true}) .then(function (result) { ... }) .catch(function (error) { ... });
All properties of this object are optional. You can pass the filename to customize the resulting file name. This name is only used when you allow the end-user to directly download 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 the result.ext
name where .ext
stands for the actual file extension for the 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. You can find the information on how to set the type of the hi-res output in the Configuring Hi-res Output topic.
If you want to name or rewrite saved state files, you can use the stateId optional parameter. When you omit this parameter, finishProductDesign creates file names using unique product state identifiers.
This method takes the same options for proof images as getProofImages.
State files are internal Customer's Canvas files, which are created when products are saved via the Editor.saveProduct or Editor.finishProductDesign methods.
State files are located in user subfolders and have names like aaaaaaaa-bbbb-cccc-xxxx-yyyyyyyyyyyy.st
by default, where aaaaaaaa-bbbb-cccc-xxxx-yyyyyyyyyyyy is the unique 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 an extension and user identifier to the loadEditor method as follows:
//The finishing of the product customization. editor.finishProductDesign() .then(function (result) { ... //The unique identifier of the saved product state. stateId = result.stateId; //The user identifier. userId = result.userId; ... }) .catch(function (error) { ... }); ... //The loading of the previously saved product. CustomersCanvas.IframeApi.loadEditor(iframe, stateId, { userId: userId });
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:
To make this sample work do the following:
index.html
file.example.com
with the name of the site where your Customer's Canvas instance is hosted and save the file.index.html
in a browser.Alternatively, you can download this sample as a zip file and extract its content into the root folder of Customer's Canvas.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta charset="utf-8" /> <title>Business cards - The sample page - Customer's Canvas</title> <script type="text/javascript" src="http://code.jquery.com/jquery-2.1.1.min.js"> </script> <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/Generated/IframeApi.js"> </script> <script> $(function () { //Defining the product. var productDefinition = { surfaces: ["stamp"] }; //Defining the editor configuration. var configuration = { widgets: { FinishButton: { mode: "Disabled" } }}; var editorFrame = $("#editorFrame"); var editor = null; function handleError(error, message) { console.error(message, error); return error; }; var loadData = null; //Loading a product into the editor. CustomersCanvas.IframeApi.loadEditor(editorFrame[0], productDefinition, configuration) //If the editor has been successfully loaded. .then(function (e) { editor = e; }) //If there was an error thrown when loading the editor. .catch(function (error) { loadData = handleError(error, "Load failed with exception: "); }); 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 the product. $("#editorPage #saveButton").click(function () { editor.saveProduct() //If the product has been successfully saved. .then(function (result) { //Saving the 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 links to proof images. $("#editorPage #nextButton").click(function () { editor.getProofImages() //If proof images have been successfully received. .then(function (result) { //Saving proof images info. approveData = result; //Go to the approval page. goToPage("approve"); }) //If there was an error thrown while getting the proof images. .catch(function (error) { approveData = handleError(error, "Getting proof images failed with exception: "); }); }); //Finishing the product customization. $("#approvePage #approveButton").click(function () { editor.finishProductDesign() //If the product customization has been successfully completed. .then(function (result) { //Saving hi-res output info. renderData = result; //Go to the finish page. goToPage("finish"); }) //If there was an error thrown while finishing the 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 () { editor.revertProduct(); goToPage("design"); }); //Reopening the product in the designer. $("#approvePage #lnkEditAgain").click(function () { goToPage("design"); }); //Initializing the approval and 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 the 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]); }; //The wizard navigation. function goToPage(to) { setupPage(to); } window.onpopstate = function (e) { setupPage(e.state); } var workflowPages = { "design": { elements: $("#editorPage") }, "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"> <!-- The design page --> <div id="editorPage" class="area"> <div id="iframeWrapper"> <iframe id="editorFrame" width="100%" height="800px"></iframe> </div> <div id="saveAndNextButtonsWrapper"> <!-- The Save button --> <input id="saveButton" type="button" class="btn btn-info btn-lg" value="Save" /> <!-- The Finish design button --> <input id="nextButton" type="button" class="btn btn-success btn-lg" value="Finish design >" /> </div> </div> <!-- The 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> <!-- The finish page --> <div id="finishOrderPage" class="area" style="display: none"> <h1 class="">Your Product is Ready</h1> <!-- The link for downloading the hi-res output. --> The print-ready file can be downloaded from <a id="hiResLink">this link</a> <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. These styles will give your page a more user-friendly interface, however, it is not mandatory.
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; }