Skip to content

Commit

Permalink
feat(ifc-api-improvements): Improve URL and link generation APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
katie-bobbe-genesys committed Jun 1, 2021
1 parent eda9b01 commit 6f77f9d
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 39 deletions.
2 changes: 1 addition & 1 deletion client-app-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ commands from the iframe-coordinator project root (the parent directory of this

`npm install`

`npm build`
`npm run build`

`npm run start-client-example`

Expand Down
26 changes: 21 additions & 5 deletions client-app-example/public/client-app-1/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,37 @@ <h1>Application #1</h1>
<h2>Routing</h2>
<h3>Current Local Route:</h3>
<div id="path"></div>
<h3>Current Host Route:</h3>
<div id="hostPath"></div>
<h3>Current Host Route Calculated From Client:</h3>
<div id="urlFromClientPath"></div>

<h3>Change Route</h3>
<ul>
<!-- internal routes URLs get remapped in js by `transformLinks` -->
<li><a href="#/route1">This client: /route1</a></li>
<li><a href="#/route2">This client: /route2</a></li>
<!-- External URLs use full links so they open nicely in new tabs -->
<li>
<a href="//localhost:3000/#/app2/route1">Other client: /route1</a>
</li>
<li>
<a href="//localhost:3000/#/app2/route2">Other client: /route2</a>
</li>
<!-- internal routes URLs get remapped in js by `transformLinks` -->
<li>
<a class="client-transform-link" href="#/route1">This client: /route1</a>
</li>
<li>
<a class="client-transform-link" href="#/route2">This client: /route2</a>
</li>
</ul>

<h3>
Using custom elements ifc-client-link and ifc-host-link to build full urls
</h3>
<ul>
<li>
<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>
</li>
</ul>

<h2>Toasts</h2>
Expand Down
25 changes: 21 additions & 4 deletions client-app-example/public/client-app-2/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ <h3>Local Media</h3>
<h2>Routing</h2>
<h3>Current Local Route:</h3>
<div id="path"></div>
<div id="path2"></div>
<h3>Current Host Route:</h3>
<div id="hostPath"></div>
<div id="urlFromClientPath"></div>

<h3>Change Route</h3>
<ul>
Expand All @@ -44,11 +45,27 @@ <h3>Change Route</h3>
<a href="//localhost:3000/#/app1/route1">Other client: /route1</a>
</li>
<li>
<a href="//localhost:3000/#/app1/route1">Other client: /route2</a>
<a href="//localhost:3000/#/app1/route2">Other client: /route2</a>
</li>
<!-- internal routes URLs get remapped in js by `transformLinks` -->
<li><a href="#/route1">This client: /route1</a></li>
<li><a href="#/route2">This client: /route2</a></li>
<li>
<a class="client-transform-link" href="#/route1">This client: /route1</a>
</li>
<li>
<a class="client-transform-link" href="#/route2">This client: /route2</a>
</li>
</ul>

<h3>
Using custom elements ifc-client-link and ifc-host-link to build full urls
</h3>
<ul>
<li>
<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>
</li>
</ul>

<h2>Toasts</h2>
Expand Down
16 changes: 12 additions & 4 deletions client-app-example/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
import '@babel/polyfill';
import 'custom-event-polyfill/polyfill.js';
import 'url-polyfill';
import { registerCustomElements } from 'iframe-coordinator/dist/index';

import { Client } from 'iframe-coordinator/dist/client';

registerCustomElements();

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

window.onhashchange = function() {
document.getElementById('path').innerHTML = window.location.hash;
document.getElementById('hostPath').innerHTML = iframeClient.asHostUrl(
document.getElementById('urlFromClientPath').innerHTML = iframeClient.urlFromClientPath(
window.location.hash
);
};
Expand All @@ -23,6 +26,8 @@ let iframeClient = new Client({
hostOrigin: `http://${window.location.hostname}:3000`
});

iframeClient.registerCustomElements();

// Add a listener that will handled config data passed from the host to the
// client at startup.
iframeClient.addListener('environmentalData', envData => {
Expand All @@ -35,7 +40,7 @@ iframeClient.addListener('environmentalData', envData => {
console.log(
`Got locale from host. Current date formatted for ${envData.locale} is: ${localizedDate}`
);
document.getElementById('hostPath').innerHTML = iframeClient.asHostUrl(
document.getElementById('urlFromClientPath').innerHTML = iframeClient.urlFromClientPath(
window.location.hash
);
displayEnvData(envData);
Expand Down Expand Up @@ -90,9 +95,12 @@ document.addEventListener('DOMContentLoaded', () => {

// HELPER FUNCTIONS
function transformLinks() {
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.href = iframeClient.asHostUrl(link.getAttribute('href'));
document.querySelectorAll('a.client-transform-link').forEach(link => {
link.href = iframeClient.urlFromClientPath(link.getAttribute('href'));
});
document.querySelectorAll('a.host-transform-link').forEach(link => {
link.href = iframeClient.urlFromHostPath(link.getAttribute('href'));
})
}

function displayEnvData(envData) {
Expand Down
27 changes: 21 additions & 6 deletions doc/client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ with `target="_top"` set.

Links rendered by the client application should have the `href` attribute set to the full host
application URL rather than a relative path within the client. To make this easier, the client
provides the [`asHostUrl`](../classes/client.client-1.html#ashosturl) method that can translate
client application routes to the corresponding host application URL.
provides the `urlFromClientPath` method that can translate
client application routes to the corresponding host application URL. The client application also provides the `urlFromHostPath` method that can translate a host application relative path to the full URL used in the host application.


### Using Custom Elements to Create Links

To further simplify the generation of these links, there are custom elements that can be used to create links. These elements are `ifc-client-link` and `ifc-host-link`. These custom elements are registered and provided by the client. These custom elements require a `path` attribute with the relative path (either client or host) that will be used to create the full URL. The `ifc-client-link` custom element uses the `urlFromClientPath` method to translate a client application route provided in the `path` attribute to a full URL. The `ifc-host-link` custom element uses the `urlFromHostPath` method to translate a host application route provided in the `path` attribute to a full URL.

### Examples

Expand All @@ -67,10 +72,10 @@ wrap, extend, or avoid built-in navigation and link generation utilities.

```typescript
// Navigate to a new route in the client app: /foo/bar
ifcClient.requestNavigation({ url: ifcClient.asHostUrl('/foo/bar') });
ifcClient.requestNavigation({ url: ifcClient.urlFromClientPath('/foo/bar') });

// Navigate to a host application route
ifcClient.requestNavigation({ url: 'https://host-app.com/host/path' });
ifcClient.requestNavigation({ url: ifcClient.urlFromHostPath('/path') });

// Navigate to a 3rd party url
ifcClient.requestNavigation({ url: 'https://external-site.com/external/path' });
Expand All @@ -79,15 +84,25 @@ ifcClient.requestNavigation({ url: 'https://external-site.com/external/path' });
**Generating Links**

```typescript
let internalLink = `<a href="${ifcClient.asHostUrl(
let internalLink = `<a href="${ifcClient.urlFromClientPath(
'foo/bar'
)}" target="_top">Internal Link</a>`;

let hostLink = `<a href="https://host-app.com/host/path" target="_top">Internal Link</a>`;
let hostLink = `<a href="${ifcClient.urlFromHostPath(
'/path'
)}" target="_top">Internal Link</a>`;

let externalLink = `<a href="https://external-site.com/external/path" target="_top">Internal Link</a>`;
```

**Generating Links Using Custom Elements**

<!-- Creates a link to a new route in the client app: /foo/bar -->
<ifc-client-link path='/foo/bar'>Internal Link</ifc-client-link>

<!-- Creates a link to a host application route: /path -->
<ifc-host-link path='/path'>Internal Link</ifc-host-link>

## Requesting Host Actions

There are common requests a client application will want to make of a host application and we strive
Expand Down
3 changes: 2 additions & 1 deletion src/FrameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const DEFAULT_SANDBOX = [
'allow-modals',
'allow-forms',
'allow-popups',
'allow-downloads'
'allow-downloads',
'allow-top-navigation-by-user-activation'
] as const;

let style: HTMLElement;
Expand Down
71 changes: 67 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

import { joinRoutes, stripLeadingSlashAndHashTag } from '../src/urlUtils';
import IfcClientLinkElement from './elements/ifc-client-link';
import IfcHostLinkElement from './elements/ifc-host-link';
import { EventEmitter, InternalEventEmitter } from './EventEmitter';
import { keyEqual } from './Key';
import {
Expand All @@ -19,13 +21,13 @@ import {
applyClientProtocol,
PartialMsg
} from './messages/LabeledMsg';
import { KeyData } from './messages/Lifecycle';
import {
EnvData,
EnvDataHandler,
LabeledEnvInit,
Lifecycle
} from './messages/Lifecycle';
import { KeyData } from './messages/Lifecycle';
import { NavRequest } from './messages/NavRequest';
import { Notification } from './messages/Notification';
import { Publication } from './messages/Publication';
Expand Down Expand Up @@ -83,6 +85,37 @@ export class Client {
this._assignedRoute = null;
}

/**
* Registers custom elements used by the client application
*/
public registerCustomElements(): void {
const clientInstance = this;
/**
* This class extends IfcClientLinkElement to provide
* the ifc-client-link custom element access to the client instance
*/
// tslint:disable-next-line:max-classes-per-file
class IfcClientLinkElementComplete extends IfcClientLinkElement {
constructor() {
super(clientInstance);
}
}
/**
* This class extends IfcHostLinkElement to provide
* the ifc-host-link custom element access to the client instance
*/
// tslint:disable-next-line:max-classes-per-file
class IfcHostLinkElementComplete extends IfcHostLinkElement {
constructor() {
super(clientInstance);
}
}
clientInstance.addListener('environmentalData', envData => {
customElements.define('ifc-client-link', IfcClientLinkElementComplete);
customElements.define('ifc-host-link', IfcHostLinkElementComplete);
});
}

/**
* Sets up a function that will be called whenever the specified event type is delivered to the target.
* This should not be confused with the general-purpose pub-sub listeners that can be set via the
Expand Down Expand Up @@ -242,14 +275,44 @@ export class Client {
/**
* Translates a client route like `/foo/bar` to the full URL used in the host
* app for the same page, e.g. `https://hostapp.com/#/client-app/foo/bar`.
* You should use this whenver generating an internal link within a client
* You should use this whenever generating an internal link within a client
* application so that the user gets a nice experience if they open a link in
* a new tab, or copy and paste a link URL into a chat message or email.
*
* @param clientRoute The /-separated path within the client app to link to.
*/
public asHostUrl(clientRoute: string): string {
const trimedClientRoute = stripLeadingSlashAndHashTag(clientRoute);
public urlFromClientPath(clientRoute: string): string {
const hostRootUrl = this.environmentData.hostRootUrl;
const assignedRoute = this._assignedRoute || '';
const trimmedClientRoute = stripLeadingSlashAndHashTag(clientRoute);
return joinRoutes(hostRootUrl, assignedRoute, trimmedClientRoute);
}

/**
* Translates a host route like `/app2` to the full URL used in the host
* app, e.g. `https://hostapp.com/#/app2`.
* You should use this whenever generating a host link within a client
* application so that the user gets a nice experience if they open a link in
* a new tab, or copy and paste a link URL into a chat message or email.
*
* @param hostRoute The /-separated path within the host app to link to.
*/
public urlFromHostPath(hostRoute: string): string {
const hostRootUrl = this.environmentData.hostRootUrl;
const trimmedHostRoute = stripLeadingSlashAndHashTag(hostRoute);
return joinRoutes(hostRootUrl, trimmedHostRoute);
}

/**
* Translates a client route like `/foo/bar` to the full URL used in the host
* app for the same page, e.g. `https://hostapp.com/#/client-app/foo/bar`.
*
* @param clientRouteLegacy The /-separated path within the client app to link to.
*
* @deprecated Use the new {@urlFromClientPath} method instead
*/
public asHostUrl(clientRouteLegacy: string): string {
const trimedClientRoute = stripLeadingSlashAndHashTag(clientRouteLegacy);
return joinRoutes(
this.environmentData.hostRootUrl,
this._assignedRoute || '',
Expand Down
53 changes: 53 additions & 0 deletions src/elements/ifc-client-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Client } from '../client';
const PATH_ATTR = 'path';

/**
* A custom element responsible for rendering an anchor element, turning a path
* within the current application to a full top-level url.
* It is registered as `ifc-client-link` within client.ts
*/

export default class IfcClientLinkElement extends HTMLElement {
private _client: Client;

/** @internal */
constructor(clientInstance: any) {
super();
this._client = clientInstance;
}

/**
* @internal
* @inheritdoc
*/
static get observedAttributes() {
return [PATH_ATTR];
}

/**
* @internal
* @inheritdoc
*/
public connectedCallback() {
const content = this.innerHTML;
let path = this.getAttribute(PATH_ATTR) || '';
path = this._client.urlFromClientPath(path);
this.innerHTML = `<a target='_top' href=${path}>${content}</a>`;
}

/**
* @internal
* @inheritdoc
*/
public attributeChangedCallback(
name: string,
oldValue: string | null,
newValue: string | null
) {
if (name === PATH_ATTR && oldValue !== newValue) {
let path = this.getAttribute(PATH_ATTR) || '';
path = this._client.urlFromClientPath(path);
this.firstElementChild?.setAttribute('href', path);
}
}
}
Loading

0 comments on commit 6f77f9d

Please sign in to comment.