Skip to content

Commit

Permalink
feat: 🎸 prompt on leave
Browse files Browse the repository at this point in the history
prompt on leave feature

✅ Closes: COMUI-1289
  • Loading branch information
Gavin Everett authored and Gavin Everett committed Mar 1, 2023
1 parent b40fff4 commit 270b039
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 14 deletions.
27 changes: 23 additions & 4 deletions apps/ifc-example-client/client-app-1/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ <h3>Change Route</h3>
</li>
<!-- internal routes URLs get remapped in js by `transformLinks` -->
<li>
<a class="client-transform-link" href="#/route1">This client: /route1</a>
<a class="client-transform-link" href="#/route1"
>This client: /route1</a
>
</li>
<li>
<a class="client-transform-link" href="#/route2">This client: /route2</a>
<a class="client-transform-link" href="#/route2"
>This client: /route2</a
>
</li>
</ul>

Expand All @@ -34,16 +38,31 @@ <h3>
</h3>
<ul>
<li>
<ifc-client-link path="/route2">Creating a full link from the client path using ifc-client-link: /route2</ifc-client-link>
<ifc-client-link path="/route2"
>Creating a full link from the client path using ifc-client-link:
/route2</ifc-client-link
>
</li>
<li>
<ifc-host-link path="/app2">Creating a full link from the host path using ifc-host-link: /app2</ifc-host-link>
<ifc-host-link path="/app2"
>Creating a full link from the host path using ifc-host-link:
/app2</ifc-host-link
>
</li>
</ul>

<h2>Toasts</h2>
<button class="toast" data-component-name="App #1">Send Toast</button>

<h2>Prompt on Leave</h2>
<button class="prompt" data-component-name="App #1">
Request prompt on leave
</button>

<button class="clearPrompt" data-component-name="App #1">
Clear prompt on leave
</button>

<h2>Pub-Sub</h2>
<div id="pub-sub">
<h3>Outbound</h3>
Expand Down
25 changes: 21 additions & 4 deletions apps/ifc-example-client/client-app-2/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ <h3>Change Route</h3>
</li>
<!-- internal routes URLs get remapped in js by `transformLinks` -->
<li>
<a class="client-transform-link" href="#/route1">This client: /route1</a>
<a class="client-transform-link" href="#/route1"
>This client: /route1</a
>
</li>
<li>
<a class="client-transform-link" href="#/route2">This client: /route2</a>
<a class="client-transform-link" href="#/route2"
>This client: /route2</a
>
</li>
</ul>

Expand All @@ -61,16 +65,29 @@ <h3>
</h3>
<ul>
<li>
<ifc-client-link path="/route2">Creating a full link from the client path: /route2</ifc-client-link>
<ifc-client-link path="/route2"
>Creating a full link from the client path: /route2</ifc-client-link
>
</li>
<li>
<ifc-host-link path="/app1">Creating a full link from the host path: /app1</ifc-host-link>
<ifc-host-link path="/app1"
>Creating a full link from the host path: /app1</ifc-host-link
>
</li>
</ul>

<h2>Toasts</h2>
<button class="toast" data-component-name="App #2">Send Toast</button>

<h2>Prompt on Leave</h2>
<button class="prompt" data-component-name="App #2">
Request prompt on leave
</button>

<button class="clearPrompt" data-component-name="App #2">
Clear prompt on leave
</button>

<h2>Pub-Sub</h2>
<div id="pub-sub">
<h3>Outbound</h3>
Expand Down
16 changes: 14 additions & 2 deletions apps/ifc-example-client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ registerCustomElements();

document.getElementById('path').innerHTML = window.location.hash;

window.onhashchange = function() {
window.onhashchange = function () {
document.getElementById('path').innerHTML = window.location.hash;
document.getElementById(
'urlFromClientPath'
Expand Down Expand Up @@ -96,10 +96,22 @@ document.addEventListener('DOMContentLoaded', () => {
level: TOAST_LEVELS[Math.round(Math.random() * 2)]
}
};

// Ask the host app to show the toast.
iframeClient.requestNotification(toast);
});

// Ask host app to display prompt on leave request.
let promptBtnEl = document.querySelector('button.prompt');
promptBtnEl.addEventListener('click', () => {
iframeClient.requestPromptOnLeave();
})

//Ask host app to clear prompt on leave request.
let clearPromptBtnEl = document.querySelector('button.clearPrompt');
clearPromptBtnEl.addEventListener('click', () => {
iframeClient.clearPromptOnLeave();
})

});

