You already know that Akeneo PIM is an open-source solution with a modular architecture. By modules in this context we mean small independent parts of JavaScript code loaded by RequireJS. A built-in component does not, however, always completely meet your business needs. What happens then? There are a few ways to modify the components. We will discuss one of the methods later in the article and explain when it should be used.
My blog post series provides some context and explanation of the principles used to build UI in the PIM. Check the other articles:
- Akeneo PIM UI – Complete Guide for Frontend Developers
- What should you know about the structure of RequireJS modules in Akeneo PIM?
- Getting started with creating frontend modules in Akeneo PIM
- How to extend existing modules based on RequireJS?
- How to share information between frontend components in Akeneo PIM with mediator pattern?
- How to use custom labels in Akeneo UI, translate components, or replace default strings?
- What is UserContext in Akeneo PIM & How to access and modify user properties from JavaScript?
Akeneo modules and views, and their customizations
The UI component system used in Akeneo is analogical to other solutions. Splitting the frontend into smaller, reused parts of code enables us to avoid repetitions in the code and to reduce the development time of new functionalities. If it is important to customize a chosen module – it is enough to extend or override a component instead of creating another one from scratch. This is a significant advantage of Akeneo PIM.
We wrote more on the product itself and about the technology stack for the frontend layer here.
Workflow and file structure
As highlighted in the last chapter, the "src" folder is an important place for the developer where he will create new and override existing modules. The existing Akeneo files, which will be modified, are included in "vendor/akeneo". In the example below, one can observe 2 packages from Akeneo: Community Edition (open-source version available on GitHub) and Enterprise Edition (version with a paid license):
As the title of our blog post suggests, our task is to override a RequireJS module using our own module. It is one of the ways to change the appearance and logic of the application without the need to interfere with the original files. In the following posts, we will present other methods as well.
To kick-off, we’ve chosen the simplest one. In order to face this challenge, we will have to perform the following activities:
- Step 1: find this component in the Akeneo source files;
- Step 2: check which key is used to load it in RequireJS;
- Step 3: add a RequireJS configuration file, which will load the new module into the files in our module;
- Step 4: create your own module in the right place;
- Step 5: copy the content from the overridden module and paste it to ours;
- Step 6: customize the module so that it meets our requirements and rebuild the frontend.
The list is quite long. But fortunately you’ve already learnt about some of them in the previous articles of the series. We hope you are ready for the next step!
How do you override an existing module? Where do you look for it and how do you attach it with requirejs.yml?
In this section we will show how to block a product field depending on an attribute value. Before we move on to the examples, let's take a moment to cover the particular steps of overriding modules.
To modify an existing module in Akeneo PIM, we have to prepare our own component consisting of a proper JavaScript file based on RequireJS and register it in a "requirejs.yml" configuration file.
- Step 1
The first step will be searching for the file that we are going to edit in the Akeneo structure. As an example, we’re extending "field-manager.js" (which supports the generated fields).
We assume that you have already created a bundle in Symfony. The path to your file results from the rules of creating bundles and from the Symfony documentation. At this stage, you should also know how to find a module. If you would like to cover these topics one more time, feel free to read our instructions.
- Step 2
Looking for the phrase "field-manager" with file mask set to "requirejs.yml", we will find the proper file, and based on this we will find out how the mentioned "field-manager.js" was added. We want to override "field-manager.js" with our own file, which will replace it. After finding the proper key "pim/field-manager" in the RequireJS files, we can add it to our config file.
- Step 3
We’re creating "requirejs.yml" for the module which we are extending. The .yml files should always be placed in "config":
In "requirejs.yml" we define a file which will override the RequireJS module "field-manager.js" in the following way:
config:
paths:
pim/field-manager: macopediaproduct/js/product/field-manager
- Step 4
"field-manager.js" has to be added now – it will override the default module.
The module created by us:
- Step 5
Finally, you have to find the original "field-manager.js" module in the Akeneo source files using our suggestions from the previous post, copy its content and paste it into the new module. If you have a problem with finding this file, check out here.
- Step 6
After all these steps you can start working on making the required changes in "field-manager.js". In order to make it visible for the watcher, you have to turn it off (if it is turned on) and rebuild the whole presentation layer with the command "make front" or "make upgrade-front" (depending on the software version).
Next, we can turn on the watcher again using the "yarn run webpack-dev --watch" command.
According to programming tradition, let’s write your first “Hello world”. In the "field-manager.js" file, we add a simple "console.log" within the "getField" method:
getField: function (attributeCode, shouldBeLocked = false, lockedAttributesArray = []) {
var deferred = $.Deferred();
conosole.log('Hello world');
if (fields[attributeCode]) {
deferred.resolve(fields[attributeCode]);
return deferred.promise();
}
FetcherRegistry.getFetcher('attribute').fetch(attributeCode).done(function (attribute) {
getFieldForAttribute(attribute).done(function (Field) {
fields[attributeCode] = new Field(attribute);
deferred.resolve(fields[attributeCode]);
});
});
return deferred.promise();
},
"getField" is called for every product field, so now “Hello world” will be displayed in "console.log" for particular fields:
We have just written the first modification of the overridden module. It is not, in fact, a huge change, but Rome wasn’t built in a day.
As an exercise, let’s try to display a code for each field as well. To do it, we’re going to replace the “Hello world” string with the "attributeCode" variable:
getField: function (attributeCode, shouldBeLocked = false, lockedAttributesArray = []) {
var deferred = $.Deferred();
console.log(attributeCode);
if (fields[attributeCode]) {
deferred.resolve(fields[attributeCode]);
return deferred.promise();
}
FetcherRegistry.getFetcher('attribute').fetch(attributeCode).done(function (attribute) {
getFieldForAttribute(attribute).done(function (Field) {
fields[attributeCode] = new Field(attribute);
deferred.resolve(fields[attributeCode]);
});
});
return deferred.promise();
},
The codes for specific fields are:
Now let’s block the field "name", so that it is read-only.
Above you can see the product view and the rendered fields. Looking from the bottom, the second field is "name" (which is supposed to be blocked). The "console.log" command can be applied also for "attribute", which will help us verify which features are available:
getField: function (attributeCode, shouldBeLocked = false, lockedAttributesArray = []) {
var deferred = $.Deferred();
if (fields[attributeCode]) {
deferred.resolve(fields[attributeCode]);
return deferred.promise();
}
FetcherRegistry.getFetcher('attribute').fetch(attributeCode).done(function (attribute) {
getFieldForAttribute(attribute).done(function (Field) {
console.log(attribute);
fields[attributeCode] = new Field(attribute);
deferred.resolve(fields[attributeCode]);
});
});
return deferred.promise();
},
After adding "console.log", we can see the "attribute" object:
One of the features is the "is_read_only" field. If your goal is to block the field, it should return the value "true". We want to add the condition, checking if we are dealing with the "name" field and set "is_read_only" to "true":
getField: function (attributeCode, shouldBeLocked = false, lockedAttributesArray = []) {
var deferred = $.Deferred();
if (fields[attributeCode]) {
deferred.resolve(fields[attributeCode]);
return deferred.promise();
}
FetcherRegistry.getFetcher('attribute').fetch(attributeCode).done(function (attribute) {
getFieldForAttribute(attribute).done(function (Field) {
if (attributeCode === 'name') {
attribute.is_read_only = true;
}
fields[attributeCode] = new Field(attribute);
deferred.resolve(fields[attributeCode]);
});
});
return deferred.promise();
},
When we look again at the browser window, we will quickly notice that "name" is blocked: