Skip to main content

Case study of implementing Mixin

Introduction

Prior to working with Avada projects, we have been working with Magento 2, Laravel, and NestJS. Those big frameworks have something in common. They have their core logic along with an extensible modular architecture. For example, in Magento 2, in order to change the behavior of a Magento 2 core function, you need to use something so-called Plugin. This mechanism allows you to intercept before, after and around a function execution. You can alternate the outcome of a core function such as: add more extra fee to the subtotal of the order.

This approach along with Observer/Listener design pattern opens a flexible and extensible architecture for third-party modules to extend Magento 2 features even more (it is still monolith anyway).

The challenge pose

In the Order Limit app, developers start to receive more request on customizing, modifying core behaviors of the app. This later on poses a challenge in code design patterns and structuring in which to isolate the core logic from the customization logic via direct and public API.

Therefore, we decided that we should structure our frontend SDK code to a more OOP-like approach in favor of inheritance and extensibility.

Previously, we have like DisplayManager and ApiManager to manager service-based functionalities. It looks like this (just mock code):


import {render} from 'preact';
import React from 'preact/compat';

export default class DisplayManager {
constructor() {
this.programs = [];
this.settings = {};
}
async initialize({programs, settings}) {
this.programs = programs;
this.settings = settings;

this.initBlockCheckoutButton();
}

initBlockCheckoutButton() {
const selectors = this.getSelectors();

selectors.forEach((selector) => {
document.querySelector(selector).disabled = true;
})
}

getSelectors() {
return [
'.checkout-btn',
// more selectors here
]
}
}

This approach handles the displaying of the widget, however, this allows us no mechanism to somehow like: "We want to customer the selectors, or we want to delay 300ms before blocking the checkout buttons". Actually, we can add a setting fields in the dev zone to tweak the code logic like: Delay time input, or Delay time before blocking. However, this poses a challenge of having infinite numbers of inputs to customize infinite need of customers because there are a lot of themes in the market.

Rethinking that if we keep adding the inputs, it would be quite hard to maintain. We wonder if there would be a way to write custom JS code to modify the current behavior via a window variable like:


window.avadaOrderLimit.registerMixin('beforeInitBlockCheckoutButton', () => {
// Do something here
})

Implementing Mixin

With Mixin, we are able to intervene the execution of the function: before, after, and override the original function. So we need a BaseManager which allow to run this via naming convention:

class BaseManager {
__init(hooks = {}) {
// Assign hooks dynamically to the instance
Object.keys(hooks).forEach(hook => {
// Bind each hook to this instance to ensure correct `this` context
// Arrow functions are used to automatically capture the lexical `this` context
this[hook] = hooks[hook].bind(this);
// console.log('The this to bind', this);
});
// Initialize method wrapping to include hook logic and check for overrides after construction
this.wrapMethodsWithHooks();
}

wrapMethodsWithHooks() {
Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach(method => {
// Filter to wrap only the relevant methods (ignoring constructor, non-function properties, and utility methods)
if (
typeof this[method] === 'function' &&
method !== 'constructor' &&
!method.startsWith('wrap')
) {
const original = this[method].bind(this); // Ensure original method is bound to `this`

// Redefine the method to include pre- and post-execution hooks
this[method] = (...args) => {
// Execute the before hook if it exists
const beforeHook = `before${method.charAt(0).toUpperCase() + method.slice(1)}`;
if (typeof this[beforeHook] === 'function') {
this[beforeHook](...args);
}

// Execute the override hook if it exists; otherwise, run the original method
const overrideHook = `override${method.charAt(0).toUpperCase() + method.slice(1)}`;
let result;
if (typeof this[overrideHook] === 'function') {
result = this[overrideHook](original, ...args);
} else {
result = original(...args);
}

// Execute the after hook if it exists
const afterHook = `after${method.charAt(0).toUpperCase() + method.slice(1)}`;
if (typeof this[afterHook] === 'function') {
result = this[afterHook](result, ...args);
}

return result;
};
}
});
}
}

export default BaseManager;

With this BaseManager, you can extend the DisplayManger class and expose a direct API for modification. So, in order to do this, you will need developer to extract the logic to smaller function in favor of Single Responsibility, because without extracting, you cannot use the mixin on the function, which later on forcing coding quality.


displayManager.registerHooks({
/**
* Take before params from the original functions
*/
beforeInitialize(params) {
// Do before
},

/**
* Take all params from the original functions
*/
afterInitialize(params) {
// Do after
},

/**
* First param is the original function, second is all params from the original functions
* @param originalFunc
* @param params
* @returns {*}
*/
overrideInitialize(originalFunc, params) {
if (something) {
return originalFunc();
}
}
})