npm package: @motiv/hedron
Description: A simple micro-frontend container.
Author: Motiv Studio Ltd.
Latest version: 0.1.0

# Hedron.js

### Version: 0.1.0

Hedron.js is a simple micro-frontend container. It supports dependency injection, event handling, filter hooks, modules, and plugins.

Hedron is dependency-free and has a small footprint of only 7.03KB (~2.04KB gzipped).

---

## Sections

- [Installation](#installation)
- [Features](#features)
  - [Dependency Injection](#dependency-injection)
    - [register()](#register-service)
    - [get()](#get-service)
  - [Events](#events)
    - [on()](#attach-event-handler)
    - [emit()](#emit-event)
    - [off()](#remove-event-handler)
  - [Filters](#filters)
    - [use()](#register-hook-to-filter)
    - [apply()](#apply-filter)
    - [remove()](#remove-hook-from-filter)
  - [Modules](#modules)
    - [install()](#install-module)
    - [uninstall()](#uninstall-module)
- [Plugins](#extending-hedron-with-plugins)
- [Miscellaneous](#miscellaneous)
- [Real-World Application](#real-world-application)

---

## Installation

### Install via npm

```bash
$ npm i @motiv/hedron
```

### Instantiate new Hedron instance

```javascript
import Hedron from '@motiv/hedron'

const app = new Hedron()
```

---

# Features

## Dependency Injection

Every Hedron instance has its own dependency injection container. Registered services are lazy-loaded when retrieved for the first time.

### Register Service

Services can be registered with the `hedron#register` method by specifying a unique service name and the service factory function.

```javascript
app.register('my-service', serviceBuilder)
```

Service dependencies can be specified by their service name as additional arguments, in the order they are passed into the factory function.

```javascript
// Service 'a' has no dependencies
app.register('a', buildA)

// Service 'a' is a dependency of service 'b'
app.register('b', buildB, 'a')

// Services 'a' and 'b' are dependencies of service 'c'
app.register('c', buildC, 'a', 'b')
```

#### Service Factory

A service factory function is a function that accepts dependencies as arguments and returns a service instance.

```javascript
const buildService = function (/* dependencies */) {
  return {
    /* ... */
  }
}
```

Any required dependencies of the service must be passed in as arguments of the factory function.

```javascript
const buildA = function () {
  return 'A'
}

const buildB = function (a) {
  return { a }
}

const buildC = function (a, b) {
  return { a, b }
}
```

### Get Service

Services can be retrieved from the container using `hedron#get`, passing in the service name as the first argument.

```javascript
const c = app.get('c') // { a: "A", b: { a: "A" } }
```

_Service Lazy-Loading_

When a service is retrieved for the first time, Hedron will recursively trace and build its dependencies, and finally build the requested service using their respective factory functions. The service instance is cached and returned directly on subsequent requests.

If required, you can specify Hedron to rebuild the service, and all of its dependencies, by setting the `rebuild` parameter in the second argument of `hedron#get`.

```javascript
// This will re-build service 'c' and all its dependencies.
const c = app.get('c', true)
```

---

## Events

Hedron features custom event handling with support for namespacing event handlers.

### Attach Event Handler

Attach an event listener using `hedron#on` by specifying the event name(s) as the first argument, and handler function as the second argument.

```javascript
app.on('an-event', myEventHandler)
```

Event handlers can be namespaced by adding a namespace preceded by a "`.`" after the event name.

```javascript
app.on('an-event.some-namespace', myEventHandler)
```

Multiple events can be specified at once by separating each event name with a space. Namespaced and non-namespaced events can be specified together.

```javascript
app.on('an-event.some-namespace another-event', myEventHandler)
```

### Emit Event

Events can be emitted using `hedron#emit` by passing in the event name(s) as the first argument.

```javascript
app.emit('an-event')
```

Optionally, an event object can be passed as a second argument.

```javascript
app.emit('an-event', new CustomEvent('an-event'))
```

If a namespace is specified, only handlers attached with the matching namespace will be called.

```javascript
app.emit('an-event.ns1')
```

Multiple events can be emitted simultaneously by specifying event names separated by a space.

```javascript
app.emit('event-1.ns1 event-2')
```

### Remove Event Handler

To remove an event handler, use `hedron#off` by passing in the event name(s) as the first argument, and handler function as the second argument.

```javascript
app.off('an-event', handler)
```

_Note:_ If a handler function was initially binded under a namespace, it must be specified.

```javascript
app.on('an-event.ns1', handler)

app.off('an-event', handler) // This will NOT unbind the handler.

app.off('an-event.ns1', handler) // This will unbind the handler.
```

An event handler can be unbinded from multiple events at once by specifying all event names, separated by a space.

```javascript
app.off('event-1.my-ns event-2', handler)
```

---

## Filters

The filter hook mechanism allows for cross-module "enrichment" of some arbitrary data payload before it is returned to and processed by the initial module that applied the filter. While event handlers support non-sequential function calls, filters are called sequentially, in order of priority.

### Register Hook to Filter

To register a hook to a filter, use `hedron#use`.

`hedron#use` accepts 3 arguments: the filter name, hook function, and priority value. Unlike events, filters cannot be namespaced.

```javascript
const myFilterHook = (payload) => {
  return {
    ...payload,
    myFilterHook: true
  }
}

app.use('enrich-payload', myFilterHook, 50)
```

**Hook Priority**

When a filter is applied, the registered handlers are executed in ascending order of priority (1, 2, ,3, ... n). Handlers registered with the same priority will be applied in the order they were registered. Handlers registered without specifying an explicit priority will be called last.

### Apply Filter

To apply a filter, use `hedron#apply` and pass an initial data payload as a second argument.

```javascript
const result = app.apply('enrich-data', {
  /* initial data value */
})
```

### Remove Hook from Filter

To remove a hook, use `hedron#remove`. If the hook was registered with an explicit priority, it must also be specified.

```javascript
app.remove('enrich-payload', myFilterHook, 50)
```

---

## Modules

As an application becomes larger and more complex, modules provide a logical way to group method calls together. Loosely-coupled modules are key to building large-scale micro-frontend web applications ([Real-World Application](#real-world-application)).

### Writing a Module

Each module must expose both an `install` and `uninstall` method, which are called during installation and uninstallation respectively. During calls to install/uninstall, the Hedron instance is passed as the first argument, allowing the module to access the instance methods.

```javascript
const myModule = {
  install: (app) => {
    app.on('some-event.my-module', myHandler)
    app.use('some-filter', myHook, 100)
  },
  uninstall: (app) => {
    app.off('some-event.my-module', myHandler)
    app.remove('some-filter', myHook, 100)
  }
  /* additional properties */
}
```

### Install Module

To install a module, use `hedron#install`, passing in 2 arguments: a unique module name, and the module object.

```javascript
app.install('my-module', myModule)
```

### Uninstall Module

To uninstall a module, use `hedron#uninstall`, specifying the name of the module.

```javascript
app.uninstall('my-module')
```

---

## Extending Hedron with Plugins

The global Hedron class can be extended via a straightforward plugin interface.

### Using a Plugin

Plugins can be installed using `Hedron#plugin`.

```javascript
Hedron.plugin('my-plugin', myPlugin)
```

### Writing a Plugin

A valid plugin must include an `install` method, which takes the global `Hedron` class as the first argument. Plugins can extend the Hedron class through its prototype object.

```javascript
const myPlugin = {
  install: (Hedron) => {
    Hedron.prototype.$greeting = 'Hello there.'
  }
  /* additional properties */
}

Hedron.plugin('my-plugin', myPlugin)
```

New methods and properties will be available on subsequent Hedron instances initialized.

```javascript
const app = new Hedron()

console.log(app.$greeting) // "Hello there."
```

---

## Miscellaneous

### Chaining Methods

With the obvious exception of `hedron#get` and `hedron#apply`, all methods return the Hedron instance (`this`). This allows methods to be chained.

```javascript
app
  .register('my-service', buildMyService)
  .on('some-event', myEventHandler)
  .use('filter-data', myFilterHook)
  .install('module-a', moduleA)
```

---

## Real-World Application

[Micro Frontends - extending the microservice idea to frontend development](https://micro-frontends.org/)

[Micro Frontends](https://martinfowler.com/articles/micro-frontends.html)

**Assumptions**

1. Modules are built and managed by separate teams.

2. Each module is its own collection of JS files.

**Lifecycle**

1. Module: Instantiate and assign module to a globally accessible container (i.e. `window['hedron_modules']`)

2. Main: Instantiate new Hedron app

3. Main: Install all modules

To be continued...

---

## License

[MIT](#https://opensource.org/licenses/MIT)

Copyright (c) 2020 Motiv Studio Ltd.