• v3.2
    • Versions
    • v3.2

 

  • Install Akeneo PIM
    • Install Akeneo PIM with Docker
    • Install Akeneo PIM manually
      • System Requirements
      • System installation on Debian 9 (Stretch)
      • System installation on Ubuntu 16.04 (Xenial Xerus)
      • Installing Akeneo PIM Community Edition (CE) with the Archive
      • Installing Akeneo PIM Enterprise Edition (EE) with the Archive
      • Setting up the job queue daemon
    • How to customize the Dataset
    • How to Add Translation Packs
  • Migrate Akeneo PIM projects
    • How to apply a patch?
      • How to apply a patch - Community Edition
      • How to apply a patch - Enterprise Edition - Flexibility Cloud offer
      • How to apply a patch - Enterprise Edition - On Premise offer
    • Where is the Changelog?
    • How to upgrade to a minor version?
    • How to upgrade to a major version?
  • Import and Export data
    • How import works
    • Understanding the Product Import
    • Understanding the Product Export
    • Formats
      • Localized labels
      • Scopable labels
      • Association types data structure
      • Attribute data structure
      • Category data structure
      • Family data structure
      • Family variant data structure
      • Group data structure
      • Options data structure
      • Product data structure
      • Product model data structure
    • Akeneo Connectors
    • How to Customize Import / Export
      • How to create a new Connector
      • How to import Products from a XML file
      • How to clean a CSV file during a Product import
      • How to automate imports/exports
  • Manipulate the Akeneo PIM data
    • How to Customize Mass Edit Operations
      • How to register a new bulk action
      • How to Register a New Mass Edit Action on Products
      • How to Avoid Rules Execution on Mass Edit Actions
    • How to Manipulate Products
      • How to Query Products
      • How to Create Products
      • How to Update Products
      • How to Validate Products
      • How to Save Products
      • How to Remove Products
    • How to Manipulate Non-Product Objects
      • How to Query Non-Product Objects
      • How to Create Non-Product Objects
      • How to Update Non-Product Objects
      • How to Validate Non-Product Objects
      • How to Save Non-Product Objects
      • How to Remove Non-Product Objects
    • How to add a custom action rule
      • General information about rule format
      • How to add a custom action in the rule engine
    • How to Define Access Control List
    • How to Customize the Catalog Structure
      • How to Create a Reference Data
      • How to add a custom unit of measure
      • How to Add New Properties to a Category
    • How to Customize Product Assets
      • How to Add a New Transformation
      • How to Add a Default Thumbnail For Unknown File Types
      • How to connect to an external server for storage
      • How to change the validation rule to match a reference file to an asset
      • How to Mass Import Assets
    • How To Customize Teamwork Assistant (Enterprise Edition)
      • Customize notifications
      • Add a calculation step
      • How to log calculation step
      • Remove projects impacted by a custom catalog update
  • Design the user interfaces
    • How to customize any frontend part of the application
    • How to add an action button or meta data to the product edit form
    • How to add a tab to a form
    • How to add a new tab in System / Configuration
    • How to add custom information to a field
    • How to add a new field type
    • Create a custom product export builder filter
    • How to create the UI to manage a Reference Data
    • How to add a new page
    • How to customize the main menu
    • Styleguide
  • Maintain Akeneo PIM projects
    • First aid kit
    • Bug qualification
    • Common issues
    • Scalability Guide
      • Audit with 3 Representative Catalogs
      • More than 10k attributes?
      • More than 10k families?
      • More than 10k categories?
      • More than 500 attributes usable in the product grids?
      • More than 100k products to export?
      • More than 1GB of product media to export?
    • How to purge history
      • How to Purge jobs executions
      • How to adapt the version purger to your needs
  • Contribute to Akeneo PIM
    • How to report an issue?
    • How to translate the user interface?
    • How to enhance the documentation?
    • How to contribute to a Connector?
    • How to submit a patch to the PIM?
    • How to contribute to the frontend part of the application
    • How behavior tests are architectured in the PIM?
      • Establishing Decorator Pattern
      • Using Spin In Behat
  • Use SSO authentication locally
  • Reference Entities (beta)
    • Configure Entity Limits
    • Create a new Reference Entity Attribute type
    • Enrich Records with a new Reference Entity Attribute type
    • Add a Custom Property to Your Custom Attribute Type
    • Refresh records completeness
  • Technical overview
    • Product Information
    • Teamwork Assistant (Enterprise Edition)
      • Project creation
      • Project completeness
      • Project Completeness widget
      • Catalog update impact
      • Scalability guide
      • Users permission summary for Behat tests
    • Collaborative workflow
      • Simple workflow
      • Partial workflow
  • Technical architecture
    • Best Practices
      • Create a project
      • Create a reusable bundle
      • Setup Behat
      • Code Conventions
      • Coding Standards
    • How to implement your business logic using the event system
    • Events
      • Storage events
      • Workflow events (Enterprise Edition only)
    • How to Localize your data
      • How to change the PIM locale
      • How to Use Localizers
      • How to use Presenters
    • How to Add a Notification
    • Performances Guide
      • Memory usage of rules execution (Enterprise Edition)
      • Memory leak fix in Rules Engine (ORM)
      • More than 100 WYSIWYG editors in a page
      • PHP7 and HHVM Compatibility?
      • Job product batch size
    • How to Use the Web REST API
    • Standard format
      • Products
      • Other entities
      • Usage
    • Application Technical Information
      • Application Technical Dependencies
      • Server side set up for hosting
      • System Requirements
      • Recommended configuration
      • Client side configuration and compatibilities
      • Operation processes
      • Flow Matrix
  • Akeneo Cloud Edition
    • Flexibility Mode
      • Overview
      • Partners Starterkit
      • Environments Access
      • Composer settings
      • Periodic tasks / Crontab settings
      • PIM Application
      • PIM Updates and Migrations
      • File Transfer (SFTP)
      • Backups management
      • Partners
      • Queue management
      • System Components
      • Disk Usage
      • Ensure the Onboarder worker is always running
    • Serenity Mode
      • Overview
      • PIM Updates and Migrations
  • Akeneo Onboarder
    • Prerequisites
    • Installation
    • Migrate Akeneo Onboarder projects
      • How to apply a patch?
        • How to apply an Onboarder patch - Enterprise Edition - Flexibility Cloud offer
        • How to apply an Onboarder patch - Enterprise Edition - On Premise offer
      • How to upgrade to a minor version?
      • How to upgrade to a major version?
    • Synchronization
    • Troubleshooting
    • Environment variables
      • Using the DotEnv file
      • Using environment variables

