https://github.com/kennyfrc/cami.js Skip to content Toggle navigation Sign up * Product + Actions Automate any workflow + Packages Host and manage packages + Security Find and fix vulnerabilities + Codespaces Instant dev environments + Copilot Write better code with AI + Code review Manage code changes + Issues Plan and track work + Discussions Collaborate outside of code Explore + All features + Documentation + GitHub Skills + Blog * Solutions For + Enterprise + Teams + Startups + Education By Solution + CI/CD & Automation + DevOps + DevSecOps Resources + Learning Pathways + White papers, Ebooks, Webinars + Customer Stories + Partners * Open Source + GitHub Sponsors Fund open source developers + The ReadME Project GitHub community articles Repositories + Topics + Trending + Collections * Pricing Search or jump to... Search code, repositories, users, issues, pull requests... Search [ ] Clear Search syntax tips Provide feedback We read every piece of feedback, and take your input very seriously. [ ] [ ] Include my email address so I can be contacted Cancel Submit feedback Saved searches Use saved searches to filter your results more quickly Name [ ] Query [ ] To see all available qualifiers, see our documentation. Cancel Create saved search Sign in Sign up You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session. Dismiss alert {{ message }} kennyfrc / cami.js Public * Notifications * Fork 0 * Star 19 A minimalist & flexible toolkit for interactive islands & state management in hypermedia-driven web applications. License MIT license 19 stars 0 forks Activity Star Notifications * Code * Issues 0 * Pull requests 0 * Discussions * Actions * Security * Insights More * Code * Issues * Pull requests * Discussions * Actions * Security * Insights kennyfrc/cami.js This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository. master Switch branches/tags [ ] Branches Tags Could not load branches Nothing to show {{ refName }} default View all branches Could not load tags Nothing to show {{ refName }} default View all tags Name already in use A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch? Cancel Create 1 branch 4 tags Code * Local * Codespaces * Clone HTTPS GitHub CLI [https://github.com/k] Use Git or checkout with SVN using the web URL. [gh repo clone kennyf] Work fast with our official CLI. Learn more about the CLI. * Open with GitHub Desktop * Download ZIP Sign In Required Please sign in to use Codespaces. Launching GitHub Desktop If nothing happens, download GitHub Desktop and try again. Launching GitHub Desktop If nothing happens, download GitHub Desktop and try again. Launching Xcode If nothing happens, download Xcode and try again. Launching Visual Studio Code Your codespace will open once ready. There was a problem preparing your codespace, please try again. Latest commit @kennyfrc kennyfrc add builds ... 4998b86 Nov 4, 2023 add builds 4998b86 Git stats * 65 commits Files Permalink Failed to load latest commit information. Type Name Latest commit message Commit time build add builds November 5, 2023 02:02 examples add builds November 5, 2023 02:02 src jsdocs update November 5, 2023 00:04 .gitignore add builds November 5, 2023 02:02 LICENSE Create LICENSE October 27, 2023 01:56 README.md tweak: readme November 5, 2023 01:29 build_examples.sh docs update November 4, 2023 23:57 build_readme.sh tweak: readme November 4, 2023 23:58 bun.lockb docs update November 4, 2023 23:57 gzip.js gzip script November 3, 2023 10:59 jsdoc.json jsdocs: generate /docs October 27, 2023 15:19 package.json docs update November 4, 2023 23:57 tsconfig.json v0.0.3 October 27, 2023 01:41 View code [ ] [?] Cami.js Motivation Key Features: Who is this for? Get Started & View Examples Key Concepts / API ReactiveElement Class, Observable Objects, and HTML Tagged Templates Basics of Observables & Templates Basics of Computed Properties & Effects createStore(initialState) html Examples Dev Usage Install Dependencies Building Typechecking Testing Prior Art Why "Cami"? Roadmap README.md [?] Cami.js [?][?] Expect API changes until v1.0.0 [?][?] Current version: 0.0.9. Bundle Size: 8kb minified & gzipped. A minimalist & flexible toolkit for interactive islands & state management in hypermedia-driven web applications. Motivation I wanted a minimalist javascript library that has no build steps, great debuggability, and didn't take over my front-end. My workflow is simple: I want to start any application with normal HTML/CSS, and if there were fragments or islands that needed to be interactive (such as dashboards & calculators), I needed a powerful enough library that I could easily drop in without rewriting my whole front-end. Unfortunately, the latter is the case for the majority of javascript libraries out there. That said, I like the idea of declarative templates, uni-directional data flow, time-travel debugging, and fine-grained reactivity. But I wanted no build steps (or at least make 'no build' the default). So I created Cami. Key Features: * Reactive Web Components: We suggest to start any web application with normal HTML/CSS, then add interactive islands with Cami's reactive web components. Uses fine-grained reactivity with observables, computed properties, and effects. Also supports for deeply nested updates. Uses the Light DOM instead of Shadow DOM. * Tagged Templates: Declarative templates with lit-html. Supports event handling, attribute binding, composability, caching, and expressions. * Store / State Management: When you have multiple islands, you can use a singleton store to share state between them, and it acts as a single source of truth for your application state. Redux DevTools compatible. * Easy Immutable Updates: Uses Immer under the hood, so you can update your state immutably without excessive boilerplate. * Anti-Features: You can't be everything to everybody. So we made some hard choices: No Build Steps, No Client-Side Router, No JSX, No Shadow DOM. We want you to build an MPA, with mainly HTML/CSS, and return HTML responses instead of JSON. Then add interactivity as needed. Who is this for? * Lean Teams or Solo Devs: If you're building a small to medium-sized application, I built Cami with that in mind. You can start with ReactiveElement, and once you need to share state between components, you can add our store. It's a great choice for rich data tables, dashboards, calculators, and other interactive islands. If you're working with large applications with large teams, you may want to consider other frameworks. * Developers of Multi-Page Applications: For folks who have an existing server-rendered application, you can use Cami to add interactivactivity to your application, along with other MPA-oriented libraries like HTMX, Unpoly, Turbo, or TwinSpark. Get Started & View Examples To see some examples, just do the following: git clone git@github.com:kennyfrc/cami.js.git cd cami.js bun install --global serve bunx serve Open http://localhost:3000 in your browser, then navigate to the examples folder. In the examples folder, you will find a series of examples that illustrate the key concepts of Cami.js. These examples are numbered & ordered by complexity. Key Concepts / API ReactiveElement Class, Observable Objects, and HTML Tagged Templates ReactiveElement is a class that extends HTMLElement to create reactive web components. These components can automatically update their view (the template) when their state changes. Automatic updates are done by observables. An observable is an object that can be observed for state changes, and when it changes, it triggers an effect (a function that runs in response to changes in observables). Cami's observables have the following characteristics: * It has a value property that holds the current value of the observable. * It has an update method that allows you to update the value of the observable. When you update the value of an observable, it will automatically trigger a re-render of the component's html tagged template. This is what makes the component reactive. Let's illustrate these three concepts with an example. Here's a simple counter component: In this example, CounterElement is a reactive web component that maintains an internal state (count) and updates its view whenever the state changes. Basics of Observables & Templates Creating an Observable: In the context of a ReactiveElement, you can create an observable using the this.observable() method. For example, to create an observable count with an initial value of 0, you would do: this.count = this.observable(0); Getting the Value: You can get the current value of an observable by accessing its value property. For example, you can get the value of count like this: this.count.value; Setting the Value: To update the value of an observable, you use the update method. This method accepts a function that receives the current value and returns the new value. For example, to increment count, you would do: this.count.update(value => value + 1); Deeply Nested Updates: An interesting thing to note (which many libaries don't support with one method/function) is that Cami's observables support deeply nested updates. This means that if your observable's value is an object, you can update deeply nested properties within that object. Example: let user = this.observable({ name: { first: 'John', last: 'Doe' } }); user.update(value => { value.name.first = 'Jane'; }); Here, only the first property of the name object is updated. The rest of the user object remains the same. Template Rendering: Cami.js uses lit-html for its template rendering. This allows you to write HTML templates in JavaScript using tagged template literals. Think of it as fancy string interpolation. Here's the example from above: template() { return html`

