Akeneo PIM poses a challenge for Frontend Developers who take first steps in this technology. While technical documentation regarding backend is available and developers can use it to obtain answers to burning questions – the presentation layer can be challenging. It is hard to find official materials with a full set of information from a particular area. Additionally, frontend architecture is constantly evolving and the Akeneo PIM stack is changing – thus it is difficult to determine a good practice while creating UI.
This series of articles provides a deep dive into Akeneo UI. We will present the way the frontend works and explain how to modify software so that it meets our requirements.
My blog post series provides some context and explanation of the principles used to build UI in the PIM. Check the other articles:
- What should you know about the structure of RequireJS modules in Akeneo PIM?
- Getting started with creating frontend modules in Akeneo PIM
- How to override a RequireJS module?
- 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?
Do you need a PIM? Choose Akeneo!
Our handbook starts with a few words about the product itself and its application. Akeneo is a tool of the Product Information Management (PIM) family. It functions as storage and data manager. One of the most significant features of Akeneo PIM is its modular architecture for extension and adaptation of the system to your own needs – the owners of Community Edition, Growth Edition as well as Enterprise Edition can take advantage of numerous modules supporting various business aspects.
How is frontend generated in Akeneo?
Akeneo is an open-source solution written in PHP (based on the Symfony framework). The presentation layer uses technologies such as jQuery, RequireJS, TypeScript, Backbone.js and React.
As we mentioned at the very beginning, Akeneo frontend is a combination of different old and new technologies. One of the parts can be called the deprecated tech stack which includes RequireJS, Backbone.js and jQuery. Let's call the second part as the target tech stack, including webpack, React and TypeScript. Importantly, we are currently in the transitional stage in the latest version of Akeneo PIM (5.0). We have a frontend part of the application on the basis of deprecated technologies, and you can find elements built only with the use of new technology. There are also parts of the frontend that combine old and new solutions.
Now let’s have a closer look at this set of technologies!
RequireJS
RequireJS is still used in a large part of the application to import various dependencies, however with subsequent Akeneo upgrades it started to be superseded by webpack (since Akeneo PIM 2.0). For the time being requirejs.yml and webpack are used simultaneously.
An excerpt of an example .yml configuration file for RequireJS (you can find the whole file here):
config:
config:
pim/router:
indexRoute: pim_dashboard_index
# Forwarded events from mediator
pim/form/common/edit-form:
forwarded-events:
'pim_enrich:form:field:extension:add': 'pim_enrich:form:field:extension:add'
'pim_enrich:form:filter:extension:add': 'pim_enrich:form:filter:extension:add'
'pim_enrich:form:entity:pre_save': 'pim_enrich:form:entity:pre_save'
'pim_enrich:form:entity:update_state': 'pim_enrich:form:entity:update_state'
'pim_enrich:form:entity:post_fetch': 'pim_enrich:form:entity:post_fetch'
pim/cache-invalidator:
events:
- 'pim_enrich:form:entity:post_fetch'
pim/job/product/edit/content:
forwarded-events:
'pim_enrich:form:filter:extension:add': 'pim_enrich:form:filter:extension:add'
pim/grid/view-selector/selector:
forwarded-events:
'grid:product-grid:state_changed': 'grid:product-grid:state_changed'
pim/controller/group:
fetcher: group
pim/remover/association-type:
url: pim_enrich_associationtype_rest_remove
pim/remover/attribute-group:
url: pim_enrich_attributegroup_rest_remove
pim/remover/group-type:
url: pim_enrich_grouptype_rest_remove
pim/remover/channel:
url: pim_enrich_channel_rest_remove
Backbone.js
Backbone.js is also responsible for the presentation layer in Akeneo, however it is slowly being replaced by React. This framework helps to create basic views and a part of forms at present.
RequireJS using ready classes written in Backbone.js:
'use strict';
define([
'oro/translator',
'backbone',
'oro/mediator',
'pim/form',
'pim/fetcher-registry',
'pim/template/common/default-template',
], function (__, Backbone, mediator, BaseForm, FetcherRegistry, template) {
return BaseForm.extend({
template: _.template(template),
/**
* {@inheritdoc}
*/
initialize: function () {
this.model = new Backbone.Model({});
BaseForm.prototype.initialize.apply(this, arguments);
},
/**
* {@inheritdoc}
*/
configure: function () {
Backbone.Router.prototype.once('route', this.unbindEvents);
if (_.has(__moduleConfig, 'forwarded-events')) {
this.forwardMediatorEvents(__moduleConfig['forwarded-events']);
}
return BaseForm.prototype.configure.apply(this, arguments);
},
/**
* {@inheritdoc}
*/
render: function () {
if (!this.configured) {
return this;
}
this.getRoot().trigger('oro_config:form:render:before');
this.$el.html(this.template());
this.renderExtensions();
this.getRoot().trigger('oro_config:form:render:after');
return this;
},
/**
* Clear the mediator events
*/
unbindEvents: function () {
mediator.clear('oro_config:form');
},
});
});
Are you wondering how to recognize that RequireJS uses Backbone? Pay attention to the fact that Backbone is imported as the second dependency by RequireJS. Secondly, the "initialize" method generates a new Backbone model instance and it may be noticed that functions refer to other Backbone methods such as "Backbone.Router".
Furthermore, we can see that another dependency added was "BaseForm". This is one of the basic Akeneo classes that can be taken as a model for creating many other classes. The file is located here and it is an extension of "Backbone.View".
What we are presenting below is "create-button.js" file modification with "BaseForm" class used. The class has already been mentioned. We use an extra "maco-product-state" file. It stores data regarding the product e.g. a price (in the following chapters we will describe how to create and add files and modules). We download information about the Id of a product and its family (look: code lines 56-57) and use "setData" to set these values.
'use strict';
define(
[
'jquery',
'underscore',
'oro/translator',
'pim/form',
'macopedia/templates/product/transform-to-variant',
'routing',
'pim/dialogform',
'pim/form-builder',
'oro/mediator',
'maco-product-state'
],
function (
$,
_,
__,
BaseForm,
template,
Routing,
DialogForm,
FormBuilder,
mediator,
ProductState
) {
return BaseForm.extend({
template: _.template(template),
className: 'AknDropdown-menuLink transform-to-variant',
dialog: null,
productId: null,
familyId: null,
variantAxis: null,
/**
* {@inheritdoc}
*/
initialize: function (config) {
this.config = config.config;
BaseForm.prototype.initialize.apply(this, arguments);
},
/**
* {@inheritdoc}
*/
render: function () {
this.$el.html(this.template({
title: __(this.config.title),
}));
this.$el.on('click', function () {
FormBuilder.build(this.config.modalForm)
.then(function (modal) {
let state = {
productId: ProductState.getProductId(),
familyId: ProductState.getFamilyId(),
variantAxis: this.variantAxis
}
modal.setData(state);
modal.open();
}.bind(this)).catch(reason => {
console.error(reason);
})
}.bind(this));
return this;
}
});
});
jQuery
jQuery might also be used while building frontend in Akeneo. Despite the increasing popularity of other frameworks, it still remains one of the most recognized and used JavaScript libraries. jQuery appears in older Akeneo elements which have not been rewritten to React yet or sometimes even together with React. For example, we use it to support the "select" field together with the "select2" library.
Connecting jQuery, TypeScript and React in a component responsible for creating "select" field:
componentDidMount() {
if (null === this.DOMel.current) {
return;
}
this.el = $(this.DOMel.current);
if (undefined !== this.el.select2) {
this.initSelectField();
if(!this.el.val()) {
this.el.select2('val', -1);
}
this.el.on('change', (event: any) => {
const newValue = this.props.multiple
? event.val.map((recordCode: string) => RecordCode.create(recordCode))
: '' === event.val
? null
: RecordCode.create(event.val);
this.props.onChange(newValue);
});
// Prevent the onSelect event to apply it even when the options are null
const select2 = this.el.data('select2');
select2.onSelect = (function(fn) {
return function(_data: any, options: any) {
if (null === options || 'A' !== options.target.nodeName) {
fn.apply(this, arguments);
}
};
})(select2.onSelect);
} else {
this.el.prop('type', 'text');
}
}
TypeScript
TypeScript was developed by Microsoft in an open-source model as a superset of JavaScript. In this way we obtained a classical object-oriented and statically typed programming language. TypeScript is used with React in newer application layers.
Type declaration in TypeScript:
import {ButtonHTMLAttributes, ReactNode} from "react";
export type FormValue = {
[key: string]: string;
};
export type FormProps = {
children?: ReactNode;
value: FormValue;
onChange: (value: FormValue) => void;
};
export type OptionProps = {
value: string;
isSelected?: boolean;
isDisabled?: boolean;
children?: ReactNode;
onSelect?: () => void;
} & ButtonHTMLAttributes;
export type QuickExportConfiguratorProps = {
showWithLabelsSelect: boolean;
showWithMediaSelect: boolean;
onActionLaunch: (formValue: FormValue) => void;
getProductCount: () => number;
};
export type SelectProps = {
children?: ReactNode;
name: string;
value?: string | null;
isVisible?: boolean;
onChange?: (value: string | null) => void;
};
React
More and more application elements are rewritten onto React.js. In Akeneo source files we can now find dozens, if not even hundreds of components. Let’s take the component responsible for photo upload as an example.
The code responsible for rendering a photo uploader:
return (
{1 items.length ? (
>
setOpen(true)}>
{children}
{isOpen && (
>
setOpen(false)} />
{items.map(item => (
- onItemClick(item)}
>
{item.label}
))}
)}
) : (
)}
);
Storybook
With the development of the Akeneo frontend and the gradual switching to React, Storybook was also implemented to document the UI components. With Storybook you can easily check what components are available in the project, what they look like and what their possibilities are.
You can find the link to the official Akeneo Storybook here:
In next articles we will describe other challenges that must be faced by developers. Be ready for more!