Skip to content

Commit

Permalink
feat(history-service): support multiple location changes in one navig…
Browse files Browse the repository at this point in the history
…ation (#518)

closes #496
fixes #441

Co-authored-by: Stefan Meyer <stefan.meyer@sinnerschrader.com>
  • Loading branch information
unstubbable and stemey committed Jul 5, 2019
1 parent 5f4a3d3 commit 1352318
Show file tree
Hide file tree
Showing 38 changed files with 3,305 additions and 1,514 deletions.
104 changes: 43 additions & 61 deletions docs/guides/sharing-the-browser-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ its own history.

How the root location is build from the consumer locations, is a problem that
can not be solved generally, since it is dependant on the usecase. This is why
the integrator defines the service with a so-called **root location
the integrator defines the History Service with a so-called **root location
transformer**. The root location transformer provides functions for merging
consumer locations into a root location, and for extracting a consumer path from
the root location.
Expand All @@ -42,8 +42,6 @@ JSON string which will be assigned to a single configurable query parameter.

### As a Feature App

On the client:

```js
import {Router} from 'react-router';
```
Expand All @@ -54,17 +52,16 @@ const myFeatureAppDefinition = {

dependencies: {
featureServices: {
's2:history': '^1.0.0'
's2:history': '^2.0.0'
}
},

create(env) {
const historyService = env.featureServices['s2:history'];
const browserHistory = historyService.createBrowserHistory();

return {
render: () => (
<Router history={browserHistory}>
<Router history={historyService.history}>
<App />
</Router>
)
Expand All @@ -73,40 +70,9 @@ const myFeatureAppDefinition = {
};
```

On the server:

```js
import {Router} from 'react-router';
```

```js
const myFeatureAppDefinition = {
id: 'acme:my-feature-app',

dependencies: {
featureServices: {
's2:history': '^1.0.0'
}
},

create(env) {
const historyService = env.featureServices['s2:history'];
const staticHistory = historyService.createStaticHistory();

return {
render: () => (
<Router history={staticHistory}>
<App />
</Router>
)
};
}
};
```

For both the browser and the static history, the service is API-compatible with
the history package. Note, however, that the `go`, `goBack`, `goForward` and
`block` methods are not supported. For further information, reference its
The `history` property of the History Service is API-compatible with the history
package. Note, however, that the `go`, `goBack`, `goForward` and `block` methods
are not supported. For further information, reference its
[documentation][history-npm].

### As the Integrator
Expand Down Expand Up @@ -135,8 +101,9 @@ const featureHub = createFeatureHub('acme:integrator', {
```

On the server, the History Service needs the server request to compute the
initial history location of the static history. The integrator therefor defines
the server request Feature Service:
initial history location of the static history. The integrator therefore defines
the server request Feature Service, and sets the `mode` of the History Service
to `'static'`:

```js
import {createFeatureHub} from '@feature-hub/core';
Expand All @@ -159,7 +126,7 @@ const request = {
const featureHub = createFeatureHub('acme:integrator', {
featureServiceDefinitions: [
defineServerRequest(request),
defineHistoryService(rootLocationTransformer)
defineHistoryService(rootLocationTransformer, {mode: 'static'})
]
});
```
Expand All @@ -171,36 +138,26 @@ A root location transformer is an object that implements the
[`@feature-hub/history-service`][history-service-api] package. It provides two
functions, `getConsumerPathFromRootLocation` and `createRootLocation`. In the
following example, each consumer location is encoded as its own query parameter,
with the `consumerUid` used as parameter name:
with the `historyKey` used as parameter name:

```js
import * as history from 'history';
```

```js
const rootLocationTransformer = {
getConsumerPathFromRootLocation(rootLocation, consumerUid) {
getConsumerPathFromRootLocation(rootLocation, historyKey) {
const searchParams = new URLSearchParams(rootLocation.search);

return searchParams.get(consumerUid);
return searchParams.get(historyKey);
},

createRootLocation(consumerLocation, rootLocation, consumerUid) {
const searchParams = new URLSearchParams(rootLocation.search);

if (consumerLocation) {
searchParams.set(consumerUid, history.createPath(consumerLocation));
} else {
searchParams.delete(consumerUid);
}

const {pathname, state} = rootLocation;
createRootLocation(currentRootLocation, consumerLocation, historyKey) {
const searchParams = new URLSearchParams(currentRootLocation.search);
searchParams.set(historyKey, history.createPath(consumerLocation));
const {pathname, state} = currentRootLocation;

return {
pathname,
search: searchParams.toString(),
state
};
return {pathname, search: searchParams.toString(), state};
}
};
```
Expand Down Expand Up @@ -275,8 +232,33 @@ When a History Service consumer pushes the same location multiple times in a row
and the user subsequently navigates back, no pop event is emitted for the
unchanged location of this consumer.

## Changing multiple consumers at once with a single navigation

To trigger a navigation from a Feature App to another page that composes a
different set of Feature Apps, a navigation Feature Service that encapsulates
integrator routing logic would be needed.

Such a Feature Service would have the need to collect consumer locations from
other consumers (and itself), and then push a single root location that combines
these consumer locations to the root history.

To accomplish that, the History Service exposes the following additional
properties:

- `historyKey`: The history key that has been assigned to the consumer.
- `createNewRootLocationForMultipleConsumers`: A method that creates a new root
location from multiple so-called consumer locations. A consumer location
consists of the actual `location` and the `historyKey` of the consumer.
- `rootHistory`: Offers `push`, `replace`, and `createHref` methods that all
accept a new root location that was created using the
`createNewRootLocationForMultipleConsumers` method.

> For more details see the ["Advanced Routing" demo][advanced-routing-demo].
[browser-history-api]: https://developer.mozilla.org/en-US/docs/Web/API/History
[history-npm]: https://www.npmjs.com/package/history
[history-service-api]: /@feature-hub/modules/history_service.html
[history-service-demo]:
https://github.com/sinnerschrader/feature-hub/tree/master/packages/demos/src/history-service
[advanced-routing-demo]:
https://github.com/sinnerschrader/feature-hub/tree/master/packages/demos/src/advanced-routing
11 changes: 11 additions & 0 deletions packages/demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ Demonstrates:
yarn watch:demo integrator-dom
```

### [Advanced Routing](src/advanced-routing)

Demonstrates:

- how the `@feature-hub/history-service` package and a custom navigation service
can be utilized to push location changes from one Feature App to another

```sh
yarn watch:demo advanced-routing
```

---

Copyright (c) 2018-2019 SinnerSchrader Deutschland GmbH. Released under the
Expand Down
6 changes: 4 additions & 2 deletions packages/demos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@types/get-port": "^4.0.0",
"@types/history": "^4.7.2",
"@types/pino": "^5.8.5",
"@types/react-router": "^5.0.2",
"@types/styled-components": "^4.1.14",
"@types/webpack": "^4.4.20",
"@types/webpack-dev-middleware": "^2.0.2",
Expand All @@ -38,9 +39,10 @@
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.5.0",
"puppeteer": "^1.11.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-media": "^1.9.2",
"react-router": "^5.0.1",
"style-loader": "^0.23.1",
"styled-components": "^4.1.3",
"ts-loader": "^6.0.0",
Expand Down
54 changes: 54 additions & 0 deletions packages/demos/src/advanced-routing/hello-world-feature-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Card, Text} from '@blueprintjs/core';
import {FeatureAppDefinition} from '@feature-hub/core';
import {ReactFeatureApp} from '@feature-hub/react';
import * as React from 'react';
import {HelloWorldServiceV1} from './hello-world-service';

interface Dependencies {
readonly 'test:hello-world-service': HelloWorldServiceV1;
}

interface HelloWorldTextProps {
readonly helloWorldService: HelloWorldServiceV1;
}

function HelloWorldText({helloWorldService}: HelloWorldTextProps): JSX.Element {
const [name, setName] = React.useState(helloWorldService.name);

React.useEffect(
() => helloWorldService.listen(() => setName(helloWorldService.name)),
[helloWorldService]
);

return (
<Text>
<span id="hello-text">Hello, {name}!</span>
</Text>
);
}

const featureAppDefinition: FeatureAppDefinition<
ReactFeatureApp,
Dependencies
> = {
dependencies: {
featureServices: {
'test:hello-world-service': '^1.0.0'
},
externals: {
react: '^16.7.0'
}
},

create: ({featureServices}) => ({
render: () => (
<Card style={{margin: '20px'}}>
<HelloWorldText
helloWorldService={featureServices['test:hello-world-service']}
/>
</Card>
)
})
};

export default featureAppDefinition;
64 changes: 64 additions & 0 deletions packages/demos/src/advanced-routing/hello-world-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
FeatureServiceBinder,
FeatureServiceProviderDefinition,
SharedFeatureService
} from '@feature-hub/core';
import {ConsumerLocation, HistoryServiceV2} from '@feature-hub/history-service';
import * as history from 'history';

export interface HelloWorldServiceV1 {
name: string;
listen(listener: () => void): () => void;
createLocation(name: string): ConsumerLocation;
}

export interface SharedHelloWorldService extends SharedFeatureService {
readonly '1.0.0': FeatureServiceBinder<HelloWorldServiceV1>;
}

export interface HelloWorldServiceDependencies {
readonly 's2:history': HistoryServiceV2;
}

export const helloWorldServiceDefinition: FeatureServiceProviderDefinition<
SharedHelloWorldService,
HelloWorldServiceDependencies
> = {
id: 'test:hello-world-service',

dependencies: {
featureServices: {
's2:history': '^2.0.0'
}
},

create: ({featureServices}) => {
const historyService = featureServices['s2:history'];

return {
'1.0.0': () => ({
featureService: {
get name(): string {
return historyService.history.location.pathname.slice(1) || 'World';
},

set name(value: string) {
const {location} = this.createLocation(value);
historyService.history.push(location);
},

listen(listener: () => void): () => void {
return historyService.history.listen(listener);
},

createLocation(name: string): ConsumerLocation {
return {
historyKey: historyService.historyKey,
location: history.createLocation(`/${name}`)
};
}
}
})
};
}
};
Loading

0 comments on commit 1352318

Please sign in to comment.