How to customize any frontend part of the application¶

Introduction¶

Akeneo PIM uses RequireJS and Backbone to manage modules and views in our application. On top of those two layers, we added our own layer: the form extension system. In this documentation, we will discover the basics of those three layers to be able to understand Akeneo PIM stack and customize it.

Note

Since the 2.0 version, we migrated to webpack to manage our front dependencies. To ease the migration we decided to keep the historicals requirejs.yml files to give the community enough time to migrate their configuration.

Create our first RequireJS module¶

RequireJS modules are the equivalent of php classes. They provide a way to declare dependencies to a module (like use statements) and expose a new module.

Simple business logic¶

As an example, we will create a module that will display in the console the current UI locale.

To begin, here is the business logic of our component:

1
2
3
4
5
{
    logLocale: function () {
        console.log(UserContext.get('uiLocale'));
    }
}

Our newly created object has one method which logs the current catalog locale using the UserContext module. Now that we have our business logic implemented, we need to tell the system that our module has a dependency on pim/user-context and that we are exposing this object.

The real thing¶

Here is how to do it using RequireJS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/Acme/Bundle/CustomBundle/Resources/public/js/locale_logger.js
define(
    ['pim/user-context'],
    function (UserContext) {
        return {
            logLocale: function () {
                console.log(UserContext.get('uiLocale'));
            }
        };
    }
);

