Microsoft Edge Origin Trials Developer Console
Web App Widgets
Enable installed web applications to create Widgets for Windows 11 Widget Dashboard
Available from Microsoft Edge 108 to 114
End Date 06/01/2023
Resources
request to refresh all widgets 1. Set event["action"] to "widget-resume". 1. Set event["hostId"] to the id of the Widget Host bound to message. 1. Return event.
- Else if message is a request to install a widget, set event["action"] to "widget-install".
- Else if message is a request to uninstall a widget, set event["action"] to "widget-uninstall".
- Else if message is a request to update a widget’s settings, set event["action"] to "widget-save".
- Else set event["action"] to the user action bound to message.
- Let instanceId be the id of the Widget Instance bound to message.
- Set event["instanceId"] to instanceId.
- Let widget be the result of running the algorithm specified in getByInstanceId(instanceId) with instanceId.
- Set event["widget"] to widget.
- If message includes bound data,
- Set event["data"] to the data value bound to message.
- 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:
instanceId
widget
The steps for creating a placeholder instance with WidgetEvent
event:
- Let widget be event["widget"].
- If widget is undefined, exit.
- Let payload be an object.
- Set payload["data"] to an empty JSON object.
- Set payload["settings"] to the result of creating a default
WidgetSettings
object with widget. - Let instance be the result of creating an instance with event["instanceId"], event["hostId"], and payload.
- Append instance to widget["instances"].
Here is the flow for install:
- A "widget-install" signal is received by the User Agent, the placeholder instance is created, and the event is passed along to the Service Worker.
- The Service Worker makes a
Request
for thewidget.data
endpoint. - The Service Worker then creates a payload and passes that along to the Widget Service via the
updateByInstanceId()
method.
widget-uninstall
Required WidgetEvent
data:
instanceId
widget
The "uninstall" process is similar:
- The "widget-uninstall" signal is received by the User Agent and is passed to the Service Worker.
- The Service Worker runs any necessary cleanup steps (such as un-registering a Periodic Sync if the widget is no longer in use).
- 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 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:
instanceId
widget
data
The "widget-save" process works like this:
- The "widget-save" signal is received by the User Agent.
- Internally, the
WidgetInstance
matching theinstanceId
value is examined to see if a. it has settings and a. itssettings
object matches the inbounddata
. - If it has settings and the two do not match, the new
data
is saved tosettings
in theWidgetInstance
and the "widget-save" event issued to the Service Worker. - 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 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 will issue a "widget-resume" event.
Required WidgetEvent
data:
hostId
Using this event, it is expected that the Service Worker will enumerate the Widget Instances associated with the hostId
and Fetch new data for each.
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:
Server Push
Many developers are already familiar with Push Notifications as a means of notifying users of timely updates and information. Widgets offer an alternative means of informing users without interrupting them with a notification bubble.
In order to use the Push API, a user must grant the developer the necessary permission(s). Once granted, however, developers could send widget data as part of any Server Push, either alongside pushes intended as Notifications or ones specifically intended to direct content into a widget.
Periodic Sync
The Periodic Sync API enables developers to wake up their Service Worker to synchronize data with the server. This Service Worker-directed event could be used to gather updates for any Widget Instances.
Caveats:
- This API is currently only supported in Chromium browsers.
- Sync frequency is currently governed by site engagement metrics and is capped at 2× per day (once every 12 hours). We are investigating whether the frequency could be increased for PWAs with active widgets.
Server-sent Events
Server-sent Events are similar to a web socket, but only operate in one direction: from the server to a client. The EventSource
interface is available within worker threads (including Service Workers) and can be used to "listen" for server-sent updates.
Example
Here is how this could come together in a Service Worker:
const periodicSync = self.registration.periodicSync;
async function registerPeriodicSync( widget )
{
// if the widget is set up to auto-update…
if ( "update" in widget.definition ) {
registration.periodicSync.getTags()
.then( tags => {
// only one registration per tag
if ( ! tags.includes( widget.definition.tag ) ) {
periodicSync.register( widget.definition.tag, {
minInterval: widget.definition.update
});
}
});
}
return;
}
async function unregisterPeriodicSync( widget )
{
// clean up periodic sync?
if ( widget.instances.length === 1 &&
"update" in widget.definition )
{
periodicSync.unregister( widget.definition.tag );
}
return;
}
async function updateWidgets( host_id )
{
const config = host_id ? { hostId: host_id }
: { installed: true };
let queue = [];
await widgets.matchAll( config )
.then(async widgetList => {
for (let i = 0; i < widgetList.length; i++) {
queue.push(updateWidget( widgetList[i] ));
}
});
await Promise.all(queue);
return;
}
async function updateWidget( widget ){
// Widgets with settings should be updated on a per-instance level
if ( widget.hasSettings )
{
let queue = [];
widget.instances.map( async (instance) => {
queue.push(updateInstance( instance, widget ));
});
await Promise.all(queue);
return;
}
// other widgets can be updated en masse via their tags
else
{
let opts = { headers: {} };
if ( "type" in widget.definition )
{
opts.headers.accept = widget.definition.type;
}
await fetch( widget.definition.data, opts )
.then( response => response.text() )
.then( data => {
let payload = {
template: widget.definition.template,
data: data
};
widgets.updateByTag( widget.definition.tag, payload );
});
return;
}
}
async function createInstance( instance_id, widget )
{
await updateInstance( instance_id, widget )
.then(() => {
registerPeriodicSync( widget );
});
return;
}
async function updateInstance( instance, widget )
{
// If we only get an instance id, get the instance itself
if ( typeof instance === "string" ) {
let instance_id = instance;
instance = widget.instances.find( i => i.id === instance );
if ( instance ) {
instance = { id: instance_id };
widget.instances.push( instance );
}
}
if ( typeof instance !== "object" )
{
return;
}
if ( !instance.settings ) {
instance.settings = {};
}
let settings_data = new FormData();
for ( let key in instance.settings ) {
settings_data.append(key, instance.settings[key]);
}
let opts = {};
if ( settings_data.length > 0 )
{
opts = {
method: "POST",
body: settings_data,
headers: {
contentType: "multipart/form-data"
}
};
}
if ( "type" in widget.definition )
{
opts.headers.accept = widget.definition.type;
}
await fetch( widget.definition.data, opts )
.then( response => response.text() )
.then( data => {
let payload = {
template: widget.definition.template,
data: data,
settings: instance.settings
};
widgets.updateByInstanceId( instance.id, payload );
});
return;
}
async function removeInstance( instance_id, widget )
{
console.log( `uninstalling ${widget.definition.name} instance ${instance_id}` );
unregisterPeriodicSync( widget )
.then(() => {
widgets.removeByInstanceId( instance_id );
});
return;
}
self.addEventListener("widgetclick", function(event) {
const action = event.action;
const host_id = event.hostId;
const widget = event.widget;
const instance_id = event.instanceId;
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;
// 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.
});
Open Questions
- Could the Periodic Sync frequency be increased for a domain when there are active widgets?
- Assuming a Push could carry a payload to update a widget, would the widget displaying the new content fulfill the requirement that the user be shown something when the push arrives or would we still need to trigger a notification?