// HELPER FUNCTIONS
Expand Down
13 changes: 13 additions & 0 deletions packages/iframe-coordinator-cli/src/views/IframeEmbed.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
v-on:notifyRequest="displayToast"
v-on:registeredKeyFired="handleKeyEvent"
v-on:navRequest="handleNav"
v-on:promptOnLeave="handlePromptOnLeave"
v-on:frameTransition="updateFrameUrl"
v-on:pageMetadata="updatePageMetadata"
></frame-router>
Expand Down Expand Up @@ -109,6 +110,18 @@ export default {
this.metadata = {
title: event.detail.title,
breadcrumbs: event.detail.breadcrumbs
};
},
handlePromptOnLeave(event) {
if (event.detail.shouldPrompt === true) {
window.onbeforeunload = function(event) {
event.preventDefault();
//This is needed for compatibility with Google Chrome.
event.returnValue = '';
};
}
if (event.detail.shouldPrompt === false) {
window.onbeforeunload = null;
}
}
},
Expand Down
24 changes: 23 additions & 1 deletion packages/iframe-coordinator/doc/client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,20 @@ let externalLink = `<a href="https://external-site.com/external/path" target="_t
There are common requests a client application will want to make of a host application and we strive
to provide nice default APIs for these.

Currently there are two implemented:
Currently there are four implemented:

[`requestNotification`](../classes/client.client-1.html#requestnotification), which asks the host
app to send a notification message to the user.

[`requestModal`](../classes/client.client-1.html#requestModal), which asks the host
app to launch a modal identified by a given ID, also accepts initial setup data specific to that modal.

[`requestPromptOnLeave`](../classes/client.client-1.html#requestPromptOnLeave), which asks the host
app to display a prompt on leave dialog to the user before navigating.

[`clearPromptOnLeave`](../classes/client.client-1.html#clearPromptOnLeave), which asks the host
app to clear the prompt on leave dialog before navigating.

A client application may request a modal on the host like so:

```typescript
Expand All @@ -130,6 +136,22 @@ ifcClient.requestModal({
The frame-router element will emit a custom event of 'modalRequest' with the ModalRequest object in the detail property.
A client application may request a prompt on leave dialog on the host like so:
```typescript
ifcClient.requestPromptOnLeave();
```
The frame-router element will emit a custom event of 'promptOnLeave' with the PromptOnLeave object in the detail property.
A client application may request to clear the prompt on leave dialog on the host like so:
```typescript
ifcClient.clearPromptOnLeave();
```
The frame-router element will emit a custom event 'promptOnLeave' with the PromptOnLeave object in the detail property.
## Custom Client/Host Messaging
Client and Host applications may also need to communicate about topics specific to their use case.
Expand Down
24 changes: 23 additions & 1 deletion packages/iframe-coordinator/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ModalRequest } from './messages/ModalRequest';
import { NavRequest } from './messages/NavRequest';
import { Notification } from './messages/Notification';
import { PageMetadata } from './messages/PageMetadata';
import { PromptOnLeave } from './messages/PromptOnLeave';
import { Publication } from './messages/Publication';

// Re-exports for doc visibility
Expand All @@ -42,7 +43,8 @@ export {
EnvDataHandler,
ModalRequest,
NavRequest,
Notification
Notification,
PromptOnLeave
};

/**
Expand Down Expand Up @@ -499,6 +501,26 @@ bad input into one of the iframe-coordinator client methods.
});
}

/**
* Asks the host application to display a prompt on leave dialog.
*/
public requestPromptOnLeave(messagePrompt?: string): void {
this._sendToHost({
msgType: 'promptOnLeave',
msg: { shouldPrompt: true, message: messagePrompt }
});
}

/**
* Asks the host application to clear the prompt on leave dialog.
*/
public clearPromptOnLeave(): void {
this._sendToHost({
msgType: 'promptOnLeave',
msg: { shouldPrompt: false }
});
}

/**
* Sends page metadata to host for display and browser settings
*
Expand Down
7 changes: 5 additions & 2 deletions packages/iframe-coordinator/src/messages/ClientToHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
decoder as pageMetadataDecoder,
LabeledPageMetadata
} from './PageMetadata';
import { decoder as promptDecoder, LabeledPrompt } from './PromptOnLeave';
import {
decoder as publicationDecoder,
LabeledPublication
Expand All @@ -26,7 +27,8 @@ export type ClientToHost =
| LabeledStarted
| LabeledKeyDown
| LabeledModalRequest
| LabeledPageMetadata;
| LabeledPageMetadata
| LabeledPrompt;

/**
* Validates correctness of messages being sent from
Expand All @@ -44,7 +46,8 @@ export function validate(msg: any): ClientToHost {
notifyRequest: notifyDecoder,
toastRequest: notifyDecoder,
modalRequest: modalDecoder,
pageMetadata: pageMetadataDecoder
pageMetadata: pageMetadataDecoder,
promptOnLeave: promptDecoder
})
)(msg);
}
36 changes: 36 additions & 0 deletions packages/iframe-coordinator/src/messages/PromptOnLeave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { boolean, constant, Decoder, object, optional, string } from 'decoders';
import { labeledDecoder, LabeledMsg } from './LabeledMsg';

/** A prompt on leave dialog to be displayed
* by the host application. If the host application receives a message with the shouldPrompt field set to true
* a dialog will be displayed asking the user for confirmation before navigating.
*/
export interface PromptOnLeave {
/** The host application will ask the user for confirmation before
* leaving the current page if it has received a message with the shouldPrompt field set to true.
*/
shouldPrompt: boolean;
/** Optional message to prompt the user with. */
message?: string;
}

/**
* A message used to request a prompt on leave dialog to be displayed in the host app.
*/
export interface LabeledPrompt
extends LabeledMsg<'promptOnLeave', PromptOnLeave> {
/** Message identifier */
msgType: 'promptOnLeave';
/** Message details */
msg: PromptOnLeave;
}

const decoder: Decoder<LabeledPrompt> = labeledDecoder(
constant<'promptOnLeave'>('promptOnLeave'),
object({
shouldPrompt: boolean,
message: optional(string)
})
);

export { decoder };
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ClientToHost, validate } from '../ClientToHost';
import { LabeledNavRequest } from '../NavRequest';
import { LabeledNotification } from '../Notification';
import { LabeledPrompt } from '../PromptOnLeave';
import { LabeledPublication } from '../Publication';