The define method takes two arguments: the first one is an array of the dependencies we want to use (pim/user-context in this case) and a callback that will be called every time our module is requested. This callback will receive as argument the list of dependencies we asked for. Note that you can name the arguments of the callback as you want.

Register it¶

Now that we have this small module, we would like to use it somewhere. To do so, we need to register it in the RequireJS configuration file.

# src/Acme/Bundle/CustomBundle/Resources/config/requirejs.yml
config:
    paths:
        my_locale_logger: acmecustom/js/locale_logger

We just declared a new module named my_locale_logger located in web/bundles/acmecustom/js/locale_logger.js. Note that we don’t need to specify the file extension and it is located in the public web folder (moved during the bin/console assets:install command execution). Also, my_locale_logger name is an arbitrary name and you can replace it with whatever you want.

Make it available¶

To make your module available you need to clear your cache:

rm -rf ./var/cache/*

dump the assets in the web folder

bin/console assets:install web

You can also decide to symlink those assets instead of copying them:

bin/console assets:install --symlink web

Compile the .less files to CSS

yarn run less

After that the last step is to build the bundle.js file:

yarn run webpack

This command will compile and minify all the pim files into web files and dump them in the public folder.

You can also use the watch command which will recompile this file each time you modify a registered module.

yarn run webpack-watch

Summary¶

We just created our first module and made it available for other parts of the application. You can now use it by requiring my_locale_logger. We cannot use it now but we will see that in a minute.

Create our first Backbone view¶

Basic view¶

Now that we know how to create a RequireJS module, let’s create our first Backbone view.

1
2
3
4
5
6
7
8
9
// src/Acme/Bundle/CustomBundle/Resources/public/js/new_view.js
define(
    ['backbone'],
    function (Backbone) {
        return Backbone.View.extend({

        });
    }
);

This is the simplest view we can create using Backbone. It will result in an empty div if we add it to the DOM.

The real stuff¶

Let’s add some more interesting stuff to it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/Acme/Bundle/CustomBundle/Resources/public/js/new_view.js
define(
    ['backbone'],
    function (Backbone) {
        return Backbone.View.extend({
            events: {
                'click': 'clicked'
            },

            render: function () {
                this.$el.html('<div>Hello world</div>')
            },

            clicked: function (event) {
                console.log(event);
            }
        });
    }
);

In this code we do three things:

  • We override the render method to add custom render logic. In this example, we add a div inside our view with a simple text inside it.
  • We declare an event listener to listen to the click events on our view to call the clicked method
  • The clicked method will log the DOM click event each time a click is triggered on our view.

You can find more information about Backbone views on the dedicated documentation

Register it¶

Now that we have a view, we need to register it in the requirejs.yml file

# src/Acme/Bundle/CustomBundle/Resources/config/requirejs.yml
config:
    paths:
        my_locale_logger: acmecustom/js/locale_logger
        my_custom_view: acmecustom/js/new_view

After cache clearing, asset dump and webpack build, you should be able to use your newly created view.

Create our first form extension¶

Now that we can create a RequireJS module and a Backbone view, we would like to actually customize the PIM to add our own business logic on top of it.

A little bit of history¶

As we are an open source company, our product can be used and customized for a lot of different reasons and by a lot of different people (integrators, clients, contributors and technological partners). With this in mind, it was impossible to base our architecture on overrides to customize it.

We decided to create a tree-based architecture where each form of the application would be a tree of extensions. If you want to change a part of a page or add something to it, you need to create a RequireJS module and register it in the tree of form extensions.

The form extension¶

To continue on our example we can use our previously created Backbone view to transform it into a form extension.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/Acme/Bundle/CustomBundle/Resources/public/js/new_view.js
define(
    ['pim/form'],
    function (BaseForm) {
        return BaseForm.extend({
            events: {
                'click': 'clicked'
            },

            render: function () {
                this.$el.html('<div>Hello world</div>')
            },

            clicked: function (event) {
                console.log(event);
            }
        });
    }
);

As you can see, we haven’t changed much: we now extend the BaseForm instead of the Backbone.View. As BaseForm extends itself the Backbone.View, everything works as before and you can use all Backbone features.

Register it¶

Now that we created our form extension we need to register it:

# src/Acme/Bundle/CustomBundle/Resources/config/form_extensions.yml
extensions:
    my_form_extension:
        module: my_custom_view             # Your RequireJS module name
        parent: pim-product-edit-form-meta # The parent of your extension (the meta section of the product edit form in this case)

A few words about this small configuration:

  • the key my_form_extension should be a unique key to represent your form extension.
  • you can declare multiple form extensions with the same RequireJS module.
  • to be registered, your configuration file have to be named form_extensions.yml or put in a form_extensions folder in your bundle Resources/config folder.
  • You can override any form extension by using the same unique key (the order of the override is defined by your AppKernel.php registration order).

This configuration is the minimal example. Here are the other parameters that you can use:

# src/Acme/Bundle/CustomBundle/Resources/config/form_extensions.yml
extensions:
    my_form_extension:
        module: my_custom_view
        parent: pim-product-edit-form-meta
        targetZone: header
        position: 90
        aclResourceId: pim_catalog_product_edit
        config:
            here: you_can_put
            whatever: you_want

What does it mean?¶

First you must specify a key (my_form_extension). As stated above it must be unique in your application.

Then, each view has the following properties:

  • module: It’s the view module that will be rendered. The value is the key declared in the requirejs.yml file for this module.
  • parent: As forms are trees, each view must declare its parent (except the root obviously). The value is the key of the parent, that’s why keys must be unique.
  • targetZone: Views can have different zones. When a child wants to register itself in a parent, it can choose in which zone to be appended. Zones are defined by a data-drop-zone attribute in the DOM.
  • position: When several views are registered in the same parent for the same zone, they are ordered following their positions, in ascending order.
  • aclResourceId: If the current user doesn’t have this ACL granted, the view (and all its children) won’t be included in the final tree.
  • config: A free key to pass parameters to your view explained below

The last key of our module (config) is used to pass the configuration you want to the RequireJS module. You can get it in the initialize method of your module:

1
2
3
4
5
6
7
8
/**
 * {@inheritdoc}
 */
