Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a (revised) minimal plugin and template system #374

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from

Conversation

ramboz
Copy link
Collaborator

@ramboz ramboz commented Jun 21, 2024

✏️ This is a revised version of #254 that relies more on custom DOM events and reduces the custom JS APIs a bit.

Use case

As a developer, I want to be able to easily organize my code into different files, and execute some of the logic in each phase of the page load as needed.

In particular, I want a way to:

  • easily define templates that have some JS logic
  • add plugins for more advanced features like experimentation, conversion tracking, tag management, etc.
  • load templates and plugins only when they are needed
  • control in what phase a plugin is loaded to avoid negatively impacting performances if it is not needed immediately

Technical Requirements

  • plugins and templates should use the same logic to reduce code duplication
  • plugins and templates should re-use the block loading approach to load both JS and CSS files, if applicable
  • performance impact should be negligible
  • plugins and templates should offer a minimal API on the window.hlx.* namespace
  • the system should help in making plugins and templates easier to unit test (using mostly pure functions)

Proposal

Introduce a new custom event dispatching logic with the capability to await its listeners so we can have blocking logic if needed (i.e. experiments need to replace the content before the page continues rendering). Event handlers can call the ev.await(…) method with a promise.

document.addEventListener('aem:eager', (ev) => {
  ev.await(new Promise((res) => setTimeout(res, 1000)));
});

Introduce new page lifecycle events:

  • aem:section:loaded: triggered when the section is shown
  • aem:block:config: triggered before we load the block js/css so the path may be altered if needed
  • aem:block:decorated: triggered right after the decoration is done, but before block is shown in case we need additional DOM patching
  • aem:block:loaded: triggered when the block is shown
  • aem:eager/aem:lazy/aem:delayed: each dispatched at the start of the respective phase

Introduce 2 new namespaces on window.hlx.*:

  • window.hlx.templates
    • add(id, url): register a new template
    • get(id): get the template
    • has(id): check if a template exists
  • window.hlx.plugins
    • add(id, config): add a new plugin
    • get(id): get the plugin
    • has(id): check if a plugin exists

Plugins and templates would follow the same API:

  • export default function init(document, options): logic executed immediately when the plugin/template is loaded
  • document.addEventListener('aem:eager', (ev) => { … }): logic executed in the eager phase
  • document.addEventListener('aem:lazy', (ev) => { … }): logic executed in the lazy phase
  • document.addEventListener('aem:delayed', (ev) => { … }): logic executed in the delayed phase

Extracting a new loadModule(name, cssPath, jsPath, args) method that both the loadBlock and the new plugin system use.

Usage

For ease of use, we export 2 convenience methods in aem.js, namely withPlugin & withTemplate.

Adding Templates

Add a new template to the project:

withTemplate('foo', '/templates/foo.js');

or the shorthand version:

withTemplate('/templates/bar.js');

or the bulk version:

withTemplate([
  '/templates/bar.js',
  '/templates/baz.js'
]);

or the the module approach that loads both JS and CSS:

withTemplate('/templates/bar');
// loads both /templates/bar/bar.css and /templates/bar/bar.js

Adding Plugins

Add a new inline plugin to the project:

withPlugin('inline-plugin-baz', {
  condition: () => true, // if defined, the condition is evaluated before running any code in the plugin
  eager: () => {  }, // these attach as listeners for the respective `aem:eager` event
  lazy: () => {  },
  delayed: () => {  },
});

Add a regular plugin to the project that will always load (no condition):

withPlugin('plugin-qux', { url: '/plugins/qux/src/index.js' });

or the module approach that loads both JS and CSS:

withPlugin('plugin-qux', { url: '/plugins/qux' });

or the shorthand version:

withPlugin('/plugins/qux');

All plugins load by default in the lazy phase, to offer best performance out of the box. If a plugin needs to load in another phase, this can be done via:

withPlugin('plugin-qux', {
  url: '/plugins/qux/src/index.js',
  load: 'eager', // can be `eager`, `lazy` or `delayed`. defaults to `lazy` if omitted
  options: { corge: 'grault' },
});

Plugin Template

/plugins/qux.js

// document: to keep it consistent with the loadEager in the scripts.js file
// options: additional options passed to the plugin when it was added
// context: passes a plugin context which gives access to the main lib-franklin.js APIs and avoids cyclic dependencies
//          it also turns all of those into pure functions that are easier to unit test
export default (document, options) => {
  console.log('plugin qux: init', options, context);
};

document.addEventListener('aem:eager', (ev) => {
  console.log('plugin qux: eager');
});

document.addEventListener('aem:lazy', (ev) => {
  console.log('plugin qux: lazy');
});

document.addEventListener('aem:delayed', (ev) => {
  console.log('plugin qux: delayed');
});

Examples

A barebone demo site built for this PR:

Test URLs:

Copy link

aem-code-sync bot commented Jun 21, 2024

Page Scores Audits Google
/ PERFORMANCE A11Y SEO BEST PRACTICES SI FCP LCP TBT CLS PSI

@ramboz
Copy link
Collaborator Author

ramboz commented Jun 21, 2024

@rofe @trieloff here is a revised version of the plugin proposal based on:

  1. @trieloff's initial feedback that this should be more event-based
  2. learnings from the v1 version we deployed to simplify the APIs

@ramboz ramboz changed the title Plugin system2 feat: a (revised) minimal plugin and template system Jun 24, 2024
*/
function setup() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved further down in the file.

@@ -541,29 +559,25 @@ async function loadBlock(block) {
block.dataset.blockStatus = 'loading';
const { blockName } = block.dataset;
try {
const cssLoaded = loadCSS(`${window.hlx.codeBasePath}/blocks/${blockName}/${blockName}.css`);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted above in loadModule so we have a generic logic to use also to load the plugins

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants