https://github.com/zandaqo/compago
A minimalist framework inspired by Domain-Driven Design for building applications using Web Components
https://github.com/zandaqo/compago
framework javascript lit observable state state-management webcomponents
Last synced: 8 months ago
JSON representation
A minimalist framework inspired by Domain-Driven Design for building applications using Web Components
- Host: GitHub
- URL: https://github.com/zandaqo/compago
- Owner: zandaqo
- License: mit
- Created: 2015-12-28T17:52:27.000Z (almost 10 years ago)
- Default Branch: master
- Last Pushed: 2023-06-04T04:24:06.000Z (over 2 years ago)
- Last Synced: 2025-01-31T00:49:19.815Z (8 months ago)
- Topics: framework, javascript, lit, observable, state, state-management, webcomponents
- Language: TypeScript
- Homepage:
- Size: 1.38 MB
- Stars: 15
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Compago
[](https://github.com/zandaqo/compago/actions)
[](https://www.npmjs.com/package/compago)Compago is a minimalist framework inspired by Domain-Driven Design for building
applications using Web Components.Although most components are isomorphic and can be used independently, Compago
works best with [Lit](https://lit.dev) for which it offers extensions focused on
simplifying advanced state management in web components.- [Installation](#installation)
- [State Management with Observables](#state-management-with-observables)
- [Quick Example](#quick-example)
- [Observables](#observables)
- [Using with Lit](#using-with-lit)
- [Data Binding](#data-binding)
- [Shared State](#shared-state)
- [State Persistance with Repositories](#state-persistance-with-repositories)## Installation
Node.js:
```bash
npm i compago
``````js
import { ... } from "compago";
```Components can also be imported separately, to avoid loading extra dependencies,
for example:```js
import { Result } from "compago/result";
import { Observable } from "compago/observable";
```Deno:
```js
import { ... } from "https://raw.githubusercontent.com/zandaqo/compago/master/mod.ts"
```## State Management with Observables
UI frameworks like React and Lit provide built-in mechanisms to deal with simple
UI state expressed as primitive values such as `useState` hooks in React or
Lit's reactive properties. However, those mechanisms are cumbersome to work with
when dealing with a complex domain state that involves nested objects and
arrays. To give reactivity to complex domain objects, Compago introduces the
`Observable` class. Observable wraps a given domain object into a proxy that
reacts to changes on the object and all its nested structures with a `change`
event. When used in Lit elements, additional helpers like `@observer`,
`@observe` decorators and `bond` directive make reactivity and data binding
seamless.### Quick Example
```typescript
import { html, LitElement } from "lit";
import { bond, Observable, observe, observer } from "compago";class Todo {
description = "";
done = false;
}@observer()
class TodoItem extends LitElement {
// Create an observable of a Todo object tied to the element.
// The observable will be treated as internal state and updates within the observable
// (and all nested objects) will be propagated
// through the usual lifecycle of reactive properties.
@observe()
state = new Observable(new Todo());// You can hook into updates to the observable properties just like reactive ones.
// Names for the nested properties are given as a path, e.g. `state.done`, `state.description`
protected updated(changedProperties: Map) {
// check if the description of the todo has changed
if (changedProperties.has("state.description")) {
console.log("Todo description has changed!");
}
}render() {
return html`
@input=${bond({ to: this.state, key: "description" })} />
@click=${
bond({ to: this.state, key: "done", attirubute: "checked" })
} />
`;
}
}
```### Observables
`Observable` makes monitoring changes on JavaScript objects as seamless as
possible using the built-in Proxy and EventTarget interfaces. Observable wraps a
given object into a proxy that emits `change` events whenever a change happens
to the object or its "nested" objects and arrays. Since `Observable` is an
extension of DOM's EventTarget, listening to changes is done through the
standard event handling mechanisms.```ts
import { Observable } from "compago";class Todo {
description = "...";
done = false;
}const todo = new Observable(new Todo());
todo.addEventListener("change", (event) => {
console.log(event);
});todo.done;
//=> falsetodo.done = true;
//=> ChangeEvent {
//=> type: 'change',
//=> kind: 'SET'
//=> path: '.done'
//=> previous: false,
//=> defaultPrevented: false,
//=> cancelable: false,
//=> timeStamp: ...
//=>}
//=> truetodo.done;
//=> trueJSON.stringify(todo);
//=> { "description": "...", "done": true }
```Observable only monitors own, public, enumerable, non-symbolic properties, thus,
any other sort of properties (i.e. private, symbolic, or non-enumerable) can be
used to manipulate data without triggering `change` events, e.g. to store
computed properties.### Using with Lit
Observables complement Lit element's reactive properties allowing separation of
complex domain state (held in observables) and UI state (held in properties). To
simplify working with observables, Compago offers `ObserverElement` mixin or
`observer` and `observe` decorators that handle attaching and detaching from
observables and triggering LitElement updates when observables detect changes.```ts
import { observer, observe } from 'compago';class Comment {
id = 0;
text = '';
meta = {
author: '';
date: new Date();
}
}@observer()
@customElement('comment-element');
class CommentElement extends LitElement {
// Set up `comment` property to hold a reference to an observable
// holding a domain state. Any change to the comment will trigger
// the elements update cycle.
@observe() comment = new Observable(new Comment());
@property({ type: Boolean }) isEditing = false;
...updated(changedProperties: PropertyValues): void {
if (changedProperties.has('comment.text')) {
// react if the comment's text property has changed
}
}
}
```While decorators are quite popular in the Lit world, you can achieve the same
effect without them by using `ObserverMixin` and specifying list of observables
in a static property:```ts
const CommentElement = ObserverMixin(class extends LitElement {
static observables = ['comment'];
...
comment = new Observable(new Comment());
...
})
```### Data Binding
We often have to take values from DOM elements (e.g. input fields) and update
our UI or domain states with them. To simplify the process, Compago offers the
`bond` directive. The directive provides a declarative way to define an event
listener that (upon being triggered) takes the specified value from its DOM
element, optionally validates and parses it, and sets it on desired object, be
it the element itself or an observable domain state object.```ts
class CommentElement extends LitElement {
// Set up `comment` property to hold a reference to an observable
// holding a domain state. Any change to the comment will trigger
// the elements update cycle.
@observe()
comment = new Observable(new Comment());
...
render() {
return html`
@input=${
bond({
to: this.comment,
key: "text",
validate: (text) => text.length < 3000,
})
} />
`;
}
}
```### Shared State
Observable are fully independent and can be shared between components. For
example, a parent can share "parts" of an observable with children as shown in
our example [todo app]:```ts
@customElement("todo-app")
@observer()
export class TodoApp extends LitElement {
// Here we create an observable that holds an array of todo objects.
@observe()
state = new Observable({ items: [] as Array });
...
render() {
return html`
Todos Compago & Lit
${
repeat(this.state.items, (todo) =>
todo.id, (todo) =>
// We supply each todo object to a child element.
// Now when any of the todo objects is changed, it will trigger
// re-render of the todo-item and the parent todo-app,
// but not the sibling todo-items.
html``)
}
`;
}
}@observer()
@customElement("todo-item")
export class TodoItem extends LitElement {
@observe()
todo!: Todo;render() {
return html`
${this.todo.text}
x
`;
}
}
```Observables can be shared not only between parents and children, but also
between siblings, or the same observable can be shared by different components,
in fact, you can use a single observable to hold all state in your app (as a
"global store").## State Persistance with Repositories
A repository is a design pattern of data-centric, domain-driven design that
encapsulates the logic for accessing data sources while abstracting the
underlying persistence technology. Web applications exchange data with a variety
of data sources, such as servers exposed through a RESTful API, local storage
using IndexedDB API, or databases through their respective drivers. Compago
offers a unifying interface for repositories and implementations for RESTful
APIs (`RESTRepository`) and local storage with IndexedDB (`LocalRepository`).```typescript
import { RESTRepository } from 'compago';class Comment {
id = 0;
text = '';
meta = {
author: '';
date: new Date();
}
}// Create a repository for comments that will access the API end point `/comments`
const remoteRepository = new RESTRepository(Comment, '/comments', 'id');// retrieve the comment with id 1
// by sending a GET request to `/comments/1` API end-point
const readResult = await remoteRepository.read(1);readResult.ok
//=> true if successfulconst comment = readResult.value
//=> Comment {...}
```## License
MIT @ Maga D. Zandaqo