prismicio / prismic-toolbar

An embeddable UI for Prismic content and previews directly on your website.
12 stars 19 forks source link

feat: add toolbar events #89

Closed lihbr closed 3 years ago

lihbr commented 3 years ago

Status

βœ…  Following first, second, and third implementations (described below), this PR is ready, awaiting review & QA.

Types of changes

Overview

This Pull Request adds events to the toolbar in the form of native JavaScript CustomEvent.

The toolbar registers and triggers two custom events on the window object: prismicPreviewUpdate and prismicPreviewEnd. Users can listen to them using the native JavaScript API: window.addEventListener()

These events are meant to solve preview issues some frameworks have while also allowing some others to fully enable their preview capabilities.

Background Information

The toolbar is the companion integration of Prismic within Prismic-powered websites, it is mainly responsible for orchestrating Prismic preview sessions within those. In order to work for all Prismic users, the toolbar is designed to be and remain framework-agnostic.

This design choice appeared to cause issues for some frameworks that require specific actions to be performed for their preview capabilities to work correctly. These issues have been discussed lengthily within the following threads:

An initial proposal and integration were made last year to address these issues but did not lead to a merge for various reasons:

Description

Toolbar events allow users to listen to them on the window object. Registered event handlers are executed sequentially before the default toolbar behavior (hard reloading the page).

Due to their relative complexity, custom events are not meant to be used by users directly. They are meant to be used by framework kits we provide to enable on users' behalf the best preview experience.

Usage

Each toolbar events follow the native JavaScript event API:

window.addEventListener("eventName", (event) => {
    // Preventing default behavior
    event.preventDefault();

    // Accessing event detail
    console.log(event.detail);

    // Performing something...
    console.log("Hello World");
});

Toolbar events are cancelable: if any of the registered handlers calls event.preventDefault(), then the default behavior of the toolbar won't be executed.

Some toolbar events come with additional data, users can access them inside the event.detail object.

prismicPreviewUpdate

The preview update event will be fired every time there's an update on the preview session. The event.detail object will contain the new preview API ref (event.detail.ref).

Logging something each time the preview session gets updated:

window.addEventListener("prismicPreviewUpdate", (event) => {
    console.log("Preview session update");
    console.log(event.detail); // { ref: "..." }
});

Using Nuxt.js built-in refresh to allow hot reload instead of the default toolbar hard reload each time the preview session gets updated:

window.addEventListener("prismicPreviewUpdate", (event) => {
    event.preventDefault();
    window.$nuxt.refresh();
});

prismicPreviewEnd

The preview end event will be fired when the preview session ends (gets closed). The event.detail object will be null.

Logging something each time the preview session ends:

window.addEventListener("prismicPreviewEnd", (event) => {
    console.log("Preview session end");
    console.log(event.detail); // null
});

Exiting Next.js preview correctly each time the preview session ends:

window.addEventListener("prismicPreviewEnd", (event) => {
    event.preventDefault();
    window.location.replace("/api/prismic-preview-end");
});

Concerns

While I was able to test the toolbar locally with success, I wasn't able to test its iframe changes because I'm not able to run wroom locally.

I had to make the following changes to the iframe because the toolbar can now perform hot reload and cannot solely rely on a hard refresh to reset its state anymore: https://github.com/prismicio/prismic-toolbar/pull/89/files#diff-c58e75bd72479a6f9867399b1bb8bd6c1dbceba4eee441a890fb0d39eae9d33d

These changes allow the current preview state and its ref to be updated when the preview ping call returns a new ref, theoretically allowing the toolbar to perform multiple updates without hard reloading the page.

Without these changes, the toolbar wants to constantly update the preview as soon as a first change is detected.

This will need to be tested on wroom or with someone able to run wroom locally.

History

