The Wix editor is made using React, and likewise the frontend for it also renders the site using React components. This poses a few restrictions in terms of data acccess while also imposing limits on how you can interact with information on the page.
As a result the code for Clerk is segmented in 4 main sections:
- Affiliate tracking script.
Clerk.js, located in a field outside the Editor or Theme.
- Utility functions.
Utility function library located in the Theme public
resource.
- Custom element scripts.
A number of custom-element.js files located in the Theme public/custom-elements
resource.
- Element hydration scripts.
A number of snippets placed in the appropriate controllers page.js file which hydrate elements with contextual data before hydration.
This script loads the Clerk.js library and class into the window. This is needed for all pages, to be loaded once in the header.
clerk.js
makes Clerk(**args)
available in window scope.
<script>
(function(w,d){
const CLERK_INIT_CLASS = 'clerk_manual';
w.clerk_init_class = CLERK_INIT_CLASS;
var e=d.createElement('script');e.type='text/javascript';e.async=true;
e.src=(d.location.protocol=='https:'?'https':'http')+'://cdn.clerk.io/clerk.js';
var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(e,s);
w.__clerk_q=w.__clerk_q||[];w.Clerk=w.Clerk||function(){ w.__clerk_q.push(arguments) };
})(window,document);
Clerk('config', {
key: 'PUBLIC_API_KEY'
});
</script>
In order to keep the setup organized all functions used to retrieve contextual data, are stored in public/clerk-wix.js
.
These functions are imported in the element hydration scripts, in order to populate data before Clerk initialization.
clerkGetCart()
gets cart object, contex anywhere.
export const clerkGetCart = async () => {
const currentCart = await cart.getCurrentCart();
const cartInfo = { cartId: currentCart._id, cartLineItems: currentCart.lineItems }
return cartInfo;
}
clerkGetCartProducts()
gets product ids in cart, contex anywhere.
export const clerkGetCartProducts = async () => {
const cart = await clerkGetCart();
const product_ids = cart.cartLineItems.map(line_item => {
return line_item?.productId;
});
return product_ids;
}
clerkGetQuery()
gets value of q param if present, contex anywhere.
export const clerkGetQuery = (wixLocation) => {
return wixLocation.query['q'];
}
clerkGetProduct()
gets the current product id, context product page.
export const clerkGetProduct = async () => {
const product = await $w('#productPage1').getProduct();
return product?._id;
}
clerkGetOrder()
gets the current order, context thank you page.
export const clerkGetOrder = async () => {
const order = await $w('#thankYouPage1').getOrder();
return order;
}
clerkHydrateBasketTracking()
hydrates basket tracking, context anywhere.
export const clerkHydrateBasketTracking = async (selector_list=['']) => {
const product_ids = await clerkGetCartProducts();
selector_list.forEach(el => {
if($w(el).length !== 0){
$w(el).setAttribute('data-products', product_ids);
}
});
}
clerkHydrateSalesTracking()
hydrates sales tracking, context thank you page.
export const clerkHydrateSalesTracking = async (selector_list=['']) => {
const order_details = await clerkGetOrder();
const order_products = order_details.lineItems.map(line_item => {
return {id: line_item.productId, quantity: line_item.quantity, price: line_item.tax + line_item.priceData.price}
});
selector_list.forEach(el => {
if($w(el).length !== 0){
$w(el).setAttribute('data-sale', order_details._id);
$w(el).setAttribute('data-email', order_details.buyerInfo.email);
$w(el).setAttribute('data-products', order_products);
}
});
}
clerkHydrateCartSlider()
hydrates slider with product ids in cart, context anywhere.
export const clerkHydrateCartSlider = async (selector_list=['']) => {
const product_ids = await clerkGetCartProducts();
selector_list.forEach(el => {
if($w(el).length !== 0){
$w(el).setAttribute('data-products', `${JSON.stringify(product_ids)}`);
}
});
}
clerkHydrateProductSlider()
hydrates slider with current product id, context product page.
export const clerkHydrateProductSlider = async (selector_list=['']) => {
const product_id = await clerkGetProduct();
selector_list.forEach(el => {
if($w(el).length !== 0){
$w(el).setAttribute('data-products', `["${product_id}"]`);
}
});
}
clerkHydrateSearchPage()
hydrates search page with query, context search page.
export const clerkHydrateSearchPage = (selector_list=[''], wixLocation) => {
const query = clerkGetQuery(wixLocation);
selector_list.forEach(el => {
if($w(el).length !== 0){
$w(el).setAttribute('data-query', query);
}
});
}
Components are created as Custom Elements. All elements created for Clerk use the following pattern clerk-*
in the tag naming convention.
<clerk-slider>
clerk slider element, context anywhere.
// Attributes with significance which are expected to be static.
const RESERVED_ATTRIBUTES = [
'template',
'keywords'
];
const keywordsRegex = /([[\]"',])/g;
const mutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.attributeName !== "data-products"
) {
return
}
mutation.target.removeAttribute('data-clerk-content-id');
mutation.target.innerHTML = '';
window.Clerk('content', `.${window.clerk_init_class}`);
}
}
const observer = new MutationObserver(mutationCallback);
class clerkSlider extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return RESERVED_ATTRIBUTES;
}
attributeChangedCallback(name, oldValue, newValue) {
if(this && newValue){
if(RESERVED_ATTRIBUTES.includes(name)) {
this.removeAttribute(name);
}
if(name === 'template'){
// If input template, set data attribute and force ID on element
if(newValue.includes('@')){
this.dataset.template = newValue;
this.id = newValue.replace('@', '');
}
}
if(name === 'keywords'){
// If input given as JSON list remove quotes and brackets
this.dataset.keywords = (newValue.match(keywordsRegex)) ? newValue.replace(keywordsRegex, '') : newValue;
}
}
}
disconnectedCallback() {
observer.disconnect();
}
connectedCallback() {
this.className = window.clerk_init_class;
window.Clerk('content', `.${window.clerk_init_class}`);
observer.observe(this, {attributes: true, childList: false, characterData: false});
}
}
customElements.define('clerk-slider', clerkSlider);
<clerk-live-search>
clerk live search element, context anywhere.
// Attributes with significance which are expected to be static.
const RESERVED_ATTRIBUTES = [
'template',
'instant-search',
'instant-search-suggestions',
'instant-search-categories',
'instant-search-pages',
'instant-search-positioning'
];
class clerkLiveSearch extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return RESERVED_ATTRIBUTES;
}
attributeChangedCallback(name, oldValue, newValue) {
if(this && newValue){
if(RESERVED_ATTRIBUTES.includes(name)) {
this.removeAttribute(name);
}
if(name === 'template'){
// If input template, set data attribute and force ID on element
this.dataset.template = newValue;
this.id = newValue.replace('@', '');
}
if(name === 'instant-search'){
this.dataset.instantSearch = newValue;
}
if(name === 'instant-search-suggestions'){
this.dataset.instantSearchSuggestions = newValue;
}
if(name === 'instant-search-categories'){
this.dataset.instantSearchCategories = newValue;
}
if(name === 'instant-search-pages'){
this.dataset.instantSearchPages = newValue;
}
if(name === 'instant-search-positioning'){
this.dataset.instantSearchPositioning = newValue;
}
}
}
connectedCallback() {
// Setting Default values and propagating event.
this.className = window.clerk_init_class;
this.dataset.template = '@live-search';
this.dataset.instantSearch = 'input[type="search"]';
this.dataset.instantSearchSuggestions = 5;
this.dataset.instantSearchCategories = 5;
this.dataset.instantSearchPages = 5;
this.dataset.instantSearchPositioning = 'left';
window.Clerk('content', `.${window.clerk_init_class}`);
}
}
customElements.define('clerk-live-search', clerkLiveSearch);
<clerk-search>
clerk search page element, context search page.
// Attributes with significance which are expected to be static.
const RESERVED_ATTRIBUTES = [
'template',
'target',
'facets-target',
'facets-attributes',
'facets-titles',
'facets-price-prepend',
'facets-in-url',
'facets-view-more-text',
'facets-searchbox-text',
'facets-design'
];
const DEFAULT_INNER_HTML = `<div id="clerk-search-page-wrap">
<div id="clerk-facets-wrap">
<div id="clerk-facet-toggle"></div>
<div id="clerk-search-filters"></div>
</div>
<div id="clerk-search-results"></div>
</div>`;
const mutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.attributeName !== "data-query"
) {
return
}
mutation.target.removeAttribute('data-clerk-content-id');
mutation.target.innerHTML = DEFAULT_INNER_HTML;
window.Clerk('content', `.${window.clerk_init_class}`);
}
}
const observer = new MutationObserver(mutationCallback);
class clerkSearch extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return RESERVED_ATTRIBUTES;
}
attributeChangedCallback(name, oldValue, newValue) {
if(this && newValue){
if(RESERVED_ATTRIBUTES.includes(name)) {
this.removeAttribute(name);
}
if(name === 'template'){
// If input template, set data attribute and force ID on element
this.dataset.template = newValue;
this.id = newValue.replace('@', '');
}
if(name === 'target'){
this.dataset.target = newValue;
}
if(name === 'facets-target'){
this.dataset.facetsTarget = newValue;
}
if(name === 'facets-attributes'){
this.dataset.facetsAttributes = newValue;
}
if(name === 'facets-titles'){
this.dataset.facetsTitles = newValue;
}
if(name === 'facets-price-prepend'){
this.dataset.facetsPricePrepend = newValue;
}
if(name === 'facets-in-url'){
this.dataset.facetsInUrl = newValue;
}
if(name === 'facets-view-more-text'){
this.dataset.facetsViewMoreText = newValue;
}
if(name === 'facets-design'){
this.dataset.facetsDesign = newValue;
}
}
}
disconnectedCallback() {
observer.disconnect();
}
connectedCallback() {
// Setting Default values and propagating event.
this.className = window.clerk_init_class;
this.dataset.template = '@search-page';
this.dataset.target = '#clerk-search-results';
this.dataset.facetsTarget = '#clerk-search-filters';
this.innerHTML = DEFAULT_INNER_HTML;
window.Clerk('content', `.${window.clerk_init_class}`);
observer.observe(this, {attributes: true, childList: false, characterData: false});
}
}
customElements.define('clerk-search', clerkSearch);
<clerk-sales-tracking>
sales tracking element, context thank you page.
class clerkSalesTracking extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.className = window.clerk_init_class;
this.dataset.api = 'log/sale';
window.Clerk('content', `.${window.clerk_init_class}`);
}
}
customElements.define('clerk-sales-tracking', clerkSalesTracking);
<clerk-backet-tracking>
clerk basket tracking element, context anywhere.
const mutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.attributeName !== "data-products"
) {
return
}
mutation.target.removeAttribute('data-clerk-content-id');
mutation.target.innerHTML = '';
window.Clerk('content', `.${window.clerk_init_class}`);
}
}
const observer = new MutationObserver(mutationCallback);
class clerkBasketTracking extends HTMLElement {
constructor() {
super();
}
disconnectedCallback() {
observer.disconnect();
}
connectedCallback() {
this.className = window.clerk_init_class;
this.dataset.api = 'log/basket/set';
window.Clerk('content', `.${window.clerk_init_class}`);
observer.observe(this, {attributes: true, childList: false, characterData: false});
}
}
customElements.define('clerk-basket-tracking', clerkBasketTracking);
When adding the blocks to the site, you can right click them and choose to show for all pages.
This should be done for blocks that exist on all pages, like <clerk-basket-tracking>
or <clerk-live-search>
.
These blocks are not rendered where they are positioned.
For blocks that only live on a given template, such as a <clerk-slider>
, we only need to to place them as many times as we want on a template.
We drag and resize them to the proper position, as this is where they will be shown.
Any block can be nested in any column or strip non-custom component.
The height given to a block in the WYSIWYG editor, is the minimum height not the auto or the max.
Any block can be set to have default values for hidden
and collapsed
.
hidden
visibility: hidden;
collapsed
display: none;
All blocks have a list of reserved attributes you can configure on them in the WYSIWYG editor.
The attributes follow the same structure our normal embedcodes do, with the data-*
prefix removed.
Eg. data-template => template
https://www.wix.com/velo/reference/api-overview/introduction
https://www.wix.com/velo/reference/$w
https://support.wix.com/en/article/wix-editor-adding-a-custom-element-to-your-site
https://support.wix.com/en/article/embedding-custom-code-on-your-site