@@ -55,4 +57,4 @@ const api = await expose({
- Easy to use - Create a micro frontend with just a few lines of code.
- Use a self explainatory api to describe your micro frontends and orchestrate them in complex arrangements effortlessly.
- Built on web standards and only a few simple core concepts means that you never run into magic behaviour that ruins your day.
-- Easy to use - Simply wrap the expose() call to create custom functionality.
\ No newline at end of file
+- Easy to use - Simply wrap the expose() call to create custom functionality.
diff --git a/docs/README.md b/docs/README.md
index 90a4f15..b5ed59c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -29,7 +29,7 @@ features:
diff --git a/docs/docs/core-api.md b/docs/docs/core-api.md
index e8cb937..93d8d51 100644
--- a/docs/docs/core-api.md
+++ b/docs/docs/core-api.md
@@ -470,7 +470,7 @@ contextApi.topics.foo.myTopic.publish('a new Value');
```
-### Lifecycle Hooks
+## Lifecycle Hooks
Collage is asynchronous. To trigger certain activities in an Arrangement or Fragment, you can use Lifecycle Hooks.
A hook returns a function, which can be executed to deregister the hook (analogous to addEventListener and removeEventListener).
@@ -496,7 +496,7 @@ const options = { once: true };
// No need for using the returned deregister function
onUpdated(callback, options);
```
-##### onLoaded
+#### onLoaded
To be sure, that the initialization process is completed and the embedded fragment can be used on the arrangement, this hook exists. This hook takes the name of a fragment as first parameter and a callback as second. The third parameter is optional and represents the options argument of addEventListener.
The onLoaded hook Executes a callback if a fragment with a specific name is loaded.
@@ -510,7 +510,7 @@ onLoaded("myFragment", () => {
The onLoaded hook is based on the `collage-fragment-loaded` event, which is dispatched, if an embedded fragment is completed with the initial loading. The event emits the context id of the fragment, which was loaded.
You can use the event if necessary, but you should prefere the hook.
-##### onUpdated
+#### onUpdated
Executes a callback if the context of this fragment is updated. This hook takes a callback as first parameter. The second parameter is optional and represents the options argument of addEventListener.
```js {2-5}
@@ -524,7 +524,7 @@ onUpdated(() => {
The onUpdated hook is based on the `collage-context-updated` event, which is dispatched, everytime something on the own context changed. The event emits the updated context.
You can use the event if necessary, but you should prefere the hook.
-##### onConfigUpdated
+#### onConfigUpdated
Executes a callback if the config of this context was updated. This hook takes a callback as first parameter. The second parameter is optional and represents the options argument of addEventListener.
```js {2-9}
@@ -543,7 +543,7 @@ onConfigUpdated(() => {
The onConfigUpdated hook is also based on the `collage-context-updated` event, which is dispatched, everytime something on the own context changed. The event emits the updated context.
You can use the event if necessary, but you should prefer the hook.
-### Deregistering a fragment
+## Deregistering a fragment
If you want to remove a fragment from your arrangement, just remove it from the DOM. Collage takes care that the parents context gets cleaned up.
:::tip
diff --git a/docs/docs/features.md b/docs/docs/features.md
index 3267522..0ae7d92 100644
--- a/docs/docs/features.md
+++ b/docs/docs/features.md
@@ -11,6 +11,9 @@ Embed micro frontends in your application.
### Configure embedded micro frontends
Configure embedded micro frontends to fit them perfectly into your application.
+### Fragment Functions
+Provide functions which are directly executable on the micro frontend by an Arrangement
+
### Service API
Provide services to other micro frontends and the whole Arrangement and use services, other Contexts are exposing.
@@ -20,9 +23,6 @@ Publish messages or subscribe to topics which are available for all parts of you
### (coming soon) Style Synchronization
Define styles for your application, the embedded micro frontends will adopt it. Users of your application can also change styles at runtime, e.g. to switch to a dark-theme.
-### (coming soon) Style Synchronization
-Micro frontends adopt the styling of their parents to make your application look like one piece.
-
## Non-functional Features
### Framework Agnosticity
diff --git a/index.html b/index.html
index b989886..f6a40f9 100644
--- a/index.html
+++ b/index.html
@@ -16,13 +16,15 @@
Collage Development Dashboard
diff --git a/src/DEVELOPER_DOCUMENTATION.md b/src/DEVELOPER_DOCUMENTATION.md
new file mode 100644
index 0000000..b077072
--- /dev/null
+++ b/src/DEVELOPER_DOCUMENTATION.md
@@ -0,0 +1,119 @@
+# Developer Documentation
+
+Before starting to develop collage, please read the documentation about the collage [features](../docs/docs/features.md), [core concepts](../docs/docs/concepts.md) and [core api](../docs/docs/core-api.md).
+
+## Getting started
+
+```bash
+git clone https://github.com/SICKAG/collage.git
+cd collage
+npm install
+```
+
+Now you are ready for developing
+
+### Start the dev server
+
+You can start the dev server by running
+
+```bash
+npm run dev
+```
+
+This will start a dev server. You can then view and debug the example and integration tests defined in the sample folders of the plugins (src/core/)
+
+### Running the tests
+
+You can (and should) keep the unit test running in watch mode while developing
+
+```bash
+npm run test:watch
+```
+
+> Hint: you can keep the dev server and the unit test running at the same time
+
+## What we are building upon:
+
+- typescript (see [typescript](https://www.typescriptlang.org/) and [tsconfig.json](../tsconfig.json))
+- vite for serving our examples and integration tests and bundling the library (see [vite](https://vitejs.dev) and [vite.config.js](../vite.config.js))
+- jest for unit tests (see [jest](https://jestjs.io) and [jest.config.js](../jest.config.js))
+- vuepress for building our user documentation (see [vuepress](https://vuepress) and [vuepress-config.mjs](../docs/vuepress-config.mjs))
+- github actions for ci/cd
+- penpal for the communication between fragments and arrangements (see [penpal](https://github.com/Aaronius/penpal#readme))
+
+## Structure of the Repository
+
+The structure of the repository is as follows.
+In each folder, you will find a documenting markdown file explaining the details of the respective part.
+
+```md
+collage/
+|-- .github/ // github actions configurations
+|-- docs/ // user documentation built with vuepress
+|-- e2e/ // our e2e tests
+|-- src/ // source code
+| |-- core/ // core concepts of collage implemented as collage plugins
+| |-- elements/ // custom elements for a convenient usage of collage
+| |-- lib/ // internal library structure of collage
+| | |-- api/ // collage api
+
+```
+
+- [core](./core/CORE_CONCEPTS.md)
+- [elements](./elements/README.md)
+- [lib](./lib/README.md)
+
+## Architecture
+
+Collage is build as a plugin library. Each basic feature is defined as a plugin. All default plugins are configured and bootstrapped together.
+
+How this is done, you can read in the documentation about the [internal structure of the collage library](./lib/README.md).
+
+In short:
+
+- all types, and everything needed to put collage and the plugins together can be found in lib.
+- everything that implements functionality for the user is a plugin and can be found in core.
+
+## Contributing
+
+Every contribution is welcome, as we want to make collage a vivid community project.
+
+At the moment we lack a system to enable plugins dynamically, so each plugin must be built and bundled with the library. We are evaluating to change that in the future, so everybody could add functionality to collage.
+
+### Creating a new plugin/feature
+
+#### Workflow
+
+1. create the plugin
+ 1. create a new folder in the src/core directory
+ 1. create a plugin file as ts (orient at the core plugins)
+ 1. create a samples folder for integration testing examples
+1. create tests
+ 1. create e2e test for the new feature
+ 1. create unit tests in the plugin folder
+1. create documentation
+ 1. create a documentation file as md if necessary
+1. add the plugin to collage
+ 1. import the plugin
+ 1. add to the plugins array
+
+#### How a plugin is made
+
+With a plugin, you are able to enhance collage by new features.
+For an example of a minimal plugin, please have a look at the [create-context](./core/create-context/create-context.ts).
+
+In short, a valid plugin needs to export the default function `plugin` where it can pass an object, that uses the PluginFunctions to enhance collage by the new features (see [src/types.ts>PluginFunctions](./types.ts) and [collage-plugin](./lib/collage-plugin.ts)).
+
+```ts
+import plugin from '../../lib/collage-plugin';
+
+export default plugin({
+ enhanceExpose: async () => {
+ console.log('I send a message everytime expose() is called');
+ },
+});
+```
+
+## Core Concepts
+
+Read further in the [Core Concepts Documentation](./core/CORE_CONCEPTS.md)
diff --git a/src/architecture.md b/src/architecture.md
deleted file mode 100644
index d869941..0000000
--- a/src/architecture.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Core Architecture
-
-## Module Structure
-
-```javascript
-connectArrangement() {
-
- serviceFunctions() {
- const definition = {
- services: { foo() }
- }
-
- const context = createContext(definition) {
- return { id, parentOrigin }
- }
-
- return { ...context, services }
- }
-}
-```
-
-
-## Modules
-
-### 1. Create Context
-
-#### Description
-
-**In:**
-```javascript
-{}
-```
-
-**Out:**
-```javascript
-{}
-```
-
-
-#### Context
-
-**In:**
-```javascript
-{}
-```
-
-**Out:**
-```javascript
-{
- id: '1234-1234-1234',
- arrangementOrigin: 'http://some.server'
-}
-```
-
-
-
diff --git a/src/core/CORE_CONCEPTS.md b/src/core/CORE_CONCEPTS.md
new file mode 100644
index 0000000..07676be
--- /dev/null
+++ b/src/core/CORE_CONCEPTS.md
@@ -0,0 +1,109 @@
+# Core Concepts
+
+The core concepts and their implementations as plugins build upon each other and each enhance collage by their specific features.
+
+1. The [collage-fragment](../elements/README.md) custom element.
+1. The [create-context](./create-context/CREATE_CONTEXT.md) plugin
+1. The [handshake](./handshake-plugin/HANDSHAKE.md) plugin
+1. The [direct functions](./direct-functions-plugin/DIRECT_FUNCTIONS.md) plugin
+1. The [services](./services-plugin/SERVICES.md) plugin
+1. The [topics](./topics-plugin/TOPICS.md) plugin
+
+### Notes
+
+How the connection between arrangement and fragment works:
+
+The connection is mediated by the custom element, since that is the point where we are in the parents code context but actually know about a specific child to connect.
+
+A successful connection then enhances the fragments context with methods from the arrangement.
+
+## Create Context
+The most basic concept in collage is that of a context. At its base a context is simply the representation of a uniquely identified fragment and the connection to a potential parent.
+
+For a more detailed description of the handshake, see [Create Context plugin documentation](./create-context/CREATE_CONTEXT.md).
+
+
+## Handshake
+To set up the communication between a fragment and an arrangement, collage performs a handshake between them.
+
+For a more detailed description of the handshake, see [Handshake plugin documentation](./handshake-plugin/HANDSHAKE.md).
+
+## Service Functions
+Services are functions, an arrangement can provide to all the fragments (and their fragments).
+
+For a more detailed description of the service functions, see [Services plugin documentation](./services-plugin/SERVICES.md).
+
+## Direct Functions
+Direct Functions can be called on a fragment directly by its arrangement
+
+For a more detailed description of direct functions, see [Direct Functions plugin documentation](./direct-functions-plugin/DIRECT_FUNCTIONS.md).
+
+## Topics
+The Topics feature allows an easy way to subscribe to topics and publish new values to topics.
+
+For a more detailed description of the topics plugin, see [Topics plugin documentation](./topics-plugin/TOPICS.md).
+
+## Config
+With configurations an arrangement gets the possibility to configure an embedded fragment and overwrite the default configuration of it.
+
+For a more detailed description of the config plugin, see [Config plugin documentation](./config-plugin/CONFIG_PLUGIN.md).
+
+
+## Finalize Api
+
+The last module to be performed will trim the resulting context api to the fields that we intend a client to use.
+
+Any internal state that may have been needed to communicate between plugin modules should not be visible in client context.
+
+
+
+## Fragment <-> Arrangement
+
+**In the fragment**
+
+1. collect stuff for my children
+2. collect my exposed functions (child functions)
+3. simple beacon request --> get simple beacon answer?
+4. `connectToParent` with my functions -> gain parent services
+5. add parent stuff to context
+6. send `collage-initialized` event
+7. all my fragments will initialize iframes
+ 1. create iframe
+ 2. send _what is your id?_ as post message
+ 3. await answer
+ 4. if answer: `connectToChild`
+
+**In the arrangement**
+
+1. setup a beacon
+2. When a question arrives -> check if we have a matching fragment element (id)
+3. initiate `connectToChild` with the iframe in that element
+4. send response
+
+```javascript
+const definition = {
+ services: {},
+ topics: {},
+ configuration: {},
+ functions: {},
+}
+
+const context = await expose(description)
+
+const arrangementContext = connectToArrangement(description, context)
+
+const combined = combineContexts(context, arrangementContext)
+
+createBeacon(combined, (question) => {
+ if (findFragmentInDOM(question)) {
+ connectToChild()
+ sendAnswer()
+ }
+})
+
+return combined
+```
+
+```html
+
+```
diff --git a/src/core/README.md b/src/core/README.md
deleted file mode 100644
index 25efc6e..0000000
--- a/src/core/README.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Core Concepts
-
-Core concepts and their implementations build upon each other.
-
-## Notes
-
-How the connection between arrangement and fragment should work:
-
-The connection should be mediated be the custom element, since that is the point where I am in the parents code context but acually know about a specific child I want to connect.
-
-A successful connection should then augment the childs context with _stuff_ from the parent.
-
-Problems:
- - if the context is not **global**, then how can we get hold of it in the
- custom element
- - exactly HOW do we modify that context and what is the result
- - how do we keep a separation of domains? We don't want to know about configuration or services at this point.
-
-> Maybe use a custom event for which we can initiate event handlers (with bubbeling) during expose?
-
-> Maybe the custom element listens for a specific postMessage call that should get sent by the contained document **IF** it is a collage fragment.
-> If such a message occours, the listener will:
-> 1. mediate a penpal connection
-> 2.
-
-## Create Context
-
-The most basic concept in collage is that of a context. At it's base a context is simply the representation of a uniquly identified fragment and the connection to a potential parent.
-
-This takes no argument.
-
-And creates the following context api:
-
-```javascript
-{
- // unique (uuidv4) context id
- id: 'xxxx-xxxx-xxxx-xxxx'
-
- // wether we are embedded in another arrangement
- hasArrangement: true || false
-}
-```
-
-### Parent handshake
-
-The handshake between us and a potential parent works like this:
-
-1. before `expose` is even called (during the import), a beacon for potential fragments is initialized.
-2. when a fragment calls `expose`, it sends a _ping_ postMessage to the window at `window.parent`
-3. when after 300ms no answer arrives, we asume to be alone and end the handshake here
-4. if we receive a answer message, we assume to be embedded in an arrangement.
-
-That's it
-
-
-## Simple services
-
-Using penpal, we expose a number of functions that we can use ourselfs later on. These functions may be overwritten by a arrangement if one exists and if it contains a service function with the same name.
-
-
-> **ATTENTION** penpal wants to always initiate a handshake from parent to child, while we intend to do it the other way round (!)
-
-```javascript
-const {
- services: {
- // You are guaranteed to receive service functions for all service functions
- // you did expose. Some of them may be overwritten though.
- foo, bar, baz
- }
-} = await expose({
- // any number of named functions that act as an overwritable service
- services: {
- foo() { /* ... */ },
- bar() { /* ... */ },
- baz() { /* ... */ },
- }
-})
-```
-
-Each service function should act as follows, when called:
-
-1. attempt to obtain the penpal-parent connection
-2. (if connected) attempt to call service function on penpal parent
-3. (if successful) return the returned value to the client
-4. (if not connected or no such service on parent) call own implementation and return the return value
-
-
-## Finalize Api
-
-The last module to be performed will trim the resulting context api to the fields that we intend a client to use.
-
-Any internal state that may have been needed to communicate between plugin modules should not be visible in client context.
-
-
-
-## Fragment <-> Arrangement
-
-**In the fragment**
-
-1. collect stuff for my children
-2. collect my exposed functions (child functions)
-3. simple beacon request --> get simple beacon answer?
-4. `connectToParent` with my functions -> gain parent services
-5. add parent stuff to context
-6. send `collage-initialized` event
-7. all my fragments will initialize iframes
- 1. create iframe
- 2. send _what is your id?_ as post message
- 3. await answer
- 4. if answer: `connectToChild`
-
-**In the arrangement**
-
-1. setup a beacon
-2. When a question arrives -> check if we have a matching fragment element (id)
-3. initiate `connectToChild` with the iframe in that element
-4. send response
-
-```javascript
-const definition = {
- services: {},
- topics: {},
- configuration: {},
- functions: {},
-}
-
-const context = await expose(description)
-
-const arrangementContext = connectToArrangement(description, context)
-
-const combined = combineContexts(context, arrangementContext)
-
-createBeacon(combined, (question) => {
- if (findFragmentInDOM(question)) {
- connectToChild()
- sendAnswer()
- }
-})
-
-return combined
-```
-
-```html
-
-```
diff --git a/src/core/config-plugin/README.md b/src/core/config-plugin/CONFIG_PLUGIN.md
similarity index 100%
rename from src/core/config-plugin/README.md
rename to src/core/config-plugin/CONFIG_PLUGIN.md
diff --git a/src/core/create-context/CREATE_CONTEXT.md b/src/core/create-context/CREATE_CONTEXT.md
new file mode 100644
index 0000000..ae57126
--- /dev/null
+++ b/src/core/create-context/CREATE_CONTEXT.md
@@ -0,0 +1,16 @@
+# Create Context
+The most basic concept in collage is that of a context. As its base a context is simply the representation of a uniquely identified fragment and the connection to a potential parent.
+
+This takes no argument.
+
+And creates the following context api:
+
+```javascript
+{
+ // unique (uuidv4) context id
+ id: 'xxxx-xxxx-xxxx-xxxx'
+
+ // wether we are embedded in another arrangement
+ hasArrangement: true || false
+}
+```
diff --git a/src/core/create-context/samples/index.html b/src/core/create-context/samples/index.html
new file mode 100644
index 0000000..9e805c4
--- /dev/null
+++ b/src/core/create-context/samples/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+ Document
+
+
+
+
+
+ This example shows how to initialize a context.
+
+ ContextID:
+
+
+
diff --git a/src/core/direct-functions-plugin/DIRECT_FUNCTIONS.md b/src/core/direct-functions-plugin/DIRECT_FUNCTIONS.md
new file mode 100644
index 0000000..3144482
--- /dev/null
+++ b/src/core/direct-functions-plugin/DIRECT_FUNCTIONS.md
@@ -0,0 +1,3 @@
+# Direct Functions
+
+
diff --git a/src/core/direct-functions-plugin/direct-functions.ts b/src/core/direct-functions-plugin/direct-functions.ts
index e7af54b..cc17305 100644
--- a/src/core/direct-functions-plugin/direct-functions.ts
+++ b/src/core/direct-functions-plugin/direct-functions.ts
@@ -6,8 +6,23 @@ import {
import log from '../../lib/logging';
import { reservedWords } from '../index';
+/**
+ * The Direct Functions Plugin enhances the fragment with the possibility to provide functions
+ * to the arrangement directly.
+ */
+
+/**
+ * There are no specific requirements to the context before this plugin is applied.
+ */
type PreviousContext = { /** */ }
+/**
+ * After applying this plugin, the context has the following properties
+ * @property fragments - a proxy object, which provides access to the embedded fragments (sub fragments of this one)
+ * @property functions - a proxy object, which provides access to the functions of this fragment
+ * @property _plugins - a property, which contains all plugins, which are applied to this fragment - especially
+ * the direct functions plugin
+ */
export type EnhancedContext = PreviousContext & {
fragments: Fragments;
functions: Functions;
@@ -18,17 +33,23 @@ export type EnhancedContext = PreviousContext & {
}
}
+/**
+ * Executes a function on a fragment
+ * @param context - the arrangement context
+ * @param fragmentID - the id of the fragment, which provides the function
+ * @param functionName - the name of the function
+ */
function executeFunction(context: EnhancedContext, fragmentID: string, functionName: string) {
return (
(context._plugins.directFunctionsPlugin.fragments as Fragments)[fragmentID].functions as Functions)[functionName];
}
/**
- * Manages the communication via direct functions.
- * Direct functions can be called on contexts directly.
- * A fragment can provide such functions to its arrangement, which therefore can executed this functions.
+ * Creates a proxy handler for the functions of a fragment
+ * @param context - the arrangement context
+ * @param fragmentID - the id of the fragment, which provides the function
+ * @returns a proxy handler for the functions of a fragment
*/
-
function functionsHandler(context: EnhancedContext, fragmentID: string): ProxyHandler {
return {
get: (__: unknown, fn: string) => {
@@ -77,6 +98,19 @@ function initFragmentsFunctions(context: PreviousContext) {
);
}
+/**
+ * The Direct Functions Plugin enhances the fragment with the possibility to provide functions
+ * to the arrangement directly.
+ *
+ * It does so by adding a functions property to the context, which is a proxy to all functions defined by
+ * the fragment. This proxy is also available on the fragments property of the context, so it can be called
+ * directly on the fragments name like following:
+ * `context.fragments.nameOfChild.foo()`
+ * It also adds a fragments property to the context, which is a proxy to all embedded "sub" fragments of the fragment.
+ *
+ * The plugin takes care to clean up the proxies, when a fragment is removed from the arrangement. This is done
+ * by listening to the collage-fragment-disconnected event.
+ */
const directFunctionsPlugin: PluginFunctions = {
enhanceExpose({ functions }: FrontendDescription, context: PreviousContext) {
document.addEventListener('collage-fragment-disconnected', (e) => {
diff --git a/src/core/handshake-plugin/HANDSHAKE.md b/src/core/handshake-plugin/HANDSHAKE.md
new file mode 100644
index 0000000..d96d198
--- /dev/null
+++ b/src/core/handshake-plugin/HANDSHAKE.md
@@ -0,0 +1,127 @@
+# Handshake
+With the handshake plugin, we connect the fragment to its arrangement, so both are able to communicate with each other.
+
+## Arrangement
+An arrangement defines the layout and configuration of its embedded fragments and is able to use the Direct Functions API to communicate with its fragments directly.
+An arrangement can be a fragment itself and thus be embedded into other arrangements.
+
+The arrangement initiates the handshake with each of its fragments to enable communication.
+
+## Fragment
+
+## Handshake
+
+> penpal wants to always initiate a handshake from parent to child, while we intend to do it the other way round, so we are setting up the handshake in a separate plugin
+
+The handshake between a fragment and a potential arrangement works like this:
+
+1. before `expose` is called (during the import), a beacon for potential fragments is initialized.
+2. when a fragment calls `expose`, it sends a _ping_ postMessage to the window at `window.parent`
+3. when after 300ms no answer arrives, we assume to be alone and end the handshake
+4. if we receive an answer message, we assume to be embedded in an arrangement.
+
+
+### Sequence Diagrams of the handshake procedure
+
+The following sequence diagram describes the handshake procedure in detail.
+The steps of the handshake procedure are prefixed in pointy brackets with the following meaning.
+
+* `` Step 1 that is performed in the arrangement
+* `` Step 1 that is performed in the fragment
+* `` A step, that involves penpal directly
+These steps are referenced in comments in the [handshake plugin](./handshake.ts)
+
+The diagram shows four entities:
+1. `Zero` - a non-existing document or a document not calling a collage expose function
+1. `Arrangement` - an arrangement
+1. `Fragment` - a fragment embedded in `Arrangement`
+1. `SubFragment` - a fragment embedded in `Fragment`
+
+#### Sequence Diagram for a system of an Arrangement, a Fragment and a SubFragment
+```mermaid
+sequenceDiagram
+ participant Zero
+ participant Arrangement
+ participant Fragment
+ participant SubFragment
+
+
+
+ Arrangement ->> Arrangement: expose
+ Fragment ->> Fragment: expose
+ Arrangement -->> Arrangement: listenFor(callForArrangement)
+ Arrangement -->> Zero: ...callForArrangement
+ Arrangement -->> Arrangement: ...listenFor(answerToCallForArrangement)
+
+ Fragment -->> Fragment: listenFor(callForArrangement)
+ Fragment ->> Fragment: callForArrangement()
+
+ activate Fragment
+ Fragment -->> Fragment: listenFor(answerToCallForArrangement)
+ Fragment ->> Arrangement: sendMessage: call-for-arrangement
+ deactivate Fragment
+
+ activate Arrangement
+ Arrangement ->> Arrangement: answerToCallForArrangement()
+ Arrangement ->> Arrangement: connectToFragment()
+ Arrangement -->> Fragment: connectToChild
+ Fragment -->> Arrangement: connectToChild
+ Arrangement ->> Fragment: sendMessage: answer-to-call-for-arrangement
+ deactivate Arrangement
+
+ activate Fragment
+ Fragment ->> Fragment: connectToArrangement()
+ Fragment -->> Arrangement: connectToParent
+ activate Arrangement
+ Fragment->>Fragment: listenFor(reinitializeFragment)
+ Arrangement->>Arrangement: updateContext() // direct functions
+ Arrangement -->> Fragment: connectToParent
+
+ activate Fragment
+ Arrangement -->> Arrangement: dispatchEvent(fragmentLoaded, fragmentName)
+ deactivate Arrangement
+ Fragment->>Fragment: updateContext() // branchServices
+ Fragment ->> Fragment: reinitializeFragments()
+ deactivate Fragment
+
+ loop for fragment in DOM
+ Fragment ->> SubFragment: sendMessage: reinitialize-fragment
+ activate SubFragment
+ SubFragment->> SubFragment: 2. callForArrangement() ...
+ deactivate SubFragment
+ end
+ deactivate Fragment
+
+```
+
+### penpal functions
+Collage uses penpal to perform the communication between an arrangement and a fragment. It builds upon the penpal functions to add features like direct functions, services and topics.
+
+All references to penpal are encapsulated in the handshake plugin (see [handshake.ts](./handshake.ts) and [handshake-data.ts](./handshake-data.ts)).
+
+#### how it is done
+After the arrangement and the fragment have performed the first steps of the handshake (``, ``, ``, ``, and ``), the fragment has the information that it is embedded in an arrangement and the arrangement has the information, that it embeds a fragment. Now we can start to initialize penpal to establish the communication between them. To do so, we utilize the functions `connectToChild()` and `connectToParent()` from penpal:
+
+```typescript
+const connection = connectToChild({
+ iframe: fragmentIframe,
+ methods: extractAsArrangement(data),
+ childOrigin: '*',
+ debug: PENPAL_DEBUG,
+});
+```
+
+This sets all functions that are specified in the FrontendDescription of the Arrangement (see extractAsArrangement()) to be CallableFunctions on the penpal parent (the Arrangement). The penpal child (the Fragment) is able to call them.
+
+```typescript
+connectToParent({
+ methods: extractAsFragment({
+ description: data.description,
+ context: data.context as GenericPluginAPI,
+ callback: (description: FrontendDescription) => updateConfigCallback(description)(data.context),
+ }),
+ debug: PENPAL_DEBUG,
+})
+```
+
+This sets all functions that are specified in the FrontendDescription of the Arrangement (see extractAsArrangement()) to be CallableFunctions on the penpal child (the Fragment) by the penpal parent (the Arrangement). It additionally sets the three functions `description`, `context` and `callback`. These functions allow to access the fragments context and FrontendDescription and to update the fragments configuration.
diff --git a/src/core/handshake-plugin/README.md b/src/core/handshake-plugin/README.md
deleted file mode 100644
index 2886775..0000000
--- a/src/core/handshake-plugin/README.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Arrangement
-An arrangement defines the layout and configuration of it's embedded fragments and is able to use the Direct Functions API to communicat with its fragments directly.
-An arrangement can be a fragment itself and thus be embedded into other arrangements.
-
-The arrangement initiates the handshake with each of its fragments to enable communication.
-
-
-```mermaid
-sequenceDiagram
- participant Zero
- participant Arrangement
- participant Fragment
- participant SubFragment
-
-
-
- Arrangement ->> Arrangement: expose
- Fragment ->> Fragment: expose
- Arrangement -->> Arrangement: A-1 listenFor(callForArrangement)
- Arrangement -->> Zero: ...callForArrangement
- Arrangement -->> Arrangement: ...listenFor(answerToCallForArrangement)
-
- Fragment -->> Fragment: A-1 listenFor(callForArrangement)
- Fragment ->> Fragment: F-1 callForArrangement()
-
- activate Fragment
- Fragment -->> Fragment: F-2. listenFor(answerToCallForArrangement)
- Fragment ->> Arrangement: F-1.1 message: call-for-arrangement
- deactivate Fragment
-
- activate Arrangement
- Arrangement ->>Arrangement: A-2 answerToCallForArrangement()
- Arrangement ->> Arrangement: A-3 connectToFragment()
- Arrangement -->> Fragment: A-3.1 connectToChild
- Fragment -->> Arrangement: A-3.1 connectToChild
- Arrangement ->> Fragment: A-4 message: answer-to-call-for-arrangement
- deactivate Arrangement
-
- activate Fragment
- Fragment ->> Fragment: F-3 connectToArrangement()
- Fragment -->> Arrangement: F-3.1 connectToParent
- activate Arrangement
- Fragment->>Fragment: F-3.2 listenFor(reinitializeFragment)
- Arrangement->>Arrangement: A-3.2 updateContext() // direct functions
- Arrangement -->> Fragment: F-3.1 connectToParent
-
- activate Fragment
- Arrangement -->> Arrangement: A-3.3 dispatchEvent(fragmentLoaded, fragmentName)
- deactivate Arrangement
- Fragment->>Fragment: F-3.3 updateContext() // branchServices
- Fragment ->> Fragment: F-3.4 reinitializeFragments()
- deactivate Fragment
-
- loop for fragment in DOM
- Fragment ->> SubFragment: F-3.4 message: reinitialize-fragment
- activate SubFragment
- SubFragment->> SubFragment: 2. callForArrangement() ...
- deactivate SubFragment
- end
- deactivate Fragment
-
-```
diff --git a/src/core/handshake-plugin/handshake-data.ts b/src/core/handshake-plugin/handshake-data.ts
index 349e663..ae87de3 100644
--- a/src/core/handshake-plugin/handshake-data.ts
+++ b/src/core/handshake-plugin/handshake-data.ts
@@ -13,7 +13,7 @@ export function initiateData(data: { description: FrontendDescription, context:
/**
* Extracts the important information from a context, needed by a fragment from its arrangement
*
- * @params data -
+ * @param data - the FrontendDescription and Context of the arrangement
*/
// FIXME: This should be part of The Service Plugin
export function extractAsArrangement(data: { description: FrontendDescription, context: unknown}) {
diff --git a/src/core/handshake-plugin/handshake.ts b/src/core/handshake-plugin/handshake.ts
index f662ada..3fd2559 100644
--- a/src/core/handshake-plugin/handshake.ts
+++ b/src/core/handshake-plugin/handshake.ts
@@ -32,6 +32,7 @@ import {
* Sequence Diagram: see {@link arrangement.md}
*/
+// TODO: Add a possibility to pass the penpal debug flag at runtime
const PENPAL_DEBUG = false;
type PreviousContext = {
@@ -74,7 +75,7 @@ function callForArrangement(data: { description: FrontendDescription, context: u
sendMessage({
type: messageTypes.callForArrangement,
- recepient: window.parent,
+ recipient: window.parent,
content: window.name,
});
}
@@ -119,9 +120,10 @@ function answerToCallForArrangement(data: { description: FrontendDescription, co
});
}
log('arrangement.ts', 'A2. answerToCallForArrangement()');
+ // Handshake Step A-3.2
sendMessage({
type: messageTypes.answerToCallForArrangement,
- recepient: iframe.contentWindow,
+ recipient: iframe.contentWindow,
content: window.origin,
});
}
@@ -130,12 +132,16 @@ function answerToCallForArrangement(data: { description: FrontendDescription, co
/**
* Handshake Step A-3
- * Starting a penpal connection to a fragment (A-3.1) and merge the new context to the old (A-3.2).
+ * Starting a penpal connection to a fragment (A-3.1) and merge the new context to the old (A-3.3).
*/
-function connectToFragment(iframe: HTMLIFrameElement, data: { description: FrontendDescription, context: unknown }) {
+function connectToFragment(
+ fragmentIframe: HTMLIFrameElement,
+ data: { description: FrontendDescription, context: unknown },
+) {
log('arrangement.ts', 'A3. connectToFragment()');
+ // Step A-3.1 - uses penpal function "connectToChild"
const connection = connectToChild({
- iframe,
+ iframe: fragmentIframe,
methods: extractAsArrangement(data),
// TODO: is there a more secure way to enable redirects? Maybe using the preflight check in some way?
childOrigin: '*',
@@ -145,14 +151,14 @@ function connectToFragment(iframe: HTMLIFrameElement, data: { description: Front
// Listener for (penpal) connectToArrangement
connection.promise.then((child) => extractFragmentDescriptionFromPenpalChild(
{
- frameId: iframe.name,
+ frameId: fragmentIframe.name,
functions: child as unknown as Functions,
},
)).then(async (contextPart) => {
await updateAndMergeContext(merge(data.context, contextPart));
document.dispatchEvent(new CustomEvent(
'collage-fragment-loaded',
- { detail: iframe.name },
+ { detail: fragmentIframe.name },
));
});
@@ -165,6 +171,7 @@ function connectToFragment(iframe: HTMLIFrameElement, data: { description: Front
*/
function connectToArrangement(data: { description: FrontendDescription, context: unknown }) {
log('arrangement.ts', 'F3. connectToArrangement()');
+ // Step F-3.1 - uses penpal function "connectToParent"
connectToParent({
methods: extractAsFragment({
description: data.description,
@@ -189,7 +196,7 @@ function connectToArrangement(data: { description: FrontendDescription, context:
reinitializeFragments();
sendMessage({
type: messageTypes.reloadedFragment,
- recepient: window.parent,
+ recipient: window.parent,
content: window.name,
});
});
@@ -203,11 +210,11 @@ function updateConfigCallback(arrangementDescription: FrontendDescription) {
}
/**
- * Handshake Step A-3.2 and F-3.3
+ * Handshake Step A-3.3 and F-3.3
* Update the context and merge it with the old one.
*/
async function updateAndMergeContext(context: Context) {
- log('arrangement.ts', 'A-3.2 / F-3.3 updateContext');
+ log('arrangement.ts', 'A-3.3 / F-3.3 updateContext');
const nextContext = await updateContext(context);
mergeContexts(context, nextContext as Context);
document.dispatchEvent(new CustomEvent(
@@ -236,7 +243,7 @@ function sendReinitializeMessage(iframe: Element) {
if (contentWindow) {
sendMessage({
type: messageTypes.reinitializeFragment,
- recepient: contentWindow,
+ recipient: contentWindow,
});
} else {
log('arrangement.ts', 'F-3.4 !empty iframe', name);
diff --git a/src/core/services-plugin/README.md b/src/core/services-plugin/SERVICES.md
similarity index 60%
rename from src/core/services-plugin/README.md
rename to src/core/services-plugin/SERVICES.md
index c7db03f..3264d1d 100644
--- a/src/core/services-plugin/README.md
+++ b/src/core/services-plugin/SERVICES.md
@@ -1,4 +1,34 @@
# Services
+
+
+# Service Functions
+
+Using penpal, we expose a number of functions that we can use ourselfs later on. These functions may be overwritten by a arrangement if one exists and if it contains a service function with the same name.
+
+```javascript
+const {
+ services: {
+ // You are guaranteed to receive service functions for all service functions
+ // you did expose. Some of them may be overwritten though.
+ foo, bar, baz
+ }
+} = await expose({
+ // any number of named functions that act as an overwritable service
+ services: {
+ foo() { /* ... */ },
+ bar() { /* ... */ },
+ baz() { /* ... */ },
+ }
+})
+```
+
+Each service function should act as follows, when called:
+
+1. attempt to obtain the penpal-parent connection
+2. (if connected) attempt to call service function on penpal parent
+3. (if successful) return the returned value to the client
+4. (if not connected or no such service on parent) call own implementation and return the return value
+
The services feature allows an easy way to do a communication via a request response mechanism.
If several fragments will use the same service, the service implementation of the topmost arrangement will be used.
diff --git a/src/core/topics-plugin/README.md b/src/core/topics-plugin/TOPICS.md
similarity index 100%
rename from src/core/topics-plugin/README.md
rename to src/core/topics-plugin/TOPICS.md
diff --git a/src/core/topics-plugin/simple-topics/simpleTopics.ts b/src/core/topics-plugin/simple-topics/simpleTopics.ts
index 0ea00ff..1ca2f4c 100644
--- a/src/core/topics-plugin/simple-topics/simpleTopics.ts
+++ b/src/core/topics-plugin/simple-topics/simpleTopics.ts
@@ -10,7 +10,7 @@ import log from '../../../lib/logging';
/**
* Manages the communication with a publish subscribe mechanism where topics can be dynamically defined at runtime.
*
- * Sequence Diagram: see {@link README.md}
+ * Sequence Diagram: see {@link TOPICS.md}
*/
type PreviousContext = DirectFunctionsEnhancedContext & {
diff --git a/src/elements/README.md b/src/elements/README.md
new file mode 100644
index 0000000..9db93be
--- /dev/null
+++ b/src/elements/README.md
@@ -0,0 +1,3 @@
+# Fragment Element
+
+The 'Fragment' Custom Element enables embedding a micro-frontend as a fragment into the arangement
diff --git a/src/elements/samples/fragment.html b/src/elements/samples/fragment.html
new file mode 100644
index 0000000..6172049
--- /dev/null
+++ b/src/elements/samples/fragment.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Fragment
+
+ ContextID:
+
+
diff --git a/src/elements/samples/index.html b/src/elements/samples/index.html
new file mode 100644
index 0000000..e87c466
--- /dev/null
+++ b/src/elements/samples/index.html
@@ -0,0 +1,38 @@
+
+
+
+ Embedding a Fragment
+
+
+
+
+
+ Arrangement
+
+ This example shows the embedding of a fragment into an arrangement
+
+
+ ContextID:
+
+
+
+
+
diff --git a/src/lib/README.md b/src/lib/README.md
new file mode 100644
index 0000000..2a7939f
--- /dev/null
+++ b/src/lib/README.md
@@ -0,0 +1,26 @@
+# Internal structure of the collage library
+## collage-plugin
+Collage is build as a plugin library. Each basic feature is defined as a plugin. All default plugins are configured and bootstrapped together.
+
+Each collage plugin needs to call the plugin function. The shape and general structure of a plugin is defined here.
+
+## bootstrap
+Bootstraps all defined collage-plugins into a collage object (see [Collage](../types.ts))
+The collage object then contains the functions `expose`, `updateContext`, `extractContextAsArrangement`, `extractContextAsFragment` and `extractFragmentDescription` as well as a `reservedWords` array.
+
+The `reservedWords` are identifiers, which are reserved for collage and plugins and can not be defined as e.g. direct functions by a micro-frontend (see [direct-functions-plugin](../core/direct-functions-plugin/direct-functions.ts)).
+
+The `updateContext` function is used by the syntactic sugar functions (see [Syntactic Sugar API](./api/sugar.ts)).
+
+The `expose` function is the only function which is exported directly to be used in the micro-frontends.
+
+The other functions are for setting up a collage context and performing the handshake between fragment and arrangement.
+
+## logging
+Module for internal logging
+
+## messages
+When establishing the connection between a fragment and its arrangement, penpal is not available for communication yet. So we are using the postMessage Api directly for performing a handshake.
+
+## uuid
+util functions to define unique ids for fragments
diff --git a/src/lib/api/README.md b/src/lib/api/README.md
new file mode 100644
index 0000000..ca54bca
--- /dev/null
+++ b/src/lib/api/README.md
@@ -0,0 +1,3 @@
+# API
+
+Types and Syntactic Sugar for collage
diff --git a/src/lib/bootstrap.ts b/src/lib/bootstrap.ts
index e5d7864..7464614 100644
--- a/src/lib/bootstrap.ts
+++ b/src/lib/bootstrap.ts
@@ -1,5 +1,10 @@
import { Collage, Plugin } from '../types';
+/**
+ * bootstraps all defined collage-plugins and returns a collage object
+ * @param plugins to be bootstrapped
+ * @returns a Collage object with all the capabilities defined by the plugins
+ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function bootstrap(plugins: Array>) {
return plugins.reduce(
diff --git a/src/lib/collage-plugin.ts b/src/lib/collage-plugin.ts
index 0d1763d..61bc91a 100644
--- a/src/lib/collage-plugin.ts
+++ b/src/lib/collage-plugin.ts
@@ -6,9 +6,14 @@ import {
/**
* D, E, P refer to
*
- * Description, Expected and Provided and are used here for better readability
+ * Description, Enhanced and Provided and are used here for better readability
*/
+/**
+ * Helper function to merge two contexts
+ * @param context
+ * @param append
+ */
export function mergeContexts(context: unknown, append: unknown) {
merge(context, append, (c, a) => {
if (!c) {
@@ -29,8 +34,15 @@ export function mergeContexts(context: unknown, append: unknown) {
});
}
-// TODO: Find out if we can unify the buildContext etc functions
-
+/**
+ * Builds the context type returned by a call of expose in a micro-frontend by merging the existing context
+ * type with a plugin specific context type. This happens one after the other for each plugin.
+ *
+ * @param previous context before the plugin is called
+ * @param pluginFunction function returning the plugin specific context type and functions
+ * @returns the exposed function to be used in the micro-frontend.
+ * The exposed function then returns the merged context type.
+ */
function buildContext(
previous: (description: D) => C | Promise, // "collage function"
pluginFunction: (description: D, context: C) => Promise | E | void,
@@ -43,7 +55,14 @@ function buildContext(
};
}
-function enhanceUpdateContextBlubb(
+/**
+ * Defines the enhanceUpdateContext function
+ *
+ * @param previous context before enhanceUpdateContext is called
+ * @param pluginFunction function to be appended to the context so it is available after the plugin is bootstrapped
+ * @returns the enhanced context
+ */
+function defineEnhanceUpdateContext(
previous: (context: C) => C | Promise, // "collage Function"
pluginFunction: (context: C) => Promise | E | void,
) {
@@ -83,7 +102,7 @@ export default function plugin(
): Plugin {
return (previous: Collage) => ({
expose: buildContext(previous.expose, enhanceExpose),
- updateContext: enhanceUpdateContextBlubb(previous.updateContext, enhanceUpdateContext),
+ updateContext: defineEnhanceUpdateContext(previous.updateContext, enhanceUpdateContext),
reservedWords: concatReservedWords(previous.reservedWords, reservedWords),
extractContextAsArrangement: extractPluginSpecificProperties(
previous.extractContextAsArrangement,
diff --git a/src/lib/messages.ts b/src/lib/messages.ts
index 1b1e0a6..22bf581 100644
--- a/src/lib/messages.ts
+++ b/src/lib/messages.ts
@@ -8,6 +8,9 @@ type Message = {
content: unknown,
}
+/**
+ * Listen for postMessage events and call callback if the message matches the given type
+ */
export function listenFor(
{
type,
@@ -42,15 +45,18 @@ export function listenFor(
window.addEventListener('message', listenerCallback);
}
+/**
+ * Send a postMessage event to the given recipient
+ */
export function sendMessage(
{
- recepient,
+ recipient,
context = MESSAGE_TOKEN,
targetOrigin = '*',
type,
content = '',
}: {
- recepient: Window,
+ recipient: Window,
context?: string,
targetOrigin?: string,
type: string,
@@ -58,5 +64,5 @@ export function sendMessage(
},
) {
log('messages.ts', '-->', content, type);
- recepient.postMessage({ context, type, content }, targetOrigin);
+ recipient.postMessage({ context, type, content }, targetOrigin);
}
diff --git a/src/types.ts b/src/types.ts
index 04c6098..2989775 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -49,6 +49,8 @@ export type PluginFunctions = {
}
/**
+ * Collage object returned by the bootstrap function
+ *
* extractContextAsArrangement: Extracts the important information from a context, needed by a fragment from its
* arrangement
*/
@@ -62,7 +64,11 @@ export type Collage = {
}
/**
- * Enhances collage with additional features
+ * A Plugin is a function that enhances collage with additional features
+ *
+ * stands for Description
+ * stands for Context - before the enhancement
+ * stands for Enhanced Context - after the enhancement
*/
export type Plugin =
(previous: Collage) => Collage