{"id":15003574,"url":"https://github.com/zandaqo/compago","last_synced_at":"2025-10-30T11:30:29.266Z","repository":{"id":55384188,"uuid":"48704987","full_name":"zandaqo/compago","owner":"zandaqo","description":"A minimalist framework inspired by Domain-Driven Design for building applications using Web Components","archived":false,"fork":false,"pushed_at":"2023-06-04T04:24:06.000Z","size":1449,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-31T00:49:19.815Z","etag":null,"topics":["framework","javascript","lit","observable","state","state-management","webcomponents"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zandaqo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-12-28T17:52:27.000Z","updated_at":"2024-10-27T19:40:30.000Z","dependencies_parsed_at":"2024-10-12T07:41:22.904Z","dependency_job_id":"d98e0c69-7f8c-4bc3-be9d-7ef91cc362c3","html_url":"https://github.com/zandaqo/compago","commit_stats":{"total_commits":213,"total_committers":1,"mean_commits":213.0,"dds":0.0,"last_synced_commit":"fa52a846976c3e6e113ef1347db72b2b2cad97c6"},"previous_names":[],"tags_count":50,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zandaqo%2Fcompago","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zandaqo%2Fcompago/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zandaqo%2Fcompago/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zandaqo%2Fcompago/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zandaqo","download_url":"https://codeload.github.com/zandaqo/compago/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238955740,"owners_count":19558453,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","javascript","lit","observable","state","state-management","webcomponents"],"created_at":"2024-09-24T18:59:03.035Z","updated_at":"2025-10-30T11:30:28.933Z","avatar_url":"https://github.com/zandaqo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Compago\n\n[![Actions Status](https://github.com/zandaqo/compago/workflows/ci/badge.svg)](https://github.com/zandaqo/compago/actions)\n[![npm](https://img.shields.io/npm/v/compago.svg?style=flat-square)](https://www.npmjs.com/package/compago)\n\nCompago is a minimalist framework inspired by Domain-Driven Design for building\napplications using Web Components.\n\nAlthough most components are isomorphic and can be used independently, Compago\nworks best with [Lit](https://lit.dev) for which it offers extensions focused on\nsimplifying advanced state management in web components.\n\n- [Installation](#installation)\n- [State Management with Observables](#state-management-with-observables)\n  - [Quick Example](#quick-example)\n  - [Observables](#observables)\n  - [Using with Lit](#using-with-lit)\n  - [Data Binding](#data-binding)\n  - [Shared State](#shared-state)\n- [State Persistance with Repositories](#state-persistance-with-repositories)\n\n## Installation\n\nNode.js:\n\n```bash\nnpm i compago\n```\n\n```js\nimport { ... } from \"compago\";\n```\n\nComponents can also be imported separately, to avoid loading extra dependencies,\nfor example:\n\n```js\nimport { Result } from \"compago/result\";\nimport { Observable } from \"compago/observable\";\n```\n\nDeno:\n\n```js\nimport { ... } from \"https://raw.githubusercontent.com/zandaqo/compago/master/mod.ts\"\n```\n\n## State Management with Observables\n\nUI frameworks like React and Lit provide built-in mechanisms to deal with simple\nUI state expressed as primitive values such as `useState` hooks in React or\nLit's reactive properties. However, those mechanisms are cumbersome to work with\nwhen dealing with a complex domain state that involves nested objects and\narrays. To give reactivity to complex domain objects, Compago introduces the\n`Observable` class. Observable wraps a given domain object into a proxy that\nreacts to changes on the object and all its nested structures with a `change`\nevent. When used in Lit elements, additional helpers like `@observer`,\n`@observe` decorators and `bond` directive make reactivity and data binding\nseamless.\n\n### Quick Example\n\n```typescript\nimport { html, LitElement } from \"lit\";\nimport { bond, Observable, observe, observer } from \"compago\";\n\nclass Todo {\n  description = \"\";\n  done = false;\n}\n\n@observer()\nclass TodoItem extends LitElement {\n  // Create an observable of a Todo object tied to the element.\n  // The observable will be treated as internal state and updates within the observable\n  // (and all nested objects) will be propagated\n  // through the usual lifecycle of reactive properties.\n  @observe()\n  state = new Observable(new Todo());\n\n  // You can hook into updates to the observable properties just like reactive ones.\n  // Names for the nested properties are given as a path, e.g. `state.done`, `state.description`\n  protected updated(changedProperties: Map\u003cstring, unknown\u003e) {\n    // check if the description of the todo has changed\n    if (changedProperties.has(\"state.description\")) {\n      console.log(\"Todo description has changed!\");\n    }\n  }\n\n  render() {\n    return html`\n      \u003cdiv\u003e\n        \u003cinput .value=${this.state.description}\n          \u003c-- Bind the input value to the 'description' property of our observable --\u003e\n          @input=${bond({ to: this.state, key: \"description\" })} /\u003e\n        \u003cinput type=\"checkbox\" ?checked=${this.state.done}\n          \u003c-- Bind the 'checked' attribute of the input to the 'done' property of our observable --\u003e\n          @click=${\n      bond({ to: this.state, key: \"done\", attirubute: \"checked\" })\n    } /\u003e\n      \u003c/div\u003e\n    `;\n  }\n}\n```\n\n### Observables\n\n`Observable` makes monitoring changes on JavaScript objects as seamless as\npossible using the built-in Proxy and EventTarget interfaces. Observable wraps a\ngiven object into a proxy that emits `change` events whenever a change happens\nto the object or its \"nested\" objects and arrays. Since `Observable` is an\nextension of DOM's EventTarget, listening to changes is done through the\nstandard event handling mechanisms.\n\n```ts\nimport { Observable } from \"compago\";\n\nclass Todo {\n  description = \"...\";\n  done = false;\n}\n\nconst todo = new Observable(new Todo());\n\ntodo.addEventListener(\"change\", (event) =\u003e {\n  console.log(event);\n});\n\ntodo.done;\n//=\u003e false\n\ntodo.done = true;\n//=\u003e ChangeEvent {\n//=\u003e  type: 'change',\n//=\u003e  kind: 'SET'\n//=\u003e  path: '.done'\n//=\u003e  previous: false,\n//=\u003e  defaultPrevented: false,\n//=\u003e  cancelable: false,\n//=\u003e  timeStamp: ...\n//=\u003e}\n//=\u003e true\n\ntodo.done;\n//=\u003e true\n\nJSON.stringify(todo);\n//=\u003e { \"description\": \"...\", \"done\": true }\n```\n\nObservable only monitors own, public, enumerable, non-symbolic properties, thus,\nany other sort of properties (i.e. private, symbolic, or non-enumerable) can be\nused to manipulate data without triggering `change` events, e.g. to store\ncomputed properties.\n\n### Using with Lit\n\nObservables complement Lit element's reactive properties allowing separation of\ncomplex domain state (held in observables) and UI state (held in properties). To\nsimplify working with observables, Compago offers `ObserverElement` mixin or\n`observer` and `observe` decorators that handle attaching and detaching from\nobservables and triggering LitElement updates when observables detect changes.\n\n```ts\nimport { observer, observe } from 'compago';\n\nclass Comment {\n  id = 0;\n  text = '';\n  meta = {\n    author: '';\n    date: new Date();\n  }\n}\n\n@observer()\n@customElement('comment-element');\nclass CommentElement extends LitElement {\n  // Set up `comment` property to hold a reference to an observable\n  // holding a domain state. Any change to the comment will trigger\n  // the elements update cycle.\n  @observe() comment = new Observable(new Comment()); \n  @property({ type: Boolean }) isEditing = false;\n  ...\n\n  updated(changedProperties: PropertyValues\u003cany\u003e): void {\n    if (changedProperties.has('comment.text')) {\n      // react if the comment's text property has changed\n    }\n  }\n}\n```\n\nWhile decorators are quite popular in the Lit world, you can achieve the same\neffect without them by using `ObserverMixin` and specifying list of observables\nin a static property:\n\n```ts\nconst CommentElement = ObserverMixin(class extends LitElement {\n  static observables = ['comment'];\n  ...\n  comment = new Observable(new Comment()); \n  ...\n})\n```\n\n### Data Binding\n\nWe often have to take values from DOM elements (e.g. input fields) and update\nour UI or domain states with them. To simplify the process, Compago offers the\n`bond` directive. The directive provides a declarative way to define an event\nlistener that (upon being triggered) takes the specified value from its DOM\nelement, optionally validates and parses it, and sets it on desired object, be\nit the element itself or an observable domain state object.\n\n```ts\nclass CommentElement extends LitElement {\n  // Set up `comment` property to hold a reference to an observable\n  // holding a domain state. Any change to the comment will trigger\n  // the elements update cycle.\n  @observe()\n  comment = new Observable(new Comment());\n  ...\n  render() {\n    return html`\n      \u003cdiv\u003e\n        \u003cinput .value=${this.comment.text}\n          \u003c-- Upon input event take the input's value, validate it\n          and set as the text of our comment --\u003e\n          @input=${\n            bond({\n              to: this.comment,\n              key: \"text\",\n              validate: (text) =\u003e text.length \u003c 3000,\n            })\n          } /\u003e\n      \u003c/div\u003e\n    `;\n  }\n}\n```\n\n### Shared State\n\nObservable are fully independent and can be shared between components. For\nexample, a parent can share \"parts\" of an observable with children as shown in\nour example [todo app]:\n\n```ts\n@customElement(\"todo-app\")\n@observer()\nexport class TodoApp extends LitElement {\n  // Here we create an observable that holds an array of todo objects.\n  @observe()\n  state = new Observable({ items: [] as Array\u003cTodo\u003e });\n  ...\n  render() {\n    return html`\n      \u003ch1\u003eTodos Compago \u0026 Lit\u003c/h1\u003e\n      \u003csection\u003e\n          ${\n      repeat(this.state.items, (todo) =\u003e\n        todo.id, (todo) =\u003e\n        // We supply each todo object to a child element.\n        // Now when any of the todo objects is changed, it will trigger\n        // re-render of the todo-item and the parent todo-app,\n        // but not the sibling todo-items.\n        html`\u003ctodo-item .todo=${todo}\u003e\u003c/todo-item\u003e`)\n    }\n      \u003c/section\u003e\n  `;\n  }\n}\n\n@observer()\n@customElement(\"todo-item\")\nexport class TodoItem extends LitElement {\n  @observe()\n  todo!: Todo;\n\n  render() {\n    return html`\n      \u003cinput type=\"checkbox\" .checked=${this.todo.done} @click=${\n      bond({\n        to: this.todo,\n        key: \"done\",\n        property: \"checked\",\n      })\n    }\u003e\n      \u003clabel\u003e${this.todo.text}\u003c/label\u003e\n      \u003cbutton @click=${this.onRemove}\u003ex\u003c/button\u003e\n    `;\n  }\n}\n```\n\nObservables can be shared not only between parents and children, but also\nbetween siblings, or the same observable can be shared by different components,\nin fact, you can use a single observable to hold all state in your app (as a\n\"global store\").\n\n## State Persistance with Repositories\n\nA repository is a design pattern of data-centric, domain-driven design that\nencapsulates the logic for accessing data sources while abstracting the\nunderlying persistence technology. Web applications exchange data with a variety\nof data sources, such as servers exposed through a RESTful API, local storage\nusing IndexedDB API, or databases through their respective drivers. Compago\noffers a unifying interface for repositories and implementations for RESTful\nAPIs (`RESTRepository`) and local storage with IndexedDB (`LocalRepository`).\n\n```typescript\nimport { RESTRepository } from 'compago';\n\nclass Comment {\n  id = 0;\n  text = '';\n  meta = {\n    author: '';\n    date: new Date();\n  }\n}\n\n// Create a repository for comments that will access the API end point `/comments`\nconst remoteRepository = new RESTRepository(Comment, '/comments', 'id');\n\n// retrieve the comment with id 1\n// by sending a GET request to `/comments/1` API end-point\nconst readResult = await remoteRepository.read(1);\n\nreadResult.ok\n//=\u003e true if successful\n\nconst comment = readResult.value\n//=\u003e Comment {...}\n```\n\n## License\n\nMIT @ Maga D. Zandaqo\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzandaqo%2Fcompago","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzandaqo%2Fcompago","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzandaqo%2Fcompago/lists"}