Skip to content

Commit

Permalink
Split Widget Events (MicrosoftEdge#613)
Browse files Browse the repository at this point in the history
* Split Widget Events

When we were testing Widgets, we found an issue when opening a PWA using https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow API from the service worker is not possible from the `widgetclick` handler. This is because `client.openWindow` API works only with some event handlers (notificationclick, paymentrequest and backgroundfetchclick).

We can add the support of adding the window interaction in `widgetclick` event but this API (openWindow) is gated for only user interaction events and `widgetclick` event can occur even without user interacting with the widgets (like `widgetresume` event on scrolling the widgets dashboard).

The proposed solution will be dividing the event in to two types of events.

1. “widgtinstall”, “widgetuninstall” and “widgetresume”: Handle widget lifecycle events
self.addEventListener(“widget”, (event) => { if (event.action === “widget-install”) handleInstall() });

2. “widgetclick” Event: Scoped only for user interaction with widgets (Action.Execute)
self.addEventListener(“widget”, (event) => { if (event.action === “verb”) getClient().openWindow() });
  • Loading branch information
adolfdaniel authored Dec 14, 2022
1 parent 25d5577 commit 66e392e
Showing 1 changed file with 138 additions and 76 deletions.
214 changes: 138 additions & 76 deletions PWAWidgets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,75 +690,64 @@ Developers will use `removeByTag()` to remove all Instances of a Widget. This me

## Widget-related Events

There are a host of different events that will take place in the context of a Service Worker. For simplicity, all come through the `widgetClick` event listener.
There are a host of different events that will take place in the context of a Service Worker.

A [`WidgetEvent`](#widget-related-events) is an object with the following properties:
### WidgetEvent

The [`WidgetEvent`](#widgetevent) is a generic event for widgets with the below types.

* "widgetinstall" - Executed when a [Widget Host](#dfn-widget-host) is requesting installation of a widget.
* "widgetuninstall" - Executed when a [Widget Host](#dfn-widget-host) is requesting un-installation of a widget.
* "widgetsave" - Executed when a Widget has settings and the user saves the settings for a specific `WidgetInstance`.
* "widgetresume" - Executed when a [Widget Host](#dfn-widget-host) is switching from its inactive to active state.

A [`WidgetEvent`](#widgetevent) is an object with the following properties:

* `action` - Always required. This is the primary way to disambiguate events. The names of the events may be part of a standard lifecycle or app-specific, based on any [`WidgetAction` that has been defined](#Defining-a-WidgetAction).
* `data` - Always required. This object comprises key/value pairs representing data sent from the [Widget Host](#dfn-widget-host) as part of the event. This could be, for example, the settings values to be saved to the [Widget Instance](#dfn-widget-instance). An empty object if no data is sent.
* `widget` - Required for widget-specific events. This is a reference to the [`Widget`](#the-widget-object) (if any) associated with the event
* `instanceId` - Required for widget-specific events. This is the GUID for the specific [Widget Instance](#dfn-widget-instance) (if any) associated with the event.
* `hostId` - Required for host-specific events. This is the GUID for the specific [Widget Host](#dfn-widget-host) (if any) associated with the event.

#### A Sample [`WidgetEvent`](#widgetevent) Object

```json
{
"action": "create-event",
"widget": { },
"instanceId": "{{ GUID }}",
"data": { }
"instanceId": "{{ GUID }}"
}
```

You can see a basic example of this in use in [the user login video, above](#user-login). There is a walk through of the interaction following that video, but here’s how the actual [`WidgetEvent`](#widget-related-events) could be handled:
Here’s how the actual [`WidgetEvent`](#widgetevent) could be handled:

```js
self.addEventListener('widgetclick', (event) => {

const action = event.action;

// If user is being prompted to login
if ( action == "login" ) {
// open a new window to the login page & focus it
clients
.openWindow( "/login?from=widget" )
.then(windowClient =>
windowClient ? windowClient.focus() : null
);
}
self.addEventListener('widgetinstall', (event) => {
console.log("installing", event.widget, event.instance_id);
event.waitUntil(
createInstance( instance_id, widget )
);

});
```

There are a few special [`WidgetEvent`](#widget-related-events) `action` types to consider as well.

* "widget-install" - Executed when a [Widget Host](#dfn-widget-host) is requesting installation of a widget.
* "widget-uninstall" - Executed when a [Widget Host](#dfn-widget-host) is requesting un-installation of a widget.
* "widget-save" - Executed when a Widget has settings and the user saves the settings for a specific `WidgetInstance`.
* "widget-resume" - Executed when a [Widget Host](#dfn-widget-host) is switching from its inactive to active state.

The <b id="creating-a-WidgetEvent">steps for creating a WidgetEvent</b> with Widget Service Message <var>message</var> are as follows:

1. Let <var>event</var> be a new ExtendableEvent.
1. Let <var>event</var> be a new [`WidgetEvent`](#widgetevent) (inherits ExtendableEvent).
1. Run the following steps in parallel:
1. Set <var>event["data"]</var> to a new object.
1. If <var>message</var> is a request to refresh all widgets
1. Set <var>event["action"]</var> to "widget-resume".
1. Set <var>type</var> to "widgetresume".
1. Set <var>event["hostId"]</var> to the id of the Widget Host bound to <var>message</var>.
1. Return <var>event</var>.
1. Else if <var>message</var> is a request to install a widget, set <var>event["action"]</var> to "widget-install".
1. Else if <var>message</var> is a request to uninstall a widget, set <var>event["action"]</var> to "widget-uninstall".
1. Else if <var>message</var> is a request to update a widget’s settings, set <var>event["action"]</var> to "widget-save".
1. Else set <var>event["action"]</var> to the user action bound to <var>message</var>.
1. Else if <var>message</var> is a request to install a widget, set <var>type</var> to "widgetinstall".
1. Else if <var>message</var> is a request to uninstall a widget, set <var>type</var> to "widgetuninstall".
1. Else if <var>message</var> is a request to update a widget’s settings, set <var>type</var> to "widgetsave".
1. Let <var>instanceId</var> be the id of the Widget Instance bound to <var>message</var>.
1. Set <var>event["instanceId"]</var> to <var>instanceId</var>.
1. Let <var>widget</var> be the result of running the algorithm specified in [getByInstanceId(instanceId)](#widgetsgetbyinstanceid) with <var>instanceId</var>.
1. Set <var>event["widget"]</var> to <var>widget</var>.
1. If <var>message</var> includes bound data,
1. Set <var>event["data"]</var> to the data value bound to <var>message</var>.
1. Return <var>event</var>


### widget-install
### widgetinstall

When the User Agent receives a request to create a new instance of a widget, it will need to create a placeholder for the instance before triggering the WidgetClick event within the Service Worker.

Expand All @@ -767,12 +756,11 @@ Required `WidgetEvent` data:
* `instanceId`
* `widget`

The <b id="creating-a-placeholder-instance">steps for creating a placeholder instance</b> with `WidgetEvent` <var>event</var>:
The <b id="creating-a-placeholder-instance">steps for creating a placeholder instance</b> with `WidgetClickEvent` <var>event</var>:

1. Let <var>widget</var> be event["widget"].
1. If <var>widget</var> is undefined, exit.
1. Let <var>payload</var> be an object.
1. Set <var>payload["data"]</var> to an empty JSON object.
1. Set <var>payload["settings"]</var> to the result of [creating a default `WidgetSettings` object](#creating-a-default-widgetsettings-object) with <var>widget</var>.
1. Let <var>instance</var> be the result of [creating an instance](#creating-a-widget-instance) with <var>event["instanceId"]</var>, <var>event["hostId"]</var>, and <var>payload</var>.
1. Append <var>instance</var> to <var>widget["instances"]</var>.
Expand All @@ -781,11 +769,11 @@ The <b id="creating-a-placeholder-instance">steps for creating a placeholder ins

![](media/install.gif)

1. A "widget-install" signal is received by the User Agent, [the placeholder instance is created](#creating-a-placeholder-instance), and the event is passed along to the Service Worker.
1. The Service Worker makes a `Request` for the `widget.data` endpoint.
1. A "widgetinstall" signal is received by the User Agent, [the placeholder instance is created](#creating-a-placeholder-instance), and the event is passed along to the Service Worker.
1. The Service Worker makes a `Request` for the `widget.definition.data` endpoint.
1. The Service Worker then creates a [payload](#the-widgetpayload-object) and passes that along to the [Widget Service](#dfn-widget-service) via the `updateByInstanceId()` method.

### widget-uninstall
### widgetuninstall

Required `WidgetEvent` data:

Expand All @@ -796,32 +784,32 @@ Required `WidgetEvent` data:

![](media/uninstall.gif)

1. The "widget-uninstall" signal is received by the User Agent and is passed to the Service Worker.
1. The "widgetuninstall" signal is received by the User Agent and is passed to the Service Worker.
1. The Service Worker runs any necessary cleanup steps (such as un-registering a Periodic Sync if the widget is no longer in use).
1. The Service Worker calls `removeByInstanceId()` to complete the removal process.

Note: When a PWA is uninstalled, its widgets must also be uninstalled. In this event, the User Agent must prompt the [Widget Service](#dfn-widget-service) to remove all associated widgets. If the UA purges all site data and the Service Worker during this process, no further steps are necessary. However, if the UA does not purge all data, it must issue uninstall events for each Widget Instance so that the Service Worker may unregister related Periodic Syncs and perform any additional cleanup.

### widget-save
### widgetsave

Required `WidgetEvent` data:

* `instanceId`
* `widget`
* `data`

The "widget-save" process works like this:
The "widgetsave" process works like this:

1. The "widget-save" signal is received by the User Agent.
1. The "widgetsave" signal is received by the User Agent.
1. Internally, the `WidgetInstance` matching the `instanceId` value is examined to see if
a. it has settings and
a. its `settings` object matches the inbound `data`.
1. If it has settings and the two do not match, the new `data` is saved to `settings` in the `WidgetInstance` and the "widget-save" event issued to the Service Worker.
1. If it has settings and the two do not match, the new `data` is saved to `settings` in the `WidgetInstance` and the "widgetsave" event issued to the Service Worker.
1. The Service Worker receives the event and can react by issuing a request for new data, based on the updated settings values.

### widget-resume
### widgetresume

Many [Widget Hosts](#dfn-widget-host) will suspend the rendering surface when it is not in use (to conserve resources). In order to ensure Widgets are refreshed when the rendering surface is presented, the [Widget Host](#dfn-widget-host) will issue a "widget-resume" event.
Many [Widget Hosts](#dfn-widget-host) will suspend the rendering surface when it is not in use (to conserve resources). In order to ensure Widgets are refreshed when the rendering surface is presented, the [Widget Host](#dfn-widget-host) will issue a "widgetresume" event.

Required `WidgetEvent` data:

Expand All @@ -831,6 +819,63 @@ Using this event, it is expected that the Service Worker will enumerate the Widg

![](media/resume.gif)

### WidgetClickEvent

The [`WidgetClickEvent`](#widgetclickevent) is sent to the Service Worker when a user interacts (click/tap) with a Widget. The event handler of [`WidgetClickEvent`](#widgetclickevent) will be capable of making [`clients.openWindow()`](https://www.w3.org/TR/service-workers/#dom-clients-openwindow) to open the PWA.

A [`WidgetClickEvent`](#widgetclickevent) is an object with the following properties:

* `action` - Always required. This is the primary way to disambiguate events. The names of the events may be part of a standard lifecycle or app-specific, based on any [`WidgetAction` that has been defined](#Defining-a-WidgetAction).
* `data` - Always required. This object comprises key/value pairs representing data sent from the [Widget Host](#dfn-widget-host) as part of the event. This could be, for example, the settings values to be saved to the [Widget Instance](#dfn-widget-instance). An empty object if no data is sent.
* `widget` - Required for widget-specific events. This is a reference to the [`Widget`](#the-widget-object) (if any) associated with the event
* `instanceId` - Required for widget-specific events. This is the GUID for the specific [Widget Instance](#dfn-widget-instance) (if any) associated with the event.
* `hostId` - Required for host-specific events. This is the GUID for the specific [Widget Host](#dfn-widget-host) (if any) associated with the event.

#### A Sample [`WidgetClickEvent`](#widgetclickevent) Object

```json
{
"action": "login",
"widget": { },
"instanceId": "{{ GUID }}",
"data": { }
}
```

You can see a basic example of this in use in [the user login video, above](#user-login). There is a walk through of the interaction following that video, but here’s how the actual [`WidgetClickEvent`](#widgetclickevent) could be handled:

```js
self.addEventListener('widgetclick', (event) => {

const action = event.action;

// If user is being prompted to login
if ( action == "login" ) {
// open a new window to the login page & focus it
clients
.openWindow( "/login?from=widget" )
.then(windowClient =>
windowClient ? windowClient.focus() : null
);
}

});
```

The <b id="creating-a-WidgetClickEvent">steps for creating a WidgetClickEvent</b> with Widget Service Message <var>message</var> are as follows:

1. Let <var>event</var> be a new WidgetClickEvent (inherits [`WidgetEvent`](#widgetevent)).
1. Run the following steps in parallel:
1. Set <var>event["data"]</var> to a new object.
1. Set <var>event["action"]</var> to the user action bound to <var>message</var>.
1. Let <var>instanceId</var> be the id of the Widget Instance bound to <var>message</var>.
1. Set <var>event["instanceId"]</var> to <var>instanceId</var>.
1. Let <var>widget</var> be the result of running the algorithm specified in [getByInstanceId(instanceId)](#widgetsgetbyinstanceid) with <var>instanceId</var>.
1. Set <var>event["widget"]</var> to <var>widget</var>.
1. If <var>message</var> includes bound data,
1. Set <var>event["data"]</var> to the data value bound to <var>message</var>.
1. Return <var>event</var>

## Proactively Updating a Widget

While the events outlined above allow developers to respond to widget interactions in real-time, developers will also likely want to update their widgets at other times. There are three primary methods for getting new data into a widget without interaction from a user or prompting via the [Widget Service](#dfn-widget-service):
Expand Down Expand Up @@ -1005,48 +1050,65 @@ async function removeInstance( instance_id, widget )
});
return;
}

self.addEventListener("widgetinstall", function(event) {
const host_id = event.hostId;
const widget = event.widget;
const instance_id = event.instanceId;

console.log("installing", widget, instance_id);
event.waitUntil(
createInstance( instance_id, widget )
);
});

self.addEventListener("widgetuninstall", function(event) {
const host_id = event.hostId;
const widget = event.widget;
const instance_id = event.instanceId;

console.log("uninstalling", widget, instance_id);
event.waitUntil(
removeInstance( instance_id, widget )
);
});

self.addEventListener("widgetresume", function(event) {
const host_id = event.hostId;
const widget = event.widget;
const instance_id = event.instanceId;

console.log("resuming all widgets");
event.waitUntil(
// refresh the data on each widget
updateWidgets( host_id )
);
});

self.addEventListener("widgetclick", function(event) {

const action = event.action;
const host_id = event.hostId;
const widget = event.widget;
const instance_id = event.instanceId;


// Custom Actions
switch (action) {

// If a widget is being installed
case "widget-install":
console.log("installing", widget, instance_id);
event.waitUntil(
createInstance( instance_id, widget )
);
break;

// If a widget is being uninstalled
case "widget-uninstall":
event.waitUntil(
removeInstance( instance_id, widget )
);
break;

// If a widget host is requesting all its widgets update
case "widget-resume":
console.log("resuming all widgets");
event.waitUntil(
// refresh the data on each widget
updateWidgets( host_id )
);
break;

// Custom Actions
case "refresh":
console.log("Asking a widget to refresh itself");
event.waitUntil(
updateInstance( instance_id, widget )
);
break;

case "login":
// open a new window to the login page & focus it.
clients
.openWindow( "/login?from=widget" )
.then(windowClient =>
windowClient ? windowClient.focus() : null
);
break;
// other cases
}

Expand Down

0 comments on commit 66e392e

Please sign in to comment.