Playing with some ideas around widget definitions for PWAs
25
stars
0
forks
source link
readme
PWA widgets
Playing with some ideas around widget definitions for PWAs.
Notes:
These are my own ideas and are not reflective of anything my employer may or may not do in the future. I am just doing some thinking around what Widgets could be like if we were able to connect them to a PWA.
I’m no longer working on the Edge team, but my colleagues on that team are working on enabling web widgets. To follow along with that work, consult the Explainer on the Microsoft Edge repo. I will do my best to keep this doc up to date as we make tweaks to the APIs, but please recognize it may become out of sync from time to time.
The idea
For the last few years, I’ve been thinking about the many ways native applications can expose information and/or focused tasks within operating systems. Examples of this include Android Home Screen Widgets, macOS Dashboard and Today Panel Widgets, the Apple Touch Bar, Samsung Daily Cards, Mini App Widgets, smart watch app companions, and so on. When building Progressive Web Apps, it would useful to be able to project aspects of the web app onto these surfaces (originally I’d considered these "projections" but most systems do call them "widgets," so I opted to go with the flow).
Here are a few use cases:
A streaming video service could offer access to all of the shows or movies you have in your queue that is distinct from the actual player. It might live in a widget on one device, but, with access to all of the plumbing of the PWA itself, could enable users to control the services’ PWA running on the user’s smart TV.
A stock tracking app could offer a widget for viewing current stock prices for stocks you are watching.
A calendar service could provide a daily agenda at a glance.
A music identification service could have a button widget that, when clicked, would access the microphone and attempt to ID the currently playing song.
It’s expected that each Widget Host will provide different opportunities and have different constraints that depend on a variety of factors including memory use and power management. As such, this proposal outlines a means of defining two alternative approaches within the same Widget definition (WidgetDefinition) that would accommodate both scenarios:
Under this proposal, developers would be free to define Widgets that support both approaches or only one. In supporting only one, they should be aware that choosing a single path may limit the distribution/install-ability of that Widget.
Definitions
Nouns
Widget
A discrete user experience that represents a part of a website or app’s functionality. Refers to the prototypical definition of an experience (e.g., follow an account), *not* the individual representations of this widget (e.g., follow bob) that exist in a [Widget Host](#dfn-widget-host).
Widget Host
A container that manages and renders widgets.
Widget Instance
The interactive experience of a [Widget](#dfn-widget) within a [Widget Host](#dfn-widget-host). Multiple instances of a [Widget](#dfn-widget) may exist within a [Widget Host](#dfn-widget-host). These distinct instances may have associated [settings](#dfn-widget-settings).
Widget Settings
Configuration options, defined on a [Widget](#dfn-widget) and unique to a [Widget Instance](#dfn-widget-instance), that enable that instance to be customized.
Widget Provider
An application that exposes Widgets. A browser would likely be the Widget Provider on behalf of its PWAs and would act as the proxy between those PWAs and any Widget Service.
Widget Registry
The list of installableWidgets [registered](#dfn-register) by [Widget Providers](#dfn-widget-provider).
Widget Service
Manages communications between [Widget Hosts](#dfn-widget-host) and [Widget Providers](#dfn-widget-provider).
Verbs
Install Instantiate
Create a [Widget Instance](#dfn-widget-instance).
Register
Add a [Widget](#dfn-widget) to the [Widget Registry](#dfn-widget-registry).
Uninstall
Destroy a [Widget Instance](#dfn-widget-instance).
Unregister
Remove a [Widget](#dfn-widget) from the [Widget Registry](#dfn-widget-registry).
Update
Push new data to a [Widget Instance](#dfn-widget-instance).
Rich Widgets
Note: While this doc does include some discussion of Rich Widgets, there is a lot more work that needs to be done to explore what Rich Widgets will look like and how they will operate. This doc focuses mainly on Templated Widgets as they are less resource intensive and will play better with current widget implementations.
There are instances in which a templated widget is incapable of accomplishing the goal of a widget (e.g., turning on the microphone to identify the song that’s playing, frequently updating to display an accurate clock). For those instances, developers will a lot more control. This is where Rich Widgets come in. Rich Widgets are wholly developer managed at a URL they control.
As fully-rendered web pages (analogous to an iframe), Rich Widgets need to come with some restrictions that protect user privacy and limit stress on system resources. Each Widget Host will likely have its own set of restrictions for Rich Widgets (if it even allows them). These will likely include:
Limited RAM footprint - recommendation TBD,
Limited JavaScript execution time - recommendation TBD,
Permissions-governed API usage (e.g., location, microphone) would require user interaction to activate,
Initiating any multimedia playback requires user interaction (though "piggybacking" on pre-existing multimedia playback initiated from within the same origin would not be similarly restricted), and
Data updates must be performed by the Service Worker.
Additionally, the Widget Host will likely reserve the right to suspend a widget at any time. Looking at how many renderers handle suspension, it’s likely that many Widget Hosts will create a bitmap representation of a Widget in its last-known state. The origin’s Service Worker may periodically request to update the Widget (and create a new snapshot) when it’s not in use.[^1] For more on this, consult the events section.
Given that the web is responsive, a Rich Widget’s dimensions should not matter much. The content would merely adapt to the available real estate. Some Widget Hosts may even support user resizing of a Widget.
In more restrictive or resource-limited scenarios, a Widget Host may choose to provide enable widgets via an internal templating system. Templated widgets may be more limited in their customization through use of the PWA’s icons, theme_color, and background_color (similar to Notifications) or they may be fully-customizable through some form of templating language. Examples of structured templates include an agenda, calendar, mailbox, and a task list. A Widget Host’s complete list of available templates will likely vary by Widget Host. This proposal suggests this list of widgets template types as a reasonable starting point.
Suggested template types
For social and productivity apps:
calendar-agenda
calendar-day
calendar-week
calendar-month
For address books, directories, and social apps:
contacts-list
contacts-item
For general purposes (e.g., news, promotions, media, social):
Data (in the form of interaction) flows from a Widget to the associated Service Worker via WidgetEvents.
Here is an example of how this might look in the context of a Periodic Sync:
This video shows the following steps:
As part of a Periodic Sync, the Service Worker makes a Request to the host or some other endpoint.
The Response comes back.
As the Service Worker is aware of which widgets rely on that data, via the WidgetDefinition provided during install, the Service Worker can identify which widgets need updating. (This is internal logic and not shown in the video).
The Service Worker takes that data—perhaps packaging it with other instructions—and uses widgets.updateByInstanceId() (or widgets.updateByTag()) to update the specific widgets that make use of that data.
To show a more complicated example, consider what should happen if certain Widgets depend on authentication and the user happens to log out in the PWA or a browser tab. The developers would need to track this and ensure the Service Worker is notified so it can replace any auth-requiring Widgets with a prompt back into the app to log in.
Here’s how that might work:
This video shows:
The user logging out from the context of a Client. When that happens, the Client, sends a postMessage() to the Service Worker, alerting it to the state change in the app.
The Service Worker maintains a list of active Widgets and is aware of which ones require authentication (informed by the auth property of the WidgetDefinition). Knowing auth has been revoked, the Service Worker pushes a new template to each auth-requiring Widget with a notice and a button to prompt the user to log in again.
The next step in this flow is for the user to log back in. They could do that directly in the Client, but let’s use the WidgetAction provided in the previous step:
This video shows:
The user clicking the "Login" action in the Widget. This triggers a WidgetEvent named "login".
The Service Worker is listening for that action and redirects the user to the login page of the app, either within an existing Client or in a new Client (if one is not open).
The user logs in and the app sends a postMessage() to the Service Worker letting it know the user is authenticated again.
The Service Worker grabs new data for its auth-related widgets from the network.
This remainder of this document contains my original proposal and is not up to date with the latest imeplementation in Edge. I’ve maintained it for posterity only.
## Defining a Widget
One or more Widgets are defined within the `widgets` member of a Web App Manifest. The `widgets` member would be an array of `WidgetDefinition` objects.
### Sample `WidgetDefinition` Object
```json
{
"name": "Agenda",
"tag": "agenda",
"url": "/widgets/agenda/",
"type": "text/calendar",
"template": "agenda",
"data": "/widgets/data/agenda.ical",
"auth": true,
"update": 900,
"icons": [ ],
"screenshots": [ ],
"backgrounds": [ ],
"actions": [ ],
"settings": [ ]
}
```
### Required properties
* `name` - `DOMString`. Serves as the title of the widget presented to users.
* `tag` - `DOMString`. Serves as a way to reference the widget within the Service Worker as a `WidgetClient` and is analogous to a [Notification `tag`](https://notifications.spec.whatwg.org/#tag). `WidgetClient` still needs to be defined, but would be similar to [`WindowClient`](https://www.w3.org/TR/service-workers/#ref-for-dfn-window-client).
### Optional properties
* `short_name` - `DOMString`. An alternative short version of the `name`.
* `screenshots` - `Array`. Analogous to [the `screenshots` member of the Manifest](https://w3c.github.io/manifest-app-info/#screenshots-member). It is an array of [`ImageResource` objects](https://www.w3.org/TR/image-resource/#dom-imageresource) with optional [`platform`](https://w3c.github.io/manifest-app-info/#platform-member) values that can associate the screenshot with how it shows up in a specific [Widget Host](#dfn-widget-host). Developers should be sure to include a [`label`](https://w3c.github.io/manifest-app-info/#label-member) value in each [`ImageResource` object](https://www.w3.org/TR/image-resource/#dom-imageresource) for accessibility.
* `auth` - Boolean. Informational. Whether or not the Widget requires auth. False if not included.
* `update` - Unsigned Integer. Informational. The frequency (in seconds) a developer wishes for the widget to be updated; for use in registering a Periodic Sync. The actual update schedule will use the Service Worker’s Periodic Sync infrastructure.
### Rich Widget properties
A [Rich Widget](#Rich-Widgets) MUST include:
* `url` - A valid `URL` string pointing to the Widget’s HTML. That URL MUST exist within [the scope of the Web App Manifest](https://w3c.github.io/manifest/#dfn-scope) and [the scope of the Service Worker](https://w3c.github.io/ServiceWorker/#dom-serviceworkerregistration-scope).
*Note: Rich Widgets will likely be resource-limited, so developers are advised to avoid relying on large client-side libraries to render and update Rich Widgets.*
### Templated Widget properties
A [Templated Widget](#Templated-Widgets) MUST include the following properties:
* `data` - the `URL` where the data for the widget can be found; if the format is unsupported, the widget would not be offered.
* `type` - the MIME type of the data feed for the widget; if unsupported, the widget would not be offered.
* `template` - the template the developer would like the [Widget Host](#dfn-widget-host) to use; if unsupported, the host may offer an analogous widget experience (determined using the `type` value) or the widget would not be offered.
A developer MAY define the following display-related properties:
* `icons` - an array of alternative icons to use in the context of this Widget; if undefined, the Widget icon will be the chosen icon from [the Manifest’s `icons` array](https://w3c.github.io/manifest/#icons-member).
* `backgrounds` - an array of alternative background images (as [`ImageResource` objects](https://www.w3.org/TR/image-resource/)) that could be used in the template (if the [Widget Host](#dfn-widget-host) and template support background images).
A Manifest’s [`theme_color`](https://w3c.github.io/manifest/#theme_color-member) and [`background_color`](https://w3c.github.io/manifest/#background_color-member), if defined, may also be provided alongside this data.
A developer MAY define the following UI-related properties:
* `actions` - An array of [`WidgetAction` objects](#Defining-a-WidgetAction) that will be exposed to users (if the template supports them) within an action-supporting template and trigger an event within the origin’s Service Worker.
* `settings` - A array of [`WidgetSettingDefinition` objects](#Defining-a-WidgetSetting) that enable multiple instances of the same widget to be configured differently within a [Widget Host](#dfn-widget-host) (e.g., a weather widget that displays a single locale could be installed multiple times, targeting different cities).
### Defining a `WidgetAction`
A `WidgetAction` uses the same structure as a [Notification Action](https://notifications.spec.whatwg.org/#dictdef-notificationaction):
```json
{
"action": "create-event",
"title": "New Event",
"icons": [ ]
}
```
The `action` and `title` properties are required. The `icons` array is optional but the icon may be used in space-limited presentations with the `title` providing its [accessible name](https://w3c.github.io/aria/#dfn-accessible-name).
When activated, a `WidgetAction` will dispatch a [`WidgetEvent`](#widget-related-events) (modeled on [`NotificationEvent`](https://notifications.spec.whatwg.org/#example-50e7c86c)) within its Service Worker. Within the Service Worker, the event will contain a payload that includes a reference to the Widget itself and the `action` value.
### Defining a `WidgetSettingDefinition`
A `WidgetSetting` defines a single field for use in a widget’s setting panel.
```json
{
"label": "Where do you want to display weather for?",
"name": "locale",
"description": "Just start typing and we’ll give you some options",
"type": "autocomplete",
"options": "/path/to/options.json?q={{ value }}",
"default": "Seattle, WA USA"
}
```
Breaking this down:
* `label` is the visible text shown to the end user and acts as the accessible label for the field.
* `name` is the internal variable name used for the field (and is the key that will be sent back to the PWA).
* `description` is the _optional_ accessible description for a field, used to provide additional details/context.
* `type` is the field type that should be used. Support for the following field types are recommended:
* Basic text field types: "text" || "email" || "password" || "tel" || "url" || "number"
* One of many selection (requires `options`): "boolean" || "radio" || "select"
* Many of many selection (requires `options`): "checkbox"
* Temporal: "date" || "datetime"
* Other: "file" || "color" || "range"
* Auto-complete (requires `options`): "autocomplete"
* `options` is used for specific field `type`s noted above. It can be either an array of options for the field or a URL string referencing an endpoint expected to return an array of values. If the list is dynamic (as in the case of an autocomplete field), the URL endpoint may be passed the current `value` of the field via the reference "{{ value }}".
* `default` is the _optional_ default value for the setting.
### Extensibility
We recognize that some widget platforms may wish to allow developers to further refine a Widget’s appearance and/or functionality within their system. We recommend that those platforms use [the extensibility of the Manifest](https://www.w3.org/TR/appmanifest/#extensibility) to allow developers to encode their widgets with this additional information, if they so choose.
For example, if using something like [Microsoft’s Adaptive Cards](https://docs.microsoft.com/en-us/adaptive-cards/templating/) for rendering, a [Widget Host](#dfn-widget-host) might consider adding something like the following to the `WidgetDefinition`:
```json
"ms_ac_template": "/widgets/templates/agenda.ac.json",
```
## Registering Available Widgets
In order for [Widget Hosts](#dfn-widget-host) to be aware of what widgets are available for install, the available widgets must be added to the [Widget Registry](#dfn-widget-registry) in some way. That registration should include the following details from the Web App Manifest and the Widget itself:
* manifest["name"]
* manifest["short_name"] (optionally)
* manifest["icons"]
* manifest["lang"]
* manifest["dir"]
* widget["name"]
* widget["short_name"] (optionally)
* widget["icons"] (optionally)
* widget["screenshots"] (optionally)
The steps for parsing widgets from a Web App Manifest with Web App Manifest manifest:
1. Let widgets be a new list.
1. Let collected_tags be a new list.
1. Run the following steps in parallel:
1. For each manifest_widget in manifest["widgets"]:
1. If manifest_widget["tag"] exists in collected_tags, continue.
1. Let widget be a new object.
1. Set widget["tag"] to the value of manifest_widget["tag"].
1. Set widget["definition"] to the value of manifest_widget.
1. Set widget["hasSettings"] to false.
1. Set widget["instances"] to an empty array.
1. Set widget["installable"] to the result of [determining widget installability](#determining-installability) with manifest_widget, manifest, and Widget Host.
1. If widget["installable"] is true
1. Run the steps necessary to register manifest_widget with the [Widget Registry](#dfn-widget-registry), with manifest as necessary.
1. Add manifest_widget["tag"] to collected_tags.
1. Add widget to widgets.
1. Store a copy of widgets for use with the Service Worker API.
The steps for determining install-ability with `WidgetDefinition` widget, Web App Manifest manifest, and Widget Host host are as follows:
1. If host requires any of the above members and they are omitted, classify the Widget as uninstallable and exit.
1. If host **only** supports [rich widgets](#rich-widgets) and widget["url"] is omitted, classify the Widget as uninstallable and exit.
1. If host **only** supports [templated widgets](#templated-widgets) and widget["template"] and widget["data"] are omitted, classify the Widget as uninstallable and exit.
1. If widget["template"] is not an acceptable template generic name according to host, classify the Widget as uninstallable and exit.
1. If widget["type"] is not an acceptable MIME type for widget["data"] according to host, classify the Widget as uninstallable and exit.
1. If host has additional requirements that are not met by widget (e.g., required `WidgetDefinition` extensions), classify the Widget as uninstallable and exit.
1. Classify the widget as installable.
## Service Worker APIs
This proposal introduces a `widgets` attribute to the [`ServiceWorkerGlobalScope`](https://www.w3.org/TR/service-workers/#serviceworkerglobalscope-interface). This attribute references the `Widgets` interface (which is analogous to `Clients`) that exposes the following Promise-based methods:
* `getByTag()` - Requires an tag that matches a Widget’s `tag`. Returns a Promise that resolves to a `Widget` or *undefined*.
* `getByInstanceId()` - Requires an instance id that is used to find the associated `Widget`. Returns a Promise that resolves to a `Widget` or *undefined*.
* `matchAll()` - Requires [an `options` argument](#Options-for-Matching). Returns a Promise that resolves to an array of zero or more [`Widget` objects](#the-widget-object) that match the `options` criteria.
* `createInstance()` - Requires a host `id` and a [payload Object](#the-widgetpayload-object). Returns a Promise that resolves to the [`WidgetInstance`](#the-widgetinstance-object) `id` or Error.
* `updateByInstanceId()` - Requires an instance `id` and a [payload Object](#the-widgetpayload-object). Returns a Promise that resolves to *undefined* or Error.
* `removeByInstanceId()` - Requires an instance `id`. Returns a Promise that resolves to *undefined* or Error.
* `updateByTag()` - Requires an tag and a [payload Object](#the-widgetpayload-object). Returns a Promise that resolves to *undefined* or Error.
* `removeByTag()` - Requires an tag. Returns a Promise that resolves to *undefined* or Error.
### How Widgets are Represented in these APIs
Each Widget defined in the Web App Manifest is represented within the `Widgets` interface. [A `Widget` Object](#the-widget-object) is used to represent each defined widget and any associated [Widget Instances are exposed within that object](#the-widgetinstance-object).
#### The `Widget` Object
Each Widget is represented within the `Widgets` interface as a `Widget`. Each Widget’s representation includes the original `WidgetDefinition` (as `definition`), but is mainly focused on providing details on the Widget’s current state and enables easier interaction with its [Widget Instances](#dfn-widget-instance):
```js
{
"installable": true,
"hasSettings": false,
"definition": { },
"instances": [ ]
}
```
All properties are Read Only to developers and are updated by the User Agent as appropriate.
* `installable` - Boolean. Indicates whether the Widget is installable (based on UA logic around regarding data `type`, chosen `template`, etc.).
* `hasSettings` - Boolean. Indicates whether the `WidgetDefinition` includes a non-empty `settings` array.
* `definition` - Object. The original, as-authored, `WidgetDefinition` provided in the Manifest. Includes any [proprietary extensions](#Extensibility)).
* `instances` - Array. A collection of `WidgetInstance` objects representing the current state of each instance of a Widget (from the perspective of the Service Worker). Empty if the widget has not been [installed](#dfn-install).
#### The `WidgetInstance` Object
```js
{
"id": {{ GUID }},
"host": {{ GUID }},
"settings": { },
"updated": {{ Date() }},
"payload": { }
}
```
All properties are Read Only to developers and are updated by the implementation as appropriate.
* `id` - String. The internal GUID used to reference the `WidgetInstance` (typically provided by the [Widget Service](#dfn-widget-service)).
* `host` - String. Internal pointer to the [Widget Host](#dfn-widget-host) that has installed this `WidgetInstance`.
* `settings` - Object. If the Widget has [settings](#dfn-widget-settings), the key/values pairs set for this [instance](#dfn-widget-instance) are enumerated here.
* `updated` - Date. Timestamp for the last time data was sent to the `WidgetInstance`.
* `payload` - Object. The last payload sent to this `WidgetInstance`.
The steps for creating a `WidgetInstance` with id, host, and payload are as follows:
1. Let instance be a new Object.
1. If id is not a String or host is not a String or payload is not a `WidgetPayload`, throw an Error.
1. Set instance["id"] to id.
1. Set instance["host"] to host.
1. Set instance["settings"] to payload["settings"].
1. Set instance["payload"] to payload.
1. Set instance["updated"] to the current timestamp.
1. Return instance.
The steps for creating a default `WidgetSettings` object with `Widget` widget are as follows:
1. Let settings be a new Object.
1. For each setting in wiget["definition"]["settings"]
1. If setting["default"] is not null:
1. Set settings[setting["name"]] to setting["default"].
2. Else:
1. Set settings[setting["name"]] to an empty string.
1. Return settings.
### Finding Widgets
There are three main ways to look up information about a Widget: by `tag`, by instance `id`, by Widget Host, and [by characteristics](#widgetsmatchall).
#### `widgets.getByTag()`
The `getByTag` method is used to look up a specific `Widget` based on its `tag`.
* **Argument:** tag (String)
* **Returns:** `Widget` or *undefined*
`getByTag( tag )` must run these steps:
1. If the argument tag is omitted, return a Promise rejected with a TypeError.
1. If the argument tag is not a String, return a Promise rejected with a TypeError.
1. Let promise be a new Promise.
1. Let options be an new Object.
1. Set options["tag"] be the value of tag.
1. Run these substeps in parallel:
1. Let search be the result of running the algorithm specified in [matchAll(options)](#widgetsmatchall) with options.
1. Wait until search settles.
1. If search rejects with an exception, then:
1. Reject promise with that exception.
1. Else if search resolves with an array, matches, then:
1. If matches is an empty array, then:
1. Resolve promise with *undefined*.
1. Else:
1. Resolve promise with the first element of matches.
1. Return promise.
#### `widgets.getByInstanceId()`
The `getByInstanceId` method is used to look up a specific `Widget` based on the existence of a `WidgetInstance` object whose `id` matches id.
* **Argument:** id (String)
* **Returns:** `Widget` or *undefined*
`getByInstanceId( id )` must run these steps:
1. If the argument id is omitted, return a Promise rejected with a TypeError.
1. If the argument id is not a String, return a Promise rejected with a TypeError.
1. Let promise be a new Promise.
1. Let options be an new Object.
1. Set options["id"] be the value of id.
1. Run these substeps in parallel:
1. Let search be the result of running the algorithm specified in [matchAll(options)](#widgetsmatchall) with options.
1. Wait until search settles.
1. If search rejects with an exception, then:
1. Reject promise with that exception.
1. Else if search resolves with an array, matches, then:
1. If matches is an empty array, then:
1. Resolve promise with *undefined*.
1. Else:
1. Resolve promise with the first element of matches.
1. Return promise.
#### `widgets.getByHostId()`
The `getByHostId` method is used to look up all `Widget`s that have a `WidgetInstance` whose `host` matches id.
* **Argument:** id (String)
* **Returns:** Array of zero or more `Widget` objects
`getByHostId( id )` must run these steps:
1. If the argument id is omitted, return a Promise rejected with a TypeError.
1. If the argument id is not a String, return a Promise rejected with a TypeError.
1. Let promise be a new Promise.
1. Let options be an new Object.
1. Set options["host"] be the value of id.
1. Run these substeps in parallel:
1. Let search be the result of running the algorithm specified in [matchAll(options)](#widgetsmatchall) with options.
1. Wait until search settles.
1. If search rejects with an exception, then reject promise with that exception.
1. Let matches be the resolution of search.
1. Resolve promise with matches.
1. Return promise.
#### `widgets.matchAll()`
The `matchAll` method is used to find up one or more `Widget`s based on options criteria. The `matchAll` method is analogous to `clients.matchAll()`. It allows developers to limit the scope of matches based on any of the following:
* `tag: "tag_name"` - Only matches a Widget whose `tag` matches `tag` value.
* `instance: "id"` - Only matches a Widget that has a Widget Instance whose `id` matches the `instance` value.
* `host: "id"` - Only matches Widgets that have a Widget Instance whose `host` matches the `host` value.
* `installable: true` - Only matches Widgets supported by a [Widget Host](#dfn-widget-host) on this device.
* `installed: true` - Only matches Widgets that are currently installed on this device (determined by looking for 1+ members of each Widget’s `instances` array).
* **Argument:** options (Object)
* **Returns:** Array of zero or more `Widget` objects
`matchAll( options )` method must run these steps:
1. Let promise be a new Promise.
1. Run the following steps in parallel:
1. Let matchedWidgets be a new list.
1. For each service worker `Widget` widget:
1. If options["installable"] is defined and its value does not match widget["installable"], continue.
1. If options["installed"] is defined:
1. Let instanceCount be the number of items in widget["instances"].
1. If options["installed"] is `true` and instanceCount is 0, continue.
1. If options["installed"] is `false` and instanceCount is greater than 0, continue.
1. If options["tag"] is defined and its value does not match widget["tag"], continue.
1. Let matchingInstance be null.
1. For each instance in widget["instances"]:
1. If options["instance"] is defined:
1. If instance["id"] is equal to options["instance"]
1. Set matchingInstance to instance and exit the loop.
1. If options["host"] is defined:
1. If instance["host"] is equal to options["host"]
1. Set matchingInstance to instance and exit the loop.
1. If matchingInstance is null, continue.
1. If matchingInstance is null, continue.
1. Add widget to matchedWidgets.
1. Resolve promise with a new frozen array of matchedWidgets.
1. Return promise.
### Working with Widgets
The `Widgets` interface enables developers to work with individual widget instances or all instances of a widget.
#### The `WidgetPayload` Object
In order to create or update a widget instance, the Service Worker must send the data necessary to render that widget. This data is called a payload and includes both template- and content-related data. The members of a `WidgetPayload` are:
* `definition` - Object. A `WidgetDefinition` for the Widget. This could be the raw definition from the `Widgets` interface or a constructed/modified version of that `WidgetDefinition`.
* `data` - String. The data to flow into the Widget template. If a developer wants to route JSON data into the Widget, they will need to `stringify()` it first.
* `settings` - Object. The settings for the widget instance (if any).
Before sending a `WidgetPayload` to the Widget Service, the `WidgetDefinition` will be modified to include several members of the Web App Manifest applicable to the web app. The steps for injecting manifest members into a `WidgetPayload` with payload and Web App Manifest manifest are as follows:
1. If payload is not an `WidgetPayload`, then throw an Error.
1. If payload["definition"] is not a `WidgetDefinition`, then throw an Error.
1. If manifest["name"], set payload["definition"]["app_name"] to manifest["name"].
1. If manifest["short_name"], set payload["definition"]["app_short_name"] to manifest["short_name"].
1. If manifest["icons"], set payload["definition"]["app_icons"] to manifest["icons"].
1. If manifest["theme_color"], set payload["definition"]["theme_color"] to manifest["theme_color"].
1. If manifest["background_color"], set payload["definition"]["background_color"] to manifest["background_color"].
1. Return payload.
#### Widget Errors
Some APIs may return an Error when the widget cannot be created, updated, or removed. These Errors should have descriptive strings like:
* "Widget Host not found"
* "Widget template not supported"
* "Widget instance not found"
* "Data required by the template was not supplied."
#### `widgets.updateByInstanceId()`
Developers will use `updateByInstanceId()` to push data to a new or existing [Widget Instance](#dfn-widget-instance). This method will resolve with *undefined* if successful, but should throw [a descriptive Error](#widget-errors) if one is encountered.
* **Arguments:** instanceId (String) and payload ([`WidgetPayload` Object](#widget-payload))
* **Returns:** *undefined*
`updateByInstanceId( instanceId, payload )` method must run these steps:
1. Let promise be a new promise.
1. If instanceId is null or not a String or payload is null or not an Object or this’s active worker is null, then reject promise with a TypeError and return promise.
1. Let widget be the result of running the algorithm specified in [getByInstanceId(instanceId)](#widgetsgetbyinstanceid) with instanceId.
1. Let widgetInstance be null.
1. For i in widget["instances"]:
1. If i["id"] is equal to instanceId
1. Set widgetInstance to i and exit the loop.
1. Else continue.
1. If widgetInstance is null, reject promise with an Error and return promise.
1. Let hostId be widgetInstance["host"].
1. If widgetInstance["settings"] is null or not an Object
1. Set payload["settings"] to the result of [creating a default `WidgetSettings` object](#creating-a-default-widgetsettings-object) with widget.
1. Else
1. Set payload["settings"] to widgetInstance["settings"].
1. Set payload to the result of [injecting manifest members into a `WidgetPayload`](#injecting-manifest-members-into-a-payload) with payload.
1. Let operation be the result of updating the widget instance on the device (e.g., by calling the appropriate [Widget Service](#dfn-widget-service) API) with instanceId and payload.
1. If operation is an Error
1. Reject promise with operation and return promise.
1. Else
1. Let instance be the result of [creating an instance](#creating-a-widget-instance) with instanceId, hostId, payload.
1. Set widgetInstance to instance.
1. Resolve promise.
1. Return promise.
#### `widgets.updateByTag()`
Developers will use `updateByTag()` to push data to all [Instances](#dfn-widget-instance) of a Widget. This method will resolve with *undefined* if successful, but should throw [a descriptive Error](#widget-errors) if one is encountered.
* **Arguments:** tag (String) and payload ([`WidgetPayload` Object](#widget-payload))
* **Returns:** *undefined*
`updateByTag( tag, payload )` method must run these steps:
1. Let promise be a new promise.
1. If tag is null or not a String or payload is not a `WidgetPayload` or this’s active worker is null, then reject promise with a TypeError and return promise.
1. Let widget be the result of running the algorithm specified in [getByTag(tag)](#widgetsgetbytag) with tag.
1. Set payload["settings"] to the result of [creating a default `WidgetSettings` object](creating-a-default-widgetsettings-object) with widget.
1. Set payload to the result of [injecting manifest members into a `WidgetPayload`](#injecting-manifest-members-into-a-payload) with payload.
1. Let instanceCount be the length of widget["instances"].
1. Let instancesUpdated be 0.
1. For each widgetInstance in widget["instances"]
1. Run the following steps in parallel:
1. Let operation be the result of updating the widget instance on the device (e.g., by calling the appropriate [Widget Service](#dfn-widget-service) API) with widgetInstance["id"] and payload.
1. If operation is an Error
1. Reject promise with operation and return promise.
1. Else
1. Let instance be the result of [creating an instance](#creating-a-widget-instance) with widgetInstance["id"], widgetInstance["host"], and payload.
1. Set widgetInstance to instance.
1. Increment instancesUpdated.
1. If instancesUpdated is not equal to instanceCount, then reject promise with an Error and return promise.
1. Resolve and return promise.
#### `widgets.removeByInstanceId()`
Developers will use `removeByInstanceId()` to remove an existing Widget Instance from its Host. This method will resolve with *undefined* if successful, but should throw [a descriptive Error](#widget-errors) if one is encountered.
* **Arguments:** instanceId (String)
* **Returns:** *undefined*
`removeByInstanceId( instanceId )` method must run these steps:
1. Let promise be a new promise.
1. If instanceId is null or not a String or this’s active worker is null, then reject promise with a TypeError and return promise.
1. Let operation be the result of removing the widget instance on the device (e.g., by calling the appropriate [Widget Service](#dfn-widget-service) API) with instanceId.
1. If operation is an Error
1. Reject promise with operation and return promise.
1. Else
1. Let removed be false.
1. Let widget be the result of running the algorithm specified in [getByInstanceId(instanceId)](#widgetsgetbyinstanceid) with instanceId.
1. For each instance in widget["instances"]
1. If instance["id"] is equal to instanceId
1. Remove instance from widget["instances"]
1. Set removed to true.
1. Exit the loop.
1. Else
1. Continue.
1. If removed is false, then reject promise with an Error and return promise.
1. Resolve and return promise.
#### `widgets.removeByTag()`
Developers will use `removeByTag()` to remove all Instances of a Widget. This method will resolve with *undefined* if successful, but should throw [a descriptive Error](#widget-errors) if one is encountered.
* **Arguments:** tag (String)
* **Returns:** *undefined*
`removeByTag( tag )` method must run these steps:
1. Let promise be a new promise.
1. If tag is null or not a String or this’s active worker is null, then reject promise with a TypeError and return promise.
1. Let widget be the result of running the algorithm specified in [getByTag(tag)](#widgetsgetbytag) with tag.
1. Let instanceCount be the length of widget["instances"].
1. Let instancesRemoved be 0.
1. For each instance in widget["instances"]
1. Run the following steps in parallel:
1. Let operation be the result of removing the widget instance on the device (e.g., by calling the appropriate [Widget Service](#dfn-widget-service) API) with instance["id"].
1. If operation is an Error
1. Reject promise with operation and return promise.
1. Else
1. Remove instance from widget["instances"]
1. Increment instancesRemoved.
1. If instancesRemoved is not equal to instanceCount, then reject promise with an Error and return promise.
1. Resolve and return promise.
## 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.
A [`WidgetEvent`](#widget-related-events) is an object with the following properties:
* `host` - This is the GUID for the [Widget Host](#dfn-widget-host) (and is used for internal bookkeeping, such as which host is requesting install/uninstall).
* `instance` - This is the GUID for the [Widget Instance](#dfn-widget-instance).
* `tag` - This is the `tag` for the Widget.
* `action` - 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` - 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).
```js
{
"host": {{ GUID }},
"instance": {{ GUID }},
"tag": "agenda",
"action": "create-event",
"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 [`WidgetEvent`](#widget-related-events) could be handled:
```js
self.addEventListener('widgetclick', function(event) {
const action = event.action;
// If user is being prompted to login
if ( action == "login" ) {
// open a new window to the login page
clients.openWindow( "/login?from=widget" );
}
});
```
There are a few special [`WidgetEvent`](#widget-related-events) `action` types to consider as well.
* "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.
The steps for creating a WidgetEvent with Widget Service Message message are as follows:
1. Let event be a new ExtendableEvent.
1. Run the following steps in parallel:
1. Set event["data"] to a new object.
1. Set event["host"] to the id of the Widget Host bound to message.
1. If message is a request to refresh all widgets
1. Set event["action"] to "WidgetResume".
1. Return event.
1. Else if message is a request to install a widget, set event["action"] to "WidgetInstall".
1. Else if message is a request to uninstall a widget, set event["action"] to "WidgetUninstall".
1. Else if message is a request to update a widget’s settings, set event["action"] to "WidgetSave".
1. Else set event["action"] to the user action bound to message.
1. Let instanceId be the id of the Widget Instance bound to message.
1. Set event["instance"] to instanceId.
1. Let widget be the result of running the algorithm specified in [getByInstanceId(instanceId)](#widgetsgetbyinstanceid) with instanceId.
1. Set event["tag"] to widget["tag"].
1. If message includes bound data,
1. Set event["data"] to the data value bound to message.
1. Return event
### widget-install
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.
Required `WidgetEvent` data:
* `host`
* `instance`
* `tag`
The steps for creating a placeholder instance with WidgetEvent event:
1. Let tag be event["widget"]["tag"].
1. Let widget be the result of running the algorithm specified in [getByTag(tag)](#widgetsgetbytag) with tag.
1. If widget is undefined, exit.
1. Let payload be an object.
1. Set payload["data"] to an empty JSON object.
1. Set payload["settings"] to the result of [creating a default `WidgetSettings` object](#creating-a-default-widgetsettings-object) with widget.
1. Set payload["definition"] to the value of widget["definition"].
1. Set payload to the result of [injecting manifest members into a `WidgetPayload`](#injecting-manifest-members-into-a-payload) with payload.
1. Let instance be the result of [creating an instance](#creating-a-widget-instance) with event["widget"]["id"], event["widget"]["host"], and payload.
1. Append instance to widget["instances"].
Here is the flow for install:
![](media/install.gif)
1. A "WidgetInstall" signal is received by the User Agent, the placeholder instance is created, and the event is passed along to the Service Worker.
2. The Service Worker
a. captures the Widget Instance `id` from the `widget` property,
b. looks up the Widget via `widgets.getByInstanceId()`, and
c. makes a `Request` for its `data` endpoint.
3. The Service Worker then combines the `Response` with the Widget definition and passes that along to the [Widget Service](#dfn-widget-service) via the `updateByInstanceId()` method.
### widget-uninstall
Required `WidgetEvent` data:
* `host`
* `instance`
* `tag`
The "uninstall" process is similar:
![](media/uninstall.gif)
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
Required `WidgetEvent` data:
* `host`
* `instance`
* `tag`
* `data`
The "WidgetSave" process works like this:
1. The "WidgetSave" signal is received by the User Agent.
2. Internally, the `WidgetInstance` matching the `widget.id` value is examined to see if
a. it has settings and
b. its `settings` object matches the inbound `data`.
3. 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.
4. The Service Worker receives the event and can react by issuing a request for new data, based on the updated settings values.
### widget-resume
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:
* `host`
Using this event, it is expected that the Service Worker will enumerate the Widget Instances associated with the `host` and Fetch new data for each.
![](media/resume.gif)
## Example
Here is how this could come together in a Service Worker:
```js
const periodicSync = self.registration.periodicSync;
async function updateWidget( widget ){
// Widgets with settings should be updated on a per-instance level
if ( widget.hasSettings ) {
widget.instances.map(async (instance) => {
let settings_data = new FormData();
for ( let key in instance.settings ) {
settings_data.append(key, instance.settings[key]);
}
fetch( widget.data, {
method: "POST",
body: settings_data
})
.then( response => {
let payload = {
definition: widget.definition,
data: response.body
};
widgets.updateByInstanceId( instance.id, payload );
});
});
// other widgets can be updated en masse via their tags
} else {
fetch( widget.data )
.then( response => {
let payload = {
definition: widget.definition,
data: response.body
};
widgets.updateByTag( widget.tag, payload );
});
}
}
self.addEventListener("widgetclick", function(event) {
const action = event.action;
const host_id = event.host;
const tag = event.tag;
const instance_id = event.instance;
// If a widget is being installed
switch (action) {
case "widget-install":
console.log("installing", widget, instance_id);
event.waitUntil(
// find the widget
widgets.getByTag( tag )
.then( widget => {
// get the data needed
fetch( widget.data )
.then( response => {
let payload = {
definition: widget.definition,
data: response.body
};
// show the widget, passing in
// the widget definition and data
widgets
.updateByInstanceId( instance_id, payload )
.then(()=>{
// if the widget is set up to auto-update…
if ( "update" in widget.definition ) {
let tags = await registration.periodicSync.getTags();
// only one registration per tag
if ( ! tags.includes( tag ) ) {
periodicSync.register( tag, {
minInterval: widget.definition.update
});
}
}
});
})
})
);
break;
case "widget-uninstall":
event.waitUntil(
// find the widget
widgets.getByInstanceId( instance_id )
.then( widget => {
console.log("uninstalling", widget.definition.name, "instance", instance_id);
// clean up periodic sync?
if ( widget.instances.length === 1 && "update" in widget.definition )
{
await periodicSync.unregister( tag );
}
widgets.removeByInstanceId( instance_id );
})
);
break;
case "widget-resume":
console.log("resuming all widgets");
event.waitUntil(
// refresh the data on each widget (using Clients, just to show it can be done)
widgets.getByHostId(host_id)
.then(function(widgetList) {
for (let i = 0; i < widgetList.length; i++) {
updateWidget( widgetList[i] );
}
})
);
break;
// other cases
}
});
self.addEventListener("periodicsync", event => {
const tag = event.tag;
const widget = widgets.getByTag( tag );
if ( widget && "update" in widget.definition ) {
event.waitUntil( updateWidget( widget ) );
}
// Other logic for different tags as needed.
});
```