Plugins allow you to extend existing functionality without changing the original code. For that, ScandiPWA makes use of the Mosaic.js
package, which introduces namespaces to easily add a unique identifier to a function or class, and provides config objects to allow applying plugins to these namespaces.
Creating a plugin function and config object depends on the type of original code you want to plug in. This can be of type function
for namespaces defined above the function, member-function
for functions of a class that has the namespace, member-property
for properties of classes that have a namespace, or static-member
for static methods of classes that define a namespace.
The plugin configuration object enables the application of multiple plugins to the same namespace, which can enhance plugin reusability and readability. It also allows you to set the priority of execution of a plugin.
ScandiPWA has some best practices for working with plugins, as well as a tool for plugin debugging.
<aside> ➡️ This guide will focus on plugins. To modify the appearance of the app, please use the override mechanism. How to override a file?
</aside>
Plugins add capabilities to an existing program without changing the original program's code.
const getData = () => {
return ['Initial data'];
}
const data = getData();
console.log(data); // ['Initial data']
This getData
function returns ['Initial data']
, but how to change its functionality without changing the function directly? Plugins allow for example to change what is returned:
const plugin = (callback) => {
const data = callback(); // ['Initial data']
return [
...data,
'Data from the plugin'
];
};
const newData = plugin(getData);
console.log(newData); // ['Initial data', 'Data from the plugin']
<aside> ⚠️ This is a limited example of how plugins work. They may also include the original function parameters and a class instance depending on the type, and also a mechanism to connect the plugin to the original function or property.
</aside>
This plugin is a function that accepts the original function as a callback and changes its behavior. If this plugin function is added to the getData
function, which means that the original function is passed as a parameter to the plugin, it returns ['Initial data', 'Data from the plugin']
.
This is why a plugin acts as a proxy between the original function or property and the caller - it intermediates between the two and adds extra functionality.
There is no limit to the number of plugins a function or property can have. However, it's important to understand the tradeoffs and best practices involved.
If need to add another plugin, the function would look like this:
const newData = plugin1(plugin2(getData));
In reality, it would be impractical to require the caller to include a plugin every time one is added. That's why a plugin mechanism is necessary to connect the caller and the original function with the plugins.
ScandiPWA is installed as an NPM module in the node_modules
directory, which means that you should not modify its source directly. Instead, ScandiPWA uses the Mosaic.js
package to allow you to customize the application's appearance or extend its functionality. To achieve this, you have two options:
Create an override - Create a new file under the same location as in the original theme. Works only for themes. Works well for UI extension that is not planned to be reused on other projects.
<aside> ⚠️ This page is not about theming. To learn more about styling, please refer to the theming page:
</aside>
Create a plugin - Create a plugin file and plugin to some namespace. This will work for both themes and extensions and function well as an extension of the application's logic. Additionally, it can be easily packaged and reused for other projects if needed.
Depiction of how third-party overrides and plugins incorporate in original source code without losing overriding it entirely or losing old code.
In the context of ScandiPWA, a plugin is a JavaScript file that can be injected into functions, methods, static properties, and even classes.
Plugins enable you to redefine the behavior and properties of objects or functions within the application. For example:
For the plugin system to work, the concept of namespace is introduced.
Namespaces are unique identifiers that can be applied to functions or classes. They are defined directly above the code they identify.
/** @namespace <NAMESPACE> */
For example:
/** @namespace Route/Checkout/Container/mapStateToProps */
export const mapStateToProps = (state) => ({
//...
});
/** @namespace Route/Checkout/Container */
export class CheckoutContainer extends PureComponent {
//...
}
You can also do it for arrow functions passed as an argument:
fetch(/** ... */).then(
/** @namespace Component/Braintree/Component/fetchThen */
() => { /** ... */ }
);
<aside> ✅ For this reason, it is important to add namespaces to your code. Doing so will ensure that it is extensible via plugins.
</aside>
ScandiPWA uses a configuration object in the plugin file to map plugins to the original code using namespaces. The configuration object looks as follows:
export default {
'<NAMESPACE>': {
'<TYPE>': <PROXY_NAME>
},
//...
};
export default {
'<NAMESPACE>': {
'<TYPE>': {
<IDENTIFIER>: <PROXY_NAME>,
}
}
};
<NAMESPACE>
- Object keys are the namespaces you intend to apply proxy to.
Depending on the plugin type, the config object varies.
function
type just needs to define the proxy function.member-function
, member-property
, and static-member
are more specific. They define an object with <IDENTIFIER>
(what the proxy should be applied to) as the key and the <PROXY_NAME>
(proxy function) to be used as the value.The following example defines a config object to apply the proxy p1
to namespace Example/namespace
that identifies a type function
:
export default {
'Example/Namespace': {
'function': p1
}
};
You can define multiple namespaces and types in the same config object:
export default {
'Another/Example/Namespace': {
'member-property': {
paymentRenderMap: p1
},
'member-function': {
renderPayment: p2
}
},
'Another/Example/Namespace/function': {
'function': {
p3
}
}
};
<aside> ℹ️ It is possible to attach more than one plugin to the same namespace and type.
</aside>
<aside> ✅ When working with plugins, it is important to keep in mind the best practices.
</aside>
To create a plugin you need to:
Create the plugin file.
The file should be located in the src/plugin
folder. And be named <ORIGINAL NAME>.plugin.js
(What are the best practices for naming and placement?).
Create the plugin(proxy
) function in the file. The function arguments will depend on the type of plugin you are creating. Click the link in the description of the plugin type to see the exact arguments.
<aside> ℹ️ Consider using descriptive function names when defining the plugin function.
</aside>
<aside> 🚨 Avoid not calling the callback, unless really necessary! The callback is the content of the original function or other plugin.
</aside>
Export the config object as default in the file. There you should use the namespace, type of plugin, and proxy function.
The config
object and proxy function arguments will vary depending on the type of plugin, which can be:
function
for common functions.member-function
for class methods.member-property
for class properties.static-member
for static class methods.<aside> ✅ If the original function does not have a namespace but is a class function, you can use the member function plugin.
</aside>
Configure the exported object to use the key function
as follows:
const <PROXY_NAME> = (args, callback, context) => {
// additional logic
return callback(...args);
};
export default {
'<NAMESPACE>': {
function: <PROXY_NAME>
}
};
args
– the array of arguments of the original function.callback
– the original function.context
– context of the original function.
(can be omitted if not used)Tutorials using plugins of type function
:
Tutorial: Create a plugin to mapStateToProps and mapDispatchToProps
Tutorial: How to create a redux store in extensions?
Examples:
.then()
Configure the exported object to use the key member-function
as follows:
const <PROXY_NAME> = (args, callback, instance) => {
// additional logic
return callback(...args);
};
export default {
'<NAMESPACE>': {
'member-function': {
<CLASS_METHOD>: <PROXY_NAME>,
}
}
};
args
– the array of arguments of the function callercallback
– the original methodinstance
– the original class instance.<aside>
ℹ️ The <NAMESPACE>
must be the class namespace.
</aside>
Tutorials using plugins of type member-function
:
Tutorial: How to modify (add/remove/edit) props of a rendered component?
Tutorial: Add additional fields to a query using plugins
Examples: