Fragments, React and Widgets

Fragments are simple, are just some pieces of HTML, CSS and JavaScript that can be added to a page and easily managed by FrontEnd developers.

That was, more or less, the first definition we gave when we started the Modern Site Building project. But we know that today that definition is full of exceptions: fragments also have JSON configuration files, they can be rendered with Freemarker or even generated with a Java class... They have become much more customizable and complex, and they provide lots of new features.

This doesn't mean that we have altered the original concept, they still can be used as simple pieces, but we might have blurred the line between them and Widgets, as some of their functionality overlaps. In this post, I pretend to sharpen those lines and highlight the differences between them.

Allons-y!

Editable fields

Editable fields are probably the main difference between Fragments and Widgets. As Fragments are focused on content display we need some tool that allows defining what we called "editable fields", parts of the HTML markup that can be manipulated by end-users (aka marketers) so they can quickly create Content Pages without relying on external resources.

In the beginning, we added a new lfr-editable HTML tag which allowed a type attribute which type of editable will be used. We chose this syntax because it is valid HTML markup and developers could continue using their editors to create fragments. In both the FrontEnd (edit mode) and BackEnd (view mode), we were processing it as FreeMarker code to generate the final HTML with all editable values replaced.

After some time, we discovered that combining the new lfr-editable tag with CSS was a bit painful: in view mode we were replacing this tag with a div tag, having different HTML markups in edit and view modes and breaking CSS styles. There were still some situations where using an lfr-editable tag was good enough, so we decided to come up with some extra solution that could live with the existing tag. By adding a new data-lfr-editable-id and -type attributes, developers could mark some arbitrary HTML elements as editable without needing extra markup.

Exposing FreeMarker
In the first draft, using FreeMarker to process fragments was a hidden functionality that we needed to use to find editables, but after some time we found that using FreeMarker in HTML was useful (for example using loops to create some dynamic markup). That's why we decided to expose some extra variables (like fragmentNamespace) so users could use it as a tool when creating them.

Configuration

Although Fragments and Widgets have configuration panels, their rendering process is different. Widget configuration panels can contain anything using a JSP file, leaving all freedom and work to Widget developers. On the other hand, fragments have a more limited configuration with a restricted structure, allowing us to render customized configuration fields for each configuration type.

On the one hand, a fragment configuration can be defined just by writing a JSON file without extra work, and we can take care of all the form rendering and configuration management as needed. On the other hand, we can easily find some customization limitations when writing more complex configuration files. We are constantly adding new configuration types to the configuration Schema, but developers still need to adapt their needs to existing configurations.

We are providing these configuration values to both HTML (when using FreeMarker) and JS contexts, by exposing a configuration object user-specified values.

<a class="btn btn-${configuration.buttonSize}" href="#">
  Go Somewhere
</a>

See Button fragment at GitHub.

const video = fragmentElement.querySelector("video");
video.autoplay = configuration.autoPlay;

See Video fragment at Github.

CSS

For fragments, our CSS processing is quite simple: we are not doing anything™. We wanted to start simple, and adding some layer of SCSS (or any other language) preprocessing would increase the complexity of the build process a lot. So for now, we are providing a default auto-generated CSS class that allows users to write encapsulated styles without needing further steps.

This can of course be changed if fragments are created out of Liferay Portal, nothing stops developers from creating their own pipelines before importing them as long as they provide a single CSS file for each fragment.

JavaScript

Most fragments would barely need any JavaScript associated to them. Only more complex ones would need some lines of code to make the magic happen (for example we do have a contributed “Tabs” fragment that needs some JavaScript to toggle each tab, see code in GitHub).

In this case, and to prevent accidentally leaking global variables, we are wrapping all fragment’s JS inside an IFEE. So if a fragment has this this code:

let count = 0;
function handleClick() { alert("Count: " + (count++)); }
fragmentElement.addEventListener("click", handleClick);

It will be rendered like this:

(function () {
  const fragmentElement = /* Black magic */;
  const configuration = /* Extra black magic */;

  let count = 0;
  function handleClick() { alert("Count: " + (count++)); }
  fragmentElement.addEventListener("click", handleClick);
})();

Global variables can still be declared manually by accessing the globalThis object or any other variable available in the global scope, but fragment developers would need to take into account that:

  • Multiple different fragments or multiple instances of the same fragment might coexist.

  • Fragment JS might be executed multiple times, specially in edit mode.

Using React in fragments

Some time ago, we created the Fragment Toolkit to allow developing fragments out of Liferay. The initial idea of this was having some tool that makes talking to the API (to import/export fragments) easier. It maintains a file structure with all fragments and collections, and sends/receives zip files with them. This is something that can be done manually, but having this CLI enables automation.

Since v1.8.0, the Fragment Toolkit is also able to manage “React Fragments”, allowing developers to use JSX code inside their JavaScript. This works because we are relying on Liferay NPM Bundler to transpile them before being sent to portal, so they can reuse the existing React version running inside it (that’s why it is only available on Liferay Portal 7.3+).

All React fragments in portal are depending on the same React instance.

In this case, the rendering process of a Fragment is slightly different: HTML is being used as a placeholder, and the defined React component replaces its content when it is loaded. As a result these fragments cannot have any editable fields, dropzones, or any other dynamic HTML. That would conflict with the generated React code. Fragment developers can still use configuration files, as this configuration is being sent to the Fragment JS code.

export default function({ configuration, fragmentElement }) {
  return <div>Hello World</div>;
}

Liferay NPM Bundler and Webpack configurations can also be extended, giving custom plugins and loaders so you can customize this build step as you want. We are not supporting any custom configuration officially, but it should work with no issues, as shown in this experiment:

const config = require('generator-liferay-fragments').getBundlerConfig();

module.exports = {
  ...config,
  webpack: {
    ...config.webpack,
    module: {
      ...config.webpack.module,
      rules: [
        ...config.webpack.module.rules,
        {
          use: ['style-loader', 'css-loader', 'sass-loader'],
          test: /\.s[ac]ss$/i,
        }
      ]
    }
  }
};

A question might appear here: if I start adding lots of custom code to my fragments… What will the bundler do with it? What if I try to use some different external library? Well, all this process happens outside of portal, so all code stored in the database is already compiled, that’s why React fragments cannot be edited or exported once they have been imported.

Liferay’s bundler is not able to deduplicate these dependencies, so it bundles everything on a single JS file, even if this dependencies have the exact same version. Moreover, if you use multiple instances of the same fragment in portal, the code will be duplicated.

Be careful when creating fragments with external dependencies, the final JS file can become huge.

One possible workaround is adding this shared dependencies to portal and then reusing them in all fragments like a global dependency (or using you own custom dependency management). We have some ongoing investigation that might solve this kind on situations, but for now there is no official solution.

Being able to build fragments before using them in portal seems like a great solution, specially if you already have some reusable React Components, but never forget that the generated code might be much bigger than our original fragment. If you end up with a huge part of the page being rendered as one single fragment, maybe it is time to try splitting it into various smaller fragments.

As always, we will try to continue improving fragment development so you can keep your UI pieces simple, maintainable and reusable.

Thanks!

Related links

Blogs

"Fragments (...) they can be rendered with Freemarker or even generated with a Java class"... can you provide some reference to these two types of usage? TIA