- Does anyone know how Magento has implemented this functionality?
- What Magento custom mixins plugin is actually is ?
What is a Mixin in Magento 2?
magento2mixins
Related Solutions
I'd like to go straight to your questions and then I'll try to make it clear on what you can actually do with the mixins plugin. So, first things first.
Implementation
The main thing here is the ability of any RequireJS plugin to completely take over the loading process of certain files. This allows to modify the export value of a module before it will be passed as a resolved dependency.
Take a look at this sketchy implementation of what Magento custom mixins plugin is actually is:
// RequireJS config object.
// Like this one: app/code/Magento/Theme/view/base/requirejs-config.js
{
//...
// Every RequireJS plugin is a module and every module can
// have it's configuration.
config: {
sampleMixinPlugin: {
'path/to/the/sampleModule': ['path/to/extension']
}
}
}
define('sampleMixinPlugin', [
'module'
] function (module) {
'use strict';
// Data that was defined in the previous step.
var mixinsMap = module.config();
return {
/**
* This method will be invoked to load a module in case it was requested
* with a 'sampleMixinPlugin!' substring in it's path,
* e.g 'sampleMixinPlugin!path/to/the/module'.
*/
load: function (name, req, onLoad) {
var mixinsForModule = [],
moduleUrl = req.toUrl(name),
toLoad;
// Get a list of mixins that need to be applied to the module.
if (name in mixinsMap) {
mixinsForModule = mixinsMap[name];
}
toLoad = [moduleUrl].concat(mixinsForModule);
// Load the original module along with mixins for it.
req(toLoad, function (moduleExport, ...mixinFunctions) {
// Apply mixins to the original value exported by the sampleModule.
var modifiedExport = mixinFunctions.reduce(function (result, mixinFn) {
return mixinFn(result);
}, moduleExport);
// Tell RequireJS that this is what was actually loaded.
onLoad(modifiedExport);
});
}
}
});
The last and the most challenging part is to dynamically prepend the 'sampleMixinPlugin!' substring to the requested modules. To do this we
intercept define
and require
invocations and modify the list of dependencies before they will be processed by the original RequireJS load method.
It's a little bit tricky and I'd recommend to look at the implementation lib/web/mage/requirejs/mixins.js
if you wanna how it works.
Debugging
I'd recommend this steps:
- Make sure that the configuration for 'mixins!' plugin is actually there.
- Check that the path to a module is being modified. I.e. it turns from
path/to/module
tomixins!path/to/module
.
And the last but not least, requiresjs/mixins.js
has nothing to do with the main.js
or script.js
modules as they can only extend the configuration being passed from the data-mage-init
attribute:
<div data-mage-init='{
"path/to/module": {
"foo": "bar",
"mixins": ["path/to/configuration-modifier"]
}
}'></div>
I mean that the former two files don't mess with the value returned by a module, instead they pre-process configuration of an instance.
Usage Examples
To begin with I'd like to set the record straight that so called "mixins" (you're right about the misnaming) actually allow to modify the exported value of a module in any way you want. I'd say that this is a way more generic mechanism.
Here is a quick sample of adding extra functionality to the function being exported by a module:
// multiply.js
define(function () {
'use strict';
/**
* Multiplies two numeric values.
*/
function multiply(a, b) {
return a * b;
}
return multiply;
});
// extension.js
define(function () {
'use strict';
return function (multiply) {
// Function that allows to multiply an arbitrary number of values.
return function () {
var args = Array.from(arguments);
return args.reduce(function (result, value) {
return multiply(result, value);
}, 1);
};
};
});
// dependant.js
define(['multiply'], function (multiply) {
'use strict';
console.log(multiply(2, 3, 4)); // 24
});
You can implement an actual mixin for any object/function returned by a module and you don't need to depend on the extend
method at all.
Extending a constructor function:
// construnctor.js
define(function () {
'use strict';
function ClassA() {
this.property = 'foo';
}
ClassA.prototype.method = function () {
return this.property + 'bar';
}
return ClassA;
});
// mixin.js
define(function () {
'use strict';
return function (ClassA) {
var originalMethod = ClassA.prototype.method;
ClassA.prototype.method = function () {
return originalMethod.apply(this, arguments) + 'baz';
};
return ClassA;
}
});
I hope that this answers your questions.
Regards.
The choice of which of those techniques to use is often relative and can be decided by general principles. There are a few exceptions when a mixin would not accomplish the goal and therefore, map
must be used.
TL;DR
- Use a mixin if you only need to modify some of an object's methods.
- Use
map
if a mixin won't work or if the file is being changed extensively. - If in doubt, use a mixin.
Definitions:
Generally speaking, a mixin overrides individual methods inside of a Javascript object (or "class").
A map
is a key used in requirejs-config.js
that can replace one file with another. This overrides an entire file. Themes have the capability of doing this without requirejs-config.js
.
General Principles / Best Practice
Now, what general principles should we use when making the decision of which approach to use? Relative to this question, I suggest the following:
The goal in making modifications is to affect as little code as possible.
This isn't really the only one, though. Other considerations include overall development time, simplicity of the code in question, and probably more.
The benefit of affecting as little code as possible is upgradability and maintainability. If the core file changes, there is a smaller chance of it breaking your update. In addition, since there is less code there altogether, bug fixes or changes to it should be easier.
Specific to your question:
A mixin is a great choice because it affects very little code and is easy to implement. If you were to use map
, you would have to copy and override the entire file. You can't extend the original file like you would with PHP because referencing the original file would load your own file.
With regards to your mixin question: target
is the original Javascript object. Magento's mixin handler provides that to you. Further, your mixin must match the structure of the original Javascript object: whether an object or jQuery UI widget or other. This necessitates some slightly awkward code at times.
The code in your example is correct: Magento merges your object over the top of the original one. This replaces the original initialize
method with your method but keeps the rest of the object intact.
Best Answer
A mixin is a class whose methods are added to, or mixed in, with another class.
A base class includes the methods from a mixin instead of inheriting from it. This allows you to add to or augment the behavior of the base class by adding different mixins to it.
The following is an example of a mixin module that extends the target component with a function that introduces a new blockVisibility property to a column element.
File: OrangeCompany/Sample/view/base/web/js/columns-mixin.js
Mixins are declared in the mixins property in the requirejs-config.js configuration file. This file must be created in the same area specific directory the mixin is defined in.
The mixins configuration in the requirejs-config.js associates a target component with a mixin using their paths.
Example
The following is an example of a requirejs-config.js file that adds the columns-mixins, defined in the previous example.
Mixin example in Magento: https://github.com/magento/magento2/blob/2.2/app/code/Magento/CheckoutAgreements/view/frontend/requirejs-config.js
Reference: https://devdocs.magento.com/guides/v2.2/javascript-dev-guide/javascript/js_mixins.html
Use of JS mixins as small example: https://webkul.com/blog/use-js-mixins-magento2/