initialize: function (meta) {
    this.meta = meta.config;

    BaseForm.prototype.initialize.apply(this, arguments);
}

As you can see, we receive the entire configuration via the constructor of our view.

Now that our extension is registered, we need to clear the Symfony cache and we are good to go. You should see your extension in the meta section of the product edit form now.

Some extra features¶

Useful methods¶

Here are a list of methods that you can override or call in your extension that should make your life easier.

Managing the model¶

Each form has an internal model representing the current object we are modifying. Here is the way to access it and modify it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
BaseForm.extend({
    render: function () {
        // You can access it from any method, this is just an example

        const model = this.getFormData();

        model.hello = 'world';

        this.setData(model);
    }
});

As you can see, we can get the model by calling this.getFormData() from any extension and update the model with this.setData(). Note that calling setData will trigger the event pim_enrich:form:entity:(pre|post)_update on the root view. You can pass the option silent to true to avoid triggering it (this.setData(data, {silent: true})).

Note

Those two methods will in fact call this.getRoot().setData() and this.getRoot().getFormData() so anywhere in the tree you can get and set the common model.

Configure your extension¶

Sometimes, you want to perform actions before the first render (fetch information, do heavy computation, etc). The configure method is perfect for this need.

1
2
3
4
5
6
7
8
9
BaseForm.extend({
    configure: function () {
        return $.when(function () {
            return $.get('my_url').then(function (elements) {
                this.elements = elements;
            }.bind(this));
        }, BaseForm.prototype.configure.apply(this, arguments));
    }
});

As you can see, the configure method should return a promise. We do that because we want this method to be blocking before the first rendering of the view. We also need to call the parent configure method to configure basic behaviour.

Know your ancestors¶

As an extension, you can access your parent by calling this.getParent() and this.getRoot() to get the root extension of the form.


Found a typo or a hole in the documentation and feel like contributing?
Join us on Github!