describe('ClientToHost', () => {
Expand Down Expand Up @@ -334,4 +335,66 @@ describe('ClientToHost', () => {
});
});
});

describe('validating promptOnLeave type', () => {
describe('when only the shouldPrompt field is provided', () => {
const testMessage = {
msgType: 'promptOnLeave',
msg: {
shouldPrompt: true,
message: undefined
}
};

const expectedMessage: ClientToHost = {
msgType: 'promptOnLeave',
msg: {
shouldPrompt: true,
message: undefined
},
protocol: 'iframe-coordinator',
version: 'unknown',
direction: undefined
};

let testResult: ClientToHost;
beforeEach(() => {
testResult = validate(testMessage);
});

it('should return the validated message', () => {
expect(testResult).toEqual(expectedMessage);
});
});

describe('when both the shouldPrompt and message field are provided', () => {
const testMessage = {
msgType: 'promptOnLeave',
msg: {
shouldPrompt: true,
message: 'This is a prompt message'
}
};

const expectedMessage: ClientToHost = {
msgType: 'promptOnLeave',
msg: {
shouldPrompt: true,
message: 'This is a prompt message'
},
protocol: 'iframe-coordinator',
version: 'unknown',
direction: undefined
};

let testResult: ClientToHost;
beforeEach(() => {
testResult = validate(testMessage);
});

it('should return the validated message', () => {
expect(testResult).toEqual(expectedMessage);
});
});
});
});

0 comments on commit 270b039

Please sign in to comment.