Compose
A composable application is a pseudo-HTML document enhanced with JavaScript properties dynamically injected by the composer application.
The resulting DOM tree is constructed on the basis of a specific configuration, which can be directly provided, or sourced from an external JSON or YAML file.
Usage
Declare an application with integration mode compose
in micro-lc configuration:
interface ComposableApplication {
integrationMode: "compose"
config: PluginConfiguration | string // See explanation below
route: string // Path on which the composable application will be rendered
options?: ComposableApplicationOptions // See explanation below
}
The application configuration has to be supplied with the config
key, which may be either a full
configuration object or a URL string from which a configuration with the same structure can
be downloaded.
Plugin configuration
The configuration of a composable application is the blueprint used by micro-lc composer (being it the default one or a custom implementation) to dynamically construct the page at runtime.
interface PluginConfiguration {
$schema: string
sources?:
| string
| string[]
| {
uris: string | string[]
importmap?: ImportMap
}
content: Content
}
Key $schema
can be used to reference micro-lc plugin configuration
JSON schema to greatly ease
the writing process by constantly validating the JSON or YAML content against it.
The actual page structure is provided in content
key, and building blocks are HTML5 elements or custom web components.
In the letter case, sources have to be provided for custom components, and one can do so with the sources
key.
By polymorphism, sources
can be a string or an array of strings if just JavaScript asset entries have to be provided.
If an importmap is needed, sources
can become an object housing JavaScript asset
entry URIs (key uris
) and importmap definition (key importmap
).
applications:
# Single JavaScript asset entry URI
app-1:
sources: https://my-static-server/my-web-component.js
content: ...
# Multiple JavaScript asset entry URIs
app-2:
sources:
- https://my-static-server/my-web-component-1.js
- https://my-static-server/my-web-component-2.js
content: ...
# Importmap
app-3:
sources:
uris: https://my-static-server/my-web-component.js
importmap:
imports: ...
scopes: ...
content: ...
Content definition
A composable application content is a representation of a pseudo-DOM tree written in a markup language (namely JSON or YAML) that undergoes a series of processes to be transformed into a valid, appendable DOM.
type Content = string | number | Component | (Component | number | string)[]
A valid content can assume different shapes, as long as it is a valid HTML element or a convertible representation of one. It may be:
- a primitive (
string
ornumber
)content: "A string is a valid HTML element!"
- a stringified DOM tree#stringified-dom-tree, particularly powerful when used in YAML files, since it can benefit
from YAML block scalars to greatly enhance readability
content: |
<div .classname=${"my-class"} .microlcApi=${microlcApi}>
<p style="color: red;">
This is written as a single string
</p>
</div> - a single component representation
content:
tag: div
attributes:
style: "color: red;"
properties:
classname: my-class
content: This structure is transformed into a valid HTML element - a list of the above
content:
- "String element"
- 12
- tag: div
Component representation
A component corresponds to an HTML node, being it an HTML5 element or a custom web component. Practically speaking, a component is an object with the following structure:
interface Component {
/** HTML node tag name */
tag: string
/** HTML5 attribute applied using setAttribute API */
attributes?: Record<string, string>
/** HTML5 boolean attribute applied using setAttribute API */
booleanAttributes?: string | string[]
/** DOM element property applied as object property after creating an element */
properties?: Record<string, unknown>
/** Node children */
content?: Content
}
The type is recursive as content
is a content definition which may itself take the form of a
Component
.
content:
tag: button
attributes:
style: "color: red;"
booleanAttributes: disabled
content: Click me!
# Output: <button disabled style="color: red;">Click me!</button>
---
content:
tag: my-component
attributes:
class: my-class
my-numeric-attribute: 2
properties:
myCustomProperty: some-value
content:
tag: span
content: Hello World!
# Output: 👇
# <my-component class="my-class" my-numeric-attribute="2">
# <span>Hello World!</span>
# </my-component>
#
# document.querySelector("my-component").myCustomProperty 👉 "some-value"
Properties injection
When composing a content, the constructed nodes can receive two types of properties:
- user-supplied properties explicitly declared in configuration, and
- a set of special properties interpolated and injected directly by micro-lc composer.
User-supplied properties
User-supplied properties can be declared using the properties
property of
component interface, or through a special dotted notation
(.property_name=${property_value}
) if relying on the stringified DOM tree representation. Either case, any
valid JSON value is acceptable as property and injected into components context as is.
- Objective representation
- Stringified representation
content:
tag: my-component
properties:
stringProp: foo
numberProp: 3
arrayProp:
- foo
- bar
objectProp:
foo: bar
# myComponent.stringProp 👉 Output: "foo"
# myComponent.numberProp 👉 Output: 3
# myComponent.arrayProp 👉 Output: ["foo", "bar"]
# myComponent.objectProp 👉 Output: {foo: "bar"}
layout:
content: |
<my-component
.stringProp=${"foo"}
.numberProp=${3}
.arrayProp=${["foo", "bar"]}
.objectProp=${{"foo": "bar"}}
></my-component>
# myComponent.stringProp 👉 Output: "foo"
# myComponent.numberProp 👉 Output: 3
# myComponent.arrayProp 👉 Output: ["foo", "bar"]
# myComponent.objectProp 👉 Output: {foo: "bar"}
Interpolated properties
micro-lc injects a series of special properties into each DOM node it creates. These properties are
automatically interpolated, and therefore they need to be marked by reserved keywords for micro-lc to
recognize them and assign them the correct value (always in a secure manner without eval
or similar structures).
When using object component representation, interpolated properties do not need to be explicitly declared. However, if they are, the key used must match the reserved one, and the value must be equal to the key. On the other hand, when using stringified DOM tree representation, properties you want to be injected need to be explicitly declared with the correct key and value.
For example, let's consider the special property microlcApi
and different scenarios.
- Objective representation
- Stringified representation
content:
tag: my-component
# myComponent.microlcApi is defined and correctly set
---
content:
tag: my-component
properties:
stringProp: foo
# myComponent.microlcApi is defined and correctly set
---
content:
tag: my-component
properties:
microlcApi: microlcApi
# myComponent.microlcApi is defined and correctly set
---
content:
tag: my-component
properties:
microlcApi: foo
# myComponent.microlcApi is undefined
layout:
content: |
<my-component></my-component>
# myComponent.microlcApi is undefined
---
layout:
content: |
<my-component .microlcApi=${microlcApi}></my-component>
# myComponent.microlcApi is defined and correctly set
---
layout:
content: |
<my-component .microlcApi=${foo}></my-component>
# myComponent.microlcApi is undefined
The special properties injected by micro-lc are the following.
microlcApi
- Type:
Object
Common API offered by micro-lc as mean of communication.
composerApi
- Type:
Object
Common API offered by micro-lc composer to achieve composition.
eventBus
Composed layouts and mount points do not have access to this property.
- Type
interface EventBus<T = unknown> extends rxjs.ReplaySubject<T> {
[index: number]: rxjs.ReplaySubject<T>
pool: Record<string, rxjs.ReplaySubject<T>>
}
RxJS ReplaySubject useful to establish a reactive communication between components of the same application.
The property gives component the ability to spawn multiple ReplaySubjects, allowing multichannel communication.
eventBus
itself is a ReplaySubject, but calling eventBus[0]
or eventBus.pool.foo
will create two other –
completely different – ReplaySubject entities.
content:
tag: my-component
# myComponent.eventBus !== myComponent.eventBus[0] !== myComponent.eventBus.pool.foo
currentUser
This property will be removed in future versions. Use micro-lc API subscribe method instead.
- Type:
rxjs.Observable
RxJS Observable taken from micro-lc API Pub/Sub channel containing information on the current application user.
Shared properties
Content of properties
key of configuration key shared
. properties
key is spread and each of its property is injected independently.
Example:
- Objective representation
- Stringified representation
shared:
properties:
foo: bar
layout:
content:
tag: my-component
attributes:
id: my-div
# myComponent.foo 👉 Output: "bar"
shared:
properties:
foo: bar
layout:
content: |
<my-component id="my-div" .foo=${foo}></my-component>
# myComponent.foo 👉 Output: "bar"
Application options
Application options are available starting form micro-lc version 2.4.2
and @micro-lc/composer
version 2.2.0
A set of options can be passed along in the application configuration through the options
keyword. Options are an optional object with the following structure:
interface ComposableApplicationOptions {
fetchConfigOnMount?: boolean
}
fetchConfigOnMount
By default, micro-lc retrieves compose applications configurations just once, at bootstrap (i.e., the first time the user navigates to them). At any subsequent visit, configurations are retrieved from an in-memory cache during the mount stage of the application lifecycle.
If you need to re-fetch the configuration each time an application is visited, you can instruct micro-lc to do so by setting the fetchConfigOnMount
option to true
.
This option will have effect only on applications that specify a URL string in the config
key.