First implementation (window & redirect middleware)
# Overview This Pull Request adds two types of middleware to the toolbar: - _Window middleware_: they are functions registered in the `window` object that the toolbar can call upon certain events; - _Redirect middleware_: they are `data-*` attribute values registered on the toolbar script tag itself that the toolbar uses to redirect users upon certain events if present. Those two types of middleware are both available and fired for the toolbar `previewUpdate` and `previewEnd` events. They are meant to solve preview issues some frameworks have while also allowing some others to fully enable their preview capabilities. # Background Information The toolbar is the companion integration of Prismic within Prismic-powered websites, it is mainly responsible for orchestrating Prismic preview sessions within those. In order to work for all Prismic users, the toolbar is designed to be and remain framework-agnostic. This design choice appeared to cause issues for some frameworks that require specific actions to be performed for their preview capabilities to work correctly. These issues have been discussed lengthily within the following threads: - https://github.com/prismicio/prismic-toolbar/issues/84 - https://github.com/prismicio/issue-tracker-wroom/issues/290 (internal) An initial proposal and integration were made last year to address these issues but did not lead to a merge for various reasons: - https://github.com/prismicio/prismic-toolbar/issues/68 - https://github.com/prismicio/prismic-toolbar/pull/69 # Description ## Window Middleware Window middleware allow registering functions in the `window` object inside the already existing `window.prismic` namespace. If any of those functions are registered the toolbar will then run them and _may_ still execute its default behavior depending on those middleware functions implementations. Due to their _relative_ complexity, window middleware are not meant to be used by users directly. They are meant to be used by framework kits we provide to enable on users' behalf the best preview experience. ### Usage Each middleware function has the following interface: ```typescript type windowMiddlewareFunction = (next: () => void | Promise) => void | Promise; ``` Not _calling and returning_ `next()` inside the middleware will result in the default toolbar behavior not being performed. ### `window.prismic.middleware.previewUpdate` The preview update middleware will be fired every time there's an update on the preview session. _Logging something each time the preview session gets updated:_ ```javascript window.prismic.middleware.previewUpdate = (next) => { console.log("Preview session update"); return next(); }; ``` _Performing an async task each time the preview session gets updated:_ ```javascript window.prismic.middleware.previewUpdate = async (next) => { await new Promise(res => setTimeout(res, 200)); return next(); }; ``` _Using Nuxt.js built-in refresh to allow hot reload instead of the default toolbar hard reload each time the preview session gets updated:_ ```javascript window.prismic.middleware.previewUpdate = async (next) => { window.$nuxt.refresh(); }; ``` ### `window.prismic.middleware.previewEnd` The preview end middleware will be fired when the preview session ends (gets closed). _Logging something each time the preview session ends:_ ```javascript window.prismic.middleware.previewEnd= (next) => { console.log("Preview session end"); return next(); }; ``` _Performing an async task each time the preview session ends:_ ```javascript window.prismic.middleware.previewEnd= async (next) => { await new Promise(res => setTimeout(res, 200)); return next(); }; ``` ## Redirect Middleware Redirect middleware allow registering routes using `data-*` attributes on the toolbar script itself. If any of those attributes are registered the toolbar will then redirect users to those routes _instead_ of executing its default behavior (refreshing the current location). Due to their _relative_ user-friendly interface, redirect middleware might be used by users directly. While remaining optional for most users, this additional configuration step _might_ be necessary for some users to get previews to work correctly with their framework. ### Usage Each redirect middleware gets registered on the toolbar script itself: ```html ``` ### `data-redirect-url-on-update` The preview update redirect specifies a route to redirect the user to every time there's an update on the preview session. _Redirecting to `/api/prismic-preview-update` each time the preview session gets updated:_ ```html ``` ### `data-redirect-url-on-end` The preview end redirect specifies a route to redirect the user to when the preview session ends (gets closed). _Redirecting to `/api/prismic-preview-end` each time the preview session ends:_ ```html ``` ## Additional Information Window and redirect middleware can be used together. Window middleware are executed _before_ redirect middleware as executing them after wouldn't be possible. Not calling `next()` inside a window middleware will result in the following redirect middleware not being applied. # Concerns While I was able to test the toolbar locally with success, I wasn't able to test its iframe changes because I'm not able to run wroom locally. I had to make the following changes to the iframe because the toolbar can now perform hot reload and cannot solely rely on a hard refresh to reset its state anymore: https://github.com/prismicio/prismic-toolbar/pull/89/files#diff-c58e75bd72479a6f9867399b1bb8bd6c1dbceba4eee441a890fb0d39eae9d33d These changes allow the current preview state and its ref to be updated when the preview ping call returns a new ref, theoretically allowing the toolbar to perform multiple updates without hard reloading the page. Without these changes, the toolbar wants to constantly update the preview as soon as a first change is detected. This will need to be tested on wroom or with someone able to run wroom locally.
Second implementation (window middleware)
# Overview This Pull Request adds window middleware to the toolbar in the form of events that can be registered. Window middleware are functions registered on the `window` object that the toolbar can call upon certain events. They are available and fired for the toolbar `previewUpdate` and `previewEnd` events. Window middleware are meant to solve preview issues some frameworks have while also allowing some others to fully enable their preview capabilities. # Background Information The toolbar is the companion integration of Prismic within Prismic-powered websites, it is mainly responsible for orchestrating Prismic preview sessions within those. In order to work for all Prismic users, the toolbar is designed to be and remain framework-agnostic. This design choice appeared to cause issues for some frameworks that require specific actions to be performed for their preview capabilities to work correctly. These issues have been discussed lengthily within the following threads: - https://github.com/prismicio/prismic-toolbar/issues/84 - https://github.com/prismicio/issue-tracker-wroom/issues/290 (internal) An initial proposal and integration were made last year to address these issues but did not lead to a merge for various reasons: - https://github.com/prismicio/prismic-toolbar/issues/68 - https://github.com/prismicio/prismic-toolbar/pull/69 # Description Window middleware allow registering functions on the `window` object inside the already existing `window.prismic` namespace. If any of those functions are registered the toolbar will then run them and _may_ still execute its default behavior depending on those middleware functions implementations. Due to their _relative_ complexity, window middleware are not meant to be used by users directly. They are meant to be used by framework kits we provide to enable on users' behalf the best preview experience. ## Usage Each middleware function has the following interface: ```typescript type windowMiddlewareFunction = (next: () => void | Promise) => void | Promise; ``` Not _calling and returning_ `next()` inside the middleware will result in the default toolbar behavior not being performed. ## `window.prismic.toolbar.onPreviewUpdate` The preview update middleware will be fired every time there's an update on the preview session. _Logging something each time the preview session gets updated:_ ```javascript window.prismic.toolbar.onPreviewUpdate = (next) => { console.log("Preview session update"); return next(); }; ``` _Performing an async task each time the preview session gets updated:_ ```javascript window.prismic.toolbar.onPreviewUpdate = async (next) => { await new Promise(res => setTimeout(res, 200)); return next(); }; ``` _Using Nuxt.js built-in refresh to allow hot reload instead of the default toolbar hard reload each time the preview session gets updated:_ ```javascript window.prismic.toolbar.onPreviewUpdate = (next) => { window.$nuxt.refresh(); }; ``` ## `window.prismic.toolbar.onPreviewEnd` The preview end middleware will be fired when the preview session ends (gets closed). _Logging something each time the preview session ends:_ ```javascript window.prismic.toolbar.onPreviewEnd = (next) => { console.log("Preview session end"); return next(); }; ``` _Performing an async task each time the preview session ends:_ ```javascript window.prismic.toolbar.onPreviewEnd = async (next) => { await new Promise(res => setTimeout(res, 200)); return next(); }; ``` _Exiting Next.js preview correctly each time the preview session ends:_ ```javascript window.prismic.toolbar.onPreviewEnd = (next) => { window.location.replace("/api/prismic-preview-end"); }; ``` # Concerns While I was able to test the toolbar locally with success, I wasn't able to test its iframe changes because I'm not able to run wroom locally. I had to make the following changes to the iframe because the toolbar can now perform hot reload and cannot solely rely on a hard refresh to reset its state anymore: https://github.com/prismicio/prismic-toolbar/pull/89/files#diff-c58e75bd72479a6f9867399b1bb8bd6c1dbceba4eee441a890fb0d39eae9d33d These changes allow the current preview state and its ref to be updated when the preview ping call returns a new ref, theoretically allowing the toolbar to perform multiple updates without hard reloading the page. Without these changes, the toolbar wants to constantly update the preview as soon as a first change is detected. This will need to be tested on wroom or with someone able to run wroom locally.
angeloashmore commented 3 years ago

This looks great and really opens up a lot of possibilities! Hot reloading preview updates and exits is a killer feature.

I have some thoughts below on the API and how we might alter the implementation.

Simpler API

Supporting both data- redirects and window.prismic.middleware functions feels a bit messy in my opinion. If the middleware functions can do whatever the data- redirects can do, I think we should only have the more flexible option. The data- attribute doesn't save much typing effort:

Example 1: Using middleware to redirect on a preview update.

<script
  async
  defer
  src="https://static.cdn.prismic.io/prismic.min.js?repo=200629-sms-hoy&new=true"
></script>
<script>
  window.prismic.middleware.previewUpdate = () => {
    window.location = '/api/prismic-preview-update'
  }
</script>

Compared to the data- attribute method:

Example 2: Using data-redirect-url-on-update to redirect on a preview update.

<script
  async
  defer
  src="https://static.cdn.prismic.io/prismic.min.js?repo=200629-sms-hoy&new=true"
  data-redirect-url-on-update="/api/prismic-preview-update"
></script>

Example 1 is longer, but it is clearer what it is doing. It is also extensible if necessary, where Example 2 is not immediately extensible. Example 2 could still implement a middleware function, but now functionality is split between two APIs.

In other words, Example 1 is clearer, more obviously extensible, and should be simpler to maintain for both consumers of and developers working on the toolbar.

(I recognize that I posted a long message on proposing the data- attributes, but that was before we discussed the middleware functions in more depth πŸ˜…)

Naming

