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: add toolbar events #89

Closed
wants to merge 10 commits into from
Closed

feat: add toolbar events #89

wants to merge 10 commits into from

Conversation

lihbr
Copy link
Member

@lihbr lihbr commented Jul 9, 2021

Status

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

Types of changes

  • Bug fix (a non-breaking change which fixes an issue)
  • New feature (a non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

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:

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

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:

type windowMiddlewareFunction = (next: () => void | Promise<void>) => void | Promise<void>;

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:

window.prismic.middleware.previewUpdate = (next) => {
	console.log("Preview session update");
	return next();
};

Performing an async task each time the preview session gets updated:

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:

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:

window.prismic.middleware.previewEnd= (next) => {
	console.log("Preview session end");
	return next();
};

Performing an async task each time the preview session ends:

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:

<script
	async
	defer
	src="https:////static.cdn.prismic.io/prismic.min.js?repo=200629-sms-hoy&new=true"
	data-foo="/bar"
></script>

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:

<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>

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:

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

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:

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

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:

type windowMiddlewareFunction = (next: () => void | Promise<void>) => void | Promise<void>;

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:

window.prismic.toolbar.onPreviewUpdate = (next) => {
	console.log("Preview session update");
	return next();
};

Performing an async task each time the preview session gets updated:

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:

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:

window.prismic.toolbar.onPreviewEnd = (next) => {
	console.log("Preview session end");
	return next();
};

Performing an async task each time the preview session ends:

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:

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.

@lihbr lihbr mentioned this pull request Jul 9, 2021
7 tasks
@lihbr lihbr marked this pull request as ready for review July 9, 2021 10:03
@angeloashmore
Copy link
Member

angeloashmore commented Jul 9, 2021

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: #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
Copy link
Member Author

lihbr commented Jul 12, 2021

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 lihbr changed the title feat: add window and redirect middleware feat: add window middleware Jul 13, 2021
@lihbr
Copy link
Member Author

lihbr commented Jul 13, 2021

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
Copy link
Member Author

lihbr commented Jul 27, 2021

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

@lihbr lihbr changed the title feat: add window middleware feat: add toolbar events Jul 27, 2021
@lihbr
Copy link
Member Author

lihbr commented Jul 27, 2021

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
Copy link
Member

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

window.dispatchEvent(new CustomEvent('prismic'));

@lihbr
Copy link
Member Author

lihbr commented Jul 27, 2021

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

@lihbr
Copy link
Member Author

lihbr commented Jul 28, 2021

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

window.dispatchEvent(new CustomEvent('prismic'));

Updated.

@@ -65,12 +72,30 @@ const Share = {
const State = {
liveStateNeeded: Boolean(getCookie('is-logged-in')) || Boolean(getCookie('io.prismic.previewSession')),

Choose a reason for hiding this comment

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

Check if this is still necessary (if the toolbar is not loaded when no cookie is found, we can get rid of this @arnaudlewis)

@lihbr lihbr mentioned this pull request Sep 29, 2021
@lihbr lihbr closed this Sep 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants