Advanced Custom Options Display

The Expivi embed application consist out of 2 separate application. The 3D viewer and the options:

On left the 3D viewer and on right the options.

The options application provides various capabilities on how an option should be represented. As such it is possible to change the display type of an option inside the attribute’s settings:

User can change the appearance of an option by changing the display function

Expivi provides the ability to create a custom display function to represent the option to the end user.

Architecture

The options application is created in Vue.js (https://vuejs.org/). The application follows an MVC like design, where each attribute has a component as a controller to handle the logic and a view component to represent the option to end-user:

Vue component design

The Base node component loads the controller node component dynamically based on the attribute’s type and the controller node loads in the view component dynamically based on the display function selected by the user in Attribute’s settings.

Since the attributes are defined hierarchically, where each attribute can contain other attributes as children and those attributes can contain other attributes as children and so forth, the Base node renders all the attribute’s children as itself in it’s content recursively until all the attributes are rendered.

Custom display functions

While some attributes such as question and material group can use the same display functions, since they can provide the same model data to their view components, some other types such as image upload have a different model data requiring their own specialized view components. As such writing a vue component for each attribute type requires providing the input & output specialized to that attribute type.

Question & Material group

The controller nodes for question and material group attributes provide the same model data to their view component.

Provided data

Data passed to the view component are as follow:

{
label: string,// The name of the attribute
value: number|string,// The current value selected
items: {
id: number|string,// Value's id
label: string,// Name
description: string,// Description
thumbnail: string|null,// Url to thumbnail
color: hexstring|null,// Preferred background color
price: number|null,// Decimal number for price
visible: boolean,// If value is visible
disabled: boolean,// If value should be disabled
filtered: boolean,// If value is filtered out
categories: string[]// Value's categories
}[],
loading: boolean,// If the controller is loading
size: 'sm'|'lg',// Preferred size by user
rounded: 'yes'|'no',// Preferred rounded corners 
node_id: Number,// The id of the attribute
}

Where the items is an array of all the possible values the customer can select from.

Returning the selected value

In order to pass the selected value by user back to the application, such that the selected values is applied on the configurator, the component should emit an event with name ‘input’ and data as the value’s id.

E.g. selecting the first value provided in items:

this.$emit('input', this.items[0].id);

Example component

In our example, the following component is expecting the attribute to contain exactly 2 values. The component will render a checkbox, where attribute’s first value is selected if the checkbox is unchecked and the attribute’s second value is selected if the checkbox is checked:

const CheckboxSelectorView = {
    template: `
<div>
    <input type="checkbox" :id="'checkbox_group_'+node_id" v-model="selected" />
    <label :for="'checkbox_group_'+node_id">{{label}}</label>
</div>
    `,
    data() {
        return {
            selected: false
        }
    },
    props: {
        label: String,
        value: {
            default: null
        },
        items: Array,
        loading: {
            default: false
        },
        size: {
            type: String,
            default: 'lg'
        },
        rounded: {
            type: String,
            default: 'no'
        },
        node_id: Number,
    },
    created () {
        this.updateCheckbox();
    },
    methods: {
        updateCheckbox(){
            this.selected = this.items.length > 0 && this.value != this.items[0].id;
        }
    },
    watch:{
        value(aNewValue){
            this.updateCheckbox();
        },
        selected(aNewValue){
            if(aNewValue && this.items.length > 1){
                this.$emit('input', this.items[1].id);
            }else if(this.items.length > 0){
                this.$emit('input', this.items[0].id);
            }
        }
    }
};

Using custom display functions

To use the display function, the component has to be registered on the option’s application as well as setting the custom display function on the attribute.

Registering custom display functions

To register the custom display function, the component has to be registered before instantiating the ExpiviComponents object (the option’s application). The component can be registered on the ExpiviOptionsVueInstance.prototype.$customViews object.

e.g. registering the CheckboxSelectorView:

ExpiviOptionsVueInstance.prototype.$customViews["Checkbox"] = CheckboxSelectorView;

In above example the CheckboxSelectorView is registered as Checkbox.

Setting the custom display function

To set the custom display function, select Custom display and enter the name of registered component in under Display name.

e.g. setting the custom display to Checkbox as e.g. provided in Registering custom display functions:

Example code

Below you’ll find a complete example code of the custom display functionality.

<html>
<head>
<title>Expivi - 3D Configurator sample code</title>
<link href="https://assets.expivi.net/options/latest/css/app.css" rel="stylesheet" />
<script src="https://assets.expivi.net/viewer/latest/viewer.js"></script>
<script src="https://assets.expivi.net/options/latest/js/app.js"></script>

<script src="https://unpkg.com/vue@2"></script>

<style>
.expivi-single-product-container {
display: table;
height: 100%;
width: 100%;
}

.expivi-single-product-container .expivi-viewer-container {
display: table-cell;
width: 64%;
padding-right: 15px;
vertical-align: top;
}

.expivi-single-product-container .expivi-option-container {
height: 100%;
display: table-cell;
width: 36%;
max-width: 400px;
}

@media (max-width: 600px) {
.expivi-single-product-container {
display: block;
}

.expivi-single-product-container .expivi-viewer-container {
height: 40vh;
display: block;
width: 100%;
padding-right: 0;
}

.expivi-single-product-container .expivi-option-container {
height: initial;
display: block;
width: 100%;
}
}
</style>
</head>
<body style="width: 100%;height: 100%;margin:0;padding:0">

<div class="expivi-single-product-container">
<div class="expivi-viewer-container">
<div id="viewerContainer" style="position: sticky;top:0;width:100%;height:100vh;"></div>
</div>
<div class="expivi-option-container">
<div id="priceContainer"></div>
<div id="optionsContainer"></div>
</div>
</div>

<script type="text/javascript">
var Token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjQ3MWYwZTdiMDM4YmU3MmM5OTAxNGRhOGYwNmZiZTA0ZGM2YWUwZGMxMzBkMDA0OWE0ZTJkOTYzYTc1NTIyMjM3NWU0MjUzMDc3MjZmNjExIn0.eyJhdWQiOiIxIiwianRpIjoiNDcxZjBlN2IwMzhiZTcyYzk5MDE0ZGE4ZjA2ZmJlMDRkYzZhZTBkYzEzMGQwMDQ5YTRlMmQ5NjNhNzU1MjIyMzc1ZTQyNTMwNzcyNmY2MTEiLCJpYXQiOjE1OTkwMzUyMjgsIm5iZiI6MTU5OTAzNTIyOCwiZXhwIjoxOTE0NTY4MDI3LCJzdWIiOiIzIiwic2NvcGVzIjpbXX0.po5JOHLo5-y2-WJCdB6Yb29S20hoiQQFc7BA6i1c0FL4eRBDv6nC_2Bwv88UO8ZjvCjpxMY97M7acW7xjwbVaH-Dj69ZaDn4ynaX-o6zNFW0iVubAs7IK52nO5mGi0lZzvv2A0pzohOvr0ySy2g7lKgV5lXou2h-DIw-jmgtt_04bk_YTqKbPtMnxQa4NZoeytBt9U3ImmDWWHE-iQ06o0u13cyJfHOaJE9NLA_5EE99d1yE20vBorukIn_e61OJZfduQ6C6kOTI6cUJfmT8DEfalolTuAnhdgBHDrYXKsKyStyQd89pSgTp6aIt2w-SeOgwt9bKK0BZf5rIOF25Q4-JZXSCcB8aE-ng8RNsf_N2OCdAfCCKj9rrxpx3rXa1xI-dSKB2CDgQJAqof-92F7Ea6nCGRXwUl0oc9rTrQFfiG2vAcWrWS_7kI_wKU2ZU8v-6U41EDbikdlfk2xFgumchiqOk2o528jCD1STQFdAEKklYPjAiFtis1Ql_6HOQQXCGL6VabPoyWxmJtZ5THWJnIPBkhVwrOZoEbJupL8NwD87Du0ReGhuSyRZF9o2r4zyZCxky2q87Vx5lw_2cFmpgSDw5-megZ_Je4aX3_25fLEoxEgWCXmyxDMFeIxDU97Nzx0FRdNvCVRolkUGtTB3_g3Fz5gOeB3R9YJcgnlg";

window.addEventListener('load', function () {

let CheckboxSelectorView = Vue.component('checkbox', {
props: {
label: String,
value: {
default: null
},
items: Array,
loading: {
default: false
},
size: {
type: String,
default: 'lg'
},
rounded: {
type: String,
default: 'no'
},
node_id: Number,
},
data: function () {
return {
selected: null
};
},
methods: {
updateCheckbox(){
this.selected = this.items.length > 0 && this.value != this.items[0].id;
}
},
template: `
<div>
<h2>Our custom checkbox component:</h2>
<input type="checkbox" :id="'checkbox_group_'+node_id" v-model="selected" />
<label :for="'checkbox_group_'+node_id">{{label}}</label>
</div>
`,
watch: {
value(){
this.updateCheckbox();
},
selected(aNewValue){
if(aNewValue && this.items.length > 1){
this.$emit('input', this.items[1].id);
}else if(this.items.length > 0){
this.$emit('input', this.items[0].id);
}
}
}
});

ExpiviOptionsVueInstance.prototype.$customViews["Checkbox"] = CheckboxSelectorView;

const expiviInstance = new ExpiviComponent({
catalogueId: 67,
viewerContainer: "#viewerContainer",
optionContainer: "#optionsContainer",
priceContainer: "#priceContainer",
token: Token
});
});
</script>

</body>
<html>