window.prismic.middleware sounds vague. It's not clear what window.prismic is for, and as a result, it's not clear what the middleware object is acting on.

window.prismic is an existing API from a previous version of the toolbar (and maybe for other uses?), so that should not change. But we can still make it clearer what the middleware property is designed for. This allows window.prismic to be extended to other use cases in the future, such as middleware for a different package.

window.prismic.toolbar = {
  onPreviewUpdate: (next) => {
    // Called on update events
  },
  onPreviewExit: (next) => {
    // Called on exit events
  },
}

In addition to renaming middleware to toolbar, we can also rename the properties to be more event-like. previewUpdate could be named onPreviewUpdate to imply that the function is called "on preview updates."

Requires discussion: Use native events?

Event handlers are typically handled using addEventListener for events like click and blur.

element.addEventListener('click', () => {
  // Called on click events
})

Should we use this existing concept rather than build our own event dispatcher and handler?

Note: @lihbr originally proposed something very similar here: https://github.com/prismicio/prismic-toolbar/issues/68

Why should we use it?

Using the native event system allows us to hook into the existing event framework. It means we get support for multiple listeners, default behavior management, and listener cancellation for free.

Why shouldn't we use it?

It could introduce education complexity as there are more concepts to address. This can be mitigated by simply ignoring the advanced use cases and relying on users to understand native browser event management to make use of it. We can teach the simple, common use cases.

How would we use it?

Custom events can be registered using new Event(eventName). With this, arbitrary event types can be created, triggered, and reacted upon.

// In the toolbar, create a new event. CustomEvent is an Event with custom associated data.
const event = new CustomEvent('prismicPreviewUpdate', {
  detail: 'arbitrary data can be provided if needed',
})

// Dispatch the event when an update happens:
window.dispatchEvent(event)

In users' code, event listeners can be managed like any other event listener:

const onPrismicPreviewUpdate = (event) => {
  // Called on preview update events
}

const alsoOnPrismicPreviewUpdate = (event) => {
  // Called on preview update events
}

// Add listeners:
window.addEventListener('prismicPreviewUpdate', onPrismicPreviewUpdate)
window.addEventListener('prismicPreviewUpdate', alsoOnPrismicPreviewUpdate)

// Use native event functions to remove a listener:
window.removeEventListener('prismicPreviewUpdate', onPrismicPreviewUpdate)

This gives the user flexibility in adding their own event listeners, but also enables framework integrations to add their own.

In the case that we want to support default behavior, such as the existing full page refresh, we can rely on event.preventDefault to determine if the default behavior should be performed.

window.addEventListener('prismicPreviewUpdate', (event) => {
  event.preventDefault()
  // Do things without the full page refresh
})

In the toolbar's code, we can detect if event.preventDefault was called via the return value of window.dispatchEvent. Any event listener can cancel the default behavior.

// In the toolbar, register a new event.
const event = new CustomEvent('prismicPreviewUpdate')

// Dispatch the event when an update happens:
const shouldPerformDefaultBehavior = window.dispatchEvent(event)

if (shouldPerformDefaultBehavior) {
  window.location.reload()
}
lihbr commented 3 years ago

Thanks @angeloashmore!

Simpler API

I agree, will remove the data-* attribute way of doing things.

Naming

Works for me if we don't decide to go the native events way.

Native events

That's the implementation I prefer too. If we find consensus with @arnaudlewis on it, I'll refactor the code to use the native event API!

lihbr commented 3 years ago

I refactored the code to implement the simpler API and naming proposed by @angeloashmore. The initial PR post has been updated accordingly.

The last topic remaining to be answered is whether or not we should migrate to native events, apart from that one the PR is ready.

lihbr commented 3 years ago

Update: code needs to be refactored to use native events we agreed on using during our last open-source meeting.

lihbr commented 3 years ago

Refactored the code to use native custom events. Updated the initial post to reflect those API changes.

βœ…  The PR is ready, awaiting review & QA.

angeloashmore commented 3 years ago

Looks nice and clean. Should the existing "init" event use the same framework you have setup with toolbarEvents and dispatchToolbarEvent?

https://github.com/prismicio/prismic-toolbar/blob/94ef3d89cfcef8951b1bf12fc104afd22008e0b3/src/toolbar/index.js#L43

lihbr commented 3 years ago

Was kinda afraid to break something haha, but yes, I'll add it!

lihbr commented 3 years ago

Looks nice and clean. Should the existing "init" event use the same framework you have setup with toolbarEvents and dispatchToolbarEvent?

https://github.com/prismicio/prismic-toolbar/blob/94ef3d89cfcef8951b1bf12fc104afd22008e0b3/src/toolbar/index.js#L43

Updated.