Count: ${this.count.value}

`; } In this example, html is a function that gets called with the template literal. It processes the template literal and creates a template instance that can be efficiently updated and rendered. The ${} syntax inside the template literal is used to embed JavaScript expressions. These expressions can be variables, properties, or even functions. In the example above, $ {this.count.value} will be replaced with the current value of the count observable, and ${() => this.count.update(value => value + 1)} is a function that increments the count when the button is clicked. The @click syntax is used to attach event listeners to elements. In this case, a click event listener is attached to the button element. Basics of Computed Properties & Effects Computed Properties: Computed properties are a powerful feature in Cami.js that allow you to create properties that are derived from other observables. These properties automatically update whenever their dependencies change. This is particularly useful for calculations that depend on one or more parts of the state. For instance, in the CounterElement example from _001_counter.html, a computed property countSquared is defined as the square of the count observable: this.countSquared = this.computed(() => this.count.value * this.count.value); In this case, countSquared will always hold the square of the current count value, and will automatically update whenever count changes. This is ideal for calculations like this, but can also be used for other derived values such as total price in a shopping cart (based on quantities and individual prices), or a boolean flag indicating if a form is valid (based on individual field validations). Effects: Effects in Cami.js are functions that run in response to changes in observable properties. They are a great way to handle side effects in your application, such as integrating with non-reactive components, emitting custom events, or logging/debugging. For example, in the CounterElement example, an effect is defined to log the current count and its square whenever either of them changes: this.effect(() => console.log(`Count: ${this.count.value} & Count Squared: ${this.countSquared.value}`)); This effect will run whenever count or countSquared changes, logging the new values to the console. This can be particularly useful for debugging. Effects can also be used to emit custom events after specific state changes. For instance, you could emit a custom event whenever the count reaches a certain value. Here's a great essay on this topic: Hypermedia-Friendly Scripting this.effect(() => { if (this.count.value === 10) { this.dispatchEvent(new CustomEvent('count-reached-ten', { detail: this.count.value })); } }); In this example, a 'count-reached-ten' event is dispatched whenever the count reaches 10. This can be useful for integrating with non-reactive parts of your application or for triggering specific actions in response to state changes like with ReactiveElement Methods: * observable(initialValue): Defines an observable property with an initial value. Returns an object with value property and update method. * subscribe(key, store): Subscribes to a store and links it to an observable property. Returns the observable. * computed(computeFn): Defines a computed property that depends on other observables. Returns an object with a value getter. * effect(effectFn): Defines an effect that is triggered when an observable changes. The effect function can optionally return a cleanup function. * dispatch(action, payload): Dispatches an action to the store. * template(): A method that should be implemented to return the template to be rendered. * connectedCallback(): Lifecycle method called each time the element is added to the document. Sets up initial state and triggers initial rendering. * disconnectedCallback(): Lifecycle method called each time the element is removed from the document. Cleans up listeners and effects. * adoptedCallback(): Lifecycle method called each time the element is moved to a new document. Can be used to reset or reinitialize internal state. * attributeChangedCallback(name, oldValue, newValue): Lifecycle method called when an attribute of the element is added, removed, or changed. Useful for reacting to changes in attributes. * static get observedAttributes(): Static getter that returns an array of attribute names to monitor for changes. Used in conjunction with attributeChangedCallback. Note: Lifecycle methods are part of the Light DOM. We do not implement the Shadow DOM in this library. While Shadow DOM provides style and markup encapsulation, there are drawbacks if we want this library to interoperate with other libs. createStore(initialState) The createStore function is a fundamental part of Cami.js. It creates a new store with the provided initial state. The store is a singleton, meaning that if it has already been created, the existing instance will be returned. This store is a central place where all the state of your application lives. It's like a data warehouse where different components of your application can communicate and share data. This concept is particularly useful in scenarios where multiple components need to share and manipulate the same state. A classic example of this is a shopping cart in an e-commerce application, where various components like product listing, cart summary, and checkout need access to the same cart state. The store follows a flavor of the Flux architecture, which promotes unidirectional data flow. The cycle goes as follows: dispatch an action -> update the store -> reflect changes in the view -> dispatch another action. In addition, as we adhere to many of Redux's principles, our store is compatible with the Redux DevTools Chrome extension, which allows for time-travel debugging. Parameters: * initialState (Object): The initial state of the store. This is the starting point of your application state and can be any valid JavaScript object. Returns: A store object with the following methods: * state: The current state of the store. It represents the current snapshot of your application state. * subscribe(listener): Adds a listener to the store. This listener is a function that gets called whenever the state changes. It also returns an unsubscribe function to stop listening to state changes. * register(action, reducer): Adds a reducer to the store. A reducer is a function that knows how to update the state based on an action. * dispatch(action, payload): Adds an action to the dispatch queue and starts processing if not already doing so. An action is a description of what happened, and the payload is the data associated with this action. * use(middleware): Adds a middleware to the store. Middleware is a way to extend the store's capabilities and handle asynchronous actions or side effects. html The html function in Cami.js is a tagged template literal, based on lit-html, that allows for the creation of declarative templates. It provides several powerful features that make it effective in the context of Cami.js: 1. Event Handling: It supports event handling with directives like @click, which can be used to bind DOM events to methods in your components. For example: html`` In this example, the increment method is called when the button is clicked. 2. Attribute Binding: It allows for attribute binding, which means you can dynamically set the attributes of your HTML elements based on your component's state. For example: html`
` In this example, the class of the div is dynamically set based on the isActive property of the component. 3. Composability: It supports composability, which means you can easily include one template inside another. For example: html`${this.headerTemplate()}
${this.contentTemplate()}
` In this example, headerTemplate and contentTemplate are methods that return other templates, which are included in the main template. 4. Caching: It caches templates, which means that if you render the same template multiple times, it will only update the parts of the DOM that have changed. This makes rendering more efficient. 5. Expressions: It allows for the use of JavaScript expressions inside your templates, which means you can include complex logic in your templates. For example: html`
${this.items.length > 0 ? this.renderItems() : 'No items'}
` In this example, the template conditionally renders a list of items or a message saying 'No items' based on the length of the items array. For more detailed information, refer to the lit-html documentation: docs Examples To see some examples, just do the following: git clone git@github.com:kennyfrc/cami.js.git cd cami.js bun install --global serve bunx serve Open http://localhost:3000 in your browser, then navigate to the examples folder. In the examples folder, you will find a series of examples that illustrate the key concepts of Cami.js. These examples are numbered & ordered by complexity. They are also listed below:

Counter

Form Validation

Todo List

Products

Cart

User Form (1 Layer Nested)

User Update Page (Nested Observable)

User Update Page (Nested Store)

Dev Usage Install Dependencies bun install Building bun run build:module bun run build:cdn Typechecking JSDoc is used for typechecking & documentation. bun run type-check Testing TBD Prior Art * Immer * Redux Why "Cami"? It's short for Camiguin, a pretty nice island. Roadmap * [ ] Add Tests * [ ] Middleware usage Example * [ ] Real-time Updates Example * [ ] Own devtools About A minimalist & flexible toolkit for interactive islands & state management in hypermedia-driven web applications. Topics state-management vanilla-javascript vanilla-js reactivity vanillajs lit-html Resources Readme License MIT license Activity Stars 19 stars Watchers 1 watching Forks 0 forks Report repository Releases 4 v0.0.9 Latest Nov 4, 2023 + 3 releases Packages 0 No packages published Languages * JavaScript 70.9% * Shell 29.1% Footer (c) 2023 GitHub, Inc. Footer navigation * Terms * Privacy * Security * Status * Docs * Contact GitHub * Pricing * API * Training * Blog * About You can't perform that action at this time.