💊 Event-driven DOM programming in a new style

Overview

capsule

Capsule v0.5.3

ci

Event-driven DOM programming in a new style

Features

  • Supports event-driven style of frontend programming in a new way.
  • Supports event delegation and outside events out of the box.
  • Lightweight library. 1.2 kb gzipped. No dependencies. No build steps.
  • Uses plain JavaScript and plain HTML, requires No special syntax.
  • TypeScript friendly.

See live examples

Motivation

Virtual DOM frameworks are good for many use cases, but sometimes they are overkill for the use cases where you only need a little bit of event handlers and dom modifications.

This capsule library explores the new way of simple event-driven DOM programming without virtual dom.

Slogans

  • Local query is good. Global query is bad.
  • Define behaviors based on HTML classes.
  • Use pubsub when making remote effect.

Local query is good. Global query is bad

When people use jQuery, they often do:

$(".some-class").each(function () {
  $(this).on("some-event", () => {
    $(".some-target").each(function () {
      // some effects on this element
    });
  });
});

This is very common pattern, and this is very bad.

The above code can been seen as a behavior of .some-class elements, and they use global query $(".some-target"). Because they use global query here, they depend on the entire DOM tree of the page. If the page change anything in it, the behavior of the above code can potentially be changed.

This is so unpredictable because any change in the page can affect the behavior of the above class. You can predict what happens with the above code only when you understand every details of the entire application, and that's often impossible when the application is large size, and multiple people working on that app.

So how to fix this? We recommend you should use local queries.

Let's see this example:

$(".some-class").each(function () {
  $(this).on("some-event", () => {
    $(this).find(".some-target").each(function () {
      // some effects on this element
    });
  });
});

The difference is $(this).find(".some-target") part. This selects the elements only under each .some-class element. So this code only depends on the elements inside it, which means there is no global dependencies here.

capsule enforces this pattern by providing query function to event handlers which only finds elements under the given element.

const { on } = component("some-class");

on.click = ({ query }) => {
  query(".some-target").textContent = "clicked";
};

Here query is the alias of el.querySelector and it finds .some-target only under it. So the dependency is local here.

Define behaviors based on HTML classes

From our observation, skilled jQuery developers always define DOM behaviors based on HTML classes.

We borrowed this pattern, and capsule allows you to define behavior only based on HTML classes, not random combination of query selectors.

<div class="hello">John Doe</div>
const { on } = component("hello");

on.__mount__ = () => {
  alert(`Hello, I'm ${el.textContext}!`); // Alerts "Hello, I'm John Doe!"
};

Use pubsub when making remote effect

We generally recommend using only local queries, but how to make effects to the remote elements?

We reommend using pubsub pattern here. By using this pattern, you can decouple those affecting and affected elements. If you decouple those elements, you can test those components independently by using events as I/O of those components.

capsule library provides pub and sub APIs for encouraging this pattern.

const EVENT = "my-event";
{
  const { on } = component("publisher");

  on.click = ({ pub }) => {
    pub(EVENT);
  };
}

{
  const { on, sub } = component("subscriber");

  sub(EVENT);

  on[EVENT] = () => {
    alert(`Got ${EVENT}!`);
  };
}

Note: capsule uses DOM Event as event payload, and sub:EVENT HTML class as registration to the event. When pub(EVENT) is called the CustomEvent of EVENT type are dispatched to the elements which have sub:EVENT class.

TodoMVC

TodoMVC implementation is also available here.

Live examples

See the live demos.

Install

Vanilla js (ES Module):

<script type="module">
import { component } from "https://deno.land/x/[email protected]/dist.min.js";
// ... your code ...
</script>

Vanilla js (Legacy script tag):

<script src="https://deno.land/x/[email protected]/loader.js"></script>
<script>
capsuleLoader.then((capsule) => {
  const { component } = capsule;
  // ... your code ...
});
</script>

Deno:

import { component } from "https://deno.land/x/[email protected]/mod.ts";

Via npm:

npm install @kt3k/capsule

and

import { component } from "@kt3k/capsule";

Examples

Mirrors input value of <input> element to another dom.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("mirroring");

on.input = ({ query }) => {
  query(".src").textContent = query(".dest").value;
};

Pubsub.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const EVENT = "my-event";

{
  const { on } = component("pub-element");

  on.click = ({ pub }) => {
    pub(EVENT, { hello: "world!" });
  };
}

{
  const { on, sub } = component("sub-element");

  sub(EVENT);

  on[EVENT] = ({ e }) => {
    console.log(e.detail.hello); // => world!
  };
}

Bubbling events.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("my-component");

const EVENT = "my-event";

on.click = ({ emit }) => {
  // dispatch CustomEvent of type "my-event"
  // and it bubbles up.
  emit(EVENT);

  // dispatch CustomEvent of type "my-event"
  // with details = { foo: "bar" };
  // and it bubbles up.
  emit(EVENT, { foo: "bar" }); // dispatch
};

Mount hooks.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("my-component");

// __mount__ handler is called when the component mounts to the elements.
on.__mount__ = () => {
  console.log("hello, I'm mounted");
};

Prevent default, stop propagation.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("my-component");

on.click = ({ e }) => {
  // e is the native event object.
  // You can call methods of Event object
  e.stopPropagation();
  e.preventDefault();
  console.log("hello, I'm mounted");
};

Event delegation. You can assign handlers to on(selector).event to use event delegation pattern.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("my-component");

on(".btn").click = ({ e }) => {
  console.log(".btn is clicked!");
};

Outside event handler. By assigning on.outside.event, you can handle the event outside of the component dom.

import { component } from "https://deno.land/x/[email protected]/dist.min.js";

const { on } = component("my-component");

on.outside.click = ({ e }) => {
  console.log("The outside of my-component has been clicked!");
};

API reference

const { component, mount } from "https://deno.land/x/[email protected]/dist.min.js";

component(name): ComponentResult

This registers the component of the given name. This returns a ComponentResult which has the following shape.

interface ComponentResult {
  on: EventRegistryProxy;
  is(name: string);
  sub(type: string);
  innerHTML(html: string);
}

interface EventRegistry {
  [key: string]: EventHandler | {};
  (selector: string): {
    [key: string]: EventHandler;
  };
  outside: {
    [key: string]: EventHandler;
  };
}

component().on[eventName] = EventHandler

You can register event handler by assigning to on.event.

const { on } = component("my-component");

on.click = () => {
  alert("clicked");
};

component().on(selector)[eventName] = EventHandler

You can register event handler by assigning to on(selector).event.

The actual event handler is attached to the component dom (the root of element which this component mounts), but the handler is only triggered when the target is inside the given selector.

const { on } = component("my-component");

on(".btn").click = () => {
  alert(".btn is clicked");
};

component().on.outside[eventName] = EventHandler

You can register event handler for the outside of the component dom by assigning to on.outside.event

const { on } = component("my-component");

on.outside.click = () => {
  console.log("outside of the component has been clicked!");
};

This is useful for implementing a tooltip which closes itself if the outside of it is clicked.

component().is(name: string)

is(name) sets the html class to the component dom at mount phase.

const { is } = component("my-component");

is("my-class-name");

component().innerHTML(html: string)

innerHTML(html) sets the inner html to the component dom at mount phase.

const { innerHTML } = component("my-component");

innerHTML("<h1>Greetings!</h1><p>Hello from my-component</p>");

component().sub(type: string)

sub(type) sets the html class of the form sub:type to the component at mount phase. By adding sub:type class, the component can receive the event from pub(type) calls.

{
  const { sub, on } = component("my-component");
  sub("my-event");
  on["my-event"] = () => {
    alert("Got my-event");
  };
}
{
  const { on } = component("another-component");
  on.click = ({ pub }) => {
    pub("my-event");
  };
}

EventHandler

The event handler in capsule has the following signature. The first argument is EventHandlerContext, not Event.

type EventHandler = (ctx: ComponentEventContext) => void;

ComponentEventContext

interface ComponentEventContext {
  e: Event;
  el: Element;
  emit<T = unknown>(name: string, data: T): void;
  pub<T = unknown>(name: string, data: T): void;
  query(selector: string): Element | null;
  queryAll(selector: string): NodeListOf<Element> | null;
}

e is the native DOM Event. You can call APIs like .preventDefault() or .stopPropagation() via this object.

el is the DOM Element, which the event handler is bound to, and the event is dispatched on.

emit(type) dispatches the event on this DOM Element. The event bubbles up. So the parent component can handle those events. If you'd like to communicate with the parent elements, then use this method to send information to parent elements.

You can optionally attach data to the event. The attached data is available via .detail property of CustomEvent object.

pub(type) dispatches the event to the remote elements which have sub:type class. This should be used with sub(type) calls. For example:

{
  const { sub, on } = component("my-component");
  sub("my-event");
  on["my-event"] = () => {
    alert("Got my-event");
  };
}
{
  const { on } = component("another-component");
  on.click = ({ pub }) => {
    pub("my-event");
  };
}

This call dispatches new CustomEvent("my-type") to the elements which have sub:my-type class, like <div class="sub:my-type"></div>. The event doesn't bubbles up.

This method is for communicating with the remote elements which aren't in parent-child relationship.

mount(name?: string, el?: Element)

This function initializes the elements with the given configuration. component call itself initializes the component of the given class name automatically when document got ready, but if elements are added after the initial page load, you need to call this method explicitly to initialize capsule's event handlers.

// Initializes the all components in the entire page.
mount();

// Initializes only "my-component" components in the entire page.
// You can use this when you only added "my-component" component.
mount("my-compnent");

// Initializes the all components only in `myDom` element.
// You can use this when you only added something under `myDom`.
mount(undefined, myDom);

// Initializes only "my-component" components only in `myDom` element.
// You can use this when you only added "my-component" under `myDom`.
mount("my-component", myDom);

unmount(name: string, el: Element)

This function unmounts the component of the given name from the element. This removes the all event listeners of the component and also calls the __unmount__ hooks.

const { on } = component("my-component");

on.__unmount__ = () => {
  console.log("unmounting!");
};

unmount("my-component", el);

Note: It's ok to just remove the mounted elements without calling unmount. Such removals don't cause a problem in most cases, but if you use outside handlers, you need to call unmount to prevent the leakage of the event handler because outside handlers are bound to document object.

How capsule works

This section describes how capsule works in a big picture.

Let's look at the below basic example.

const { on } = component("my-component");

on.click = () => {
  console.log("clicked");
};

This code is roughly translated into jQuery like the below:

$(document).read(() => {
  $(".my-component").each(function () {
    $this = $(this);

    if (isAlreadyInitialized($this)) {
      return;
    }

    $this.click(() => {
      console.log("clicked");
    });
  });
});

capsule can be seen as a syntax sugar for the above pattern (with a few more utilities).

Prior art

  • capsid
    • capsule is heavily inspired by capsid

Projects with similar concepts

  • Flight by twitter
    • Not under active development
  • eddy.js
    • Archived

History

  • 2022-01-13 v0.5.2 Change el typing. #2
  • 2022-01-12 v0.5.1 Fix __mount__ hook execution order
  • 2022-01-12 v0.5.0 Add tests, setup CI.
  • 2022-01-11 v0.4.0 Add outside handlers.
  • 2022-01-11 v0.3.0 Add unmount.
  • 2022-01-11 v0.2.0 Change delegation syntax.

License

MIT

You might also like...

Cookbook Method is the process of learning a programming language by building up a repository of small programs that implement specific programming concepts.

Cookbook Method is the process of learning a programming language by building up a repository of small programs that implement specific programming concepts.

CookBook - Hacktoberfest Find the book you want to read next! PRESENTED BY What is CookBook? A cookbook in the programming context is collection of ti

Nov 17, 2022

Custom Vitest matchers to test the state of the DOM, forked from jest-dom.

vitest-dom Custom Vitest matchers to test the state of the DOM This library is a fork of @testing-library/jest-dom. It shares that library's implement

Dec 16, 2022

An extension of DOM-testing-library to provide hooks into the shadow dom

Why? Currently, DOM-testing-library does not support checking shadow roots for elements. This can be troublesome when you're looking for something wit

Dec 13, 2022

Jaksel Script, Programming language very modern and Indonesian style

Jaksel Script Jaksel Script is a new programming language, very modern, easy to learn, using Indonesia-slang language. No programming experience requi

Jan 3, 2023

A simple static type checker that enforces C-style programming in Julia

SimpleTypeChecker is an experimental Julia package designed to enforce C-style programming in Julia language. Note : it won't save you if your codes a

May 23, 2023

Base62-token.js - Generate & Verify GitHub-style & npm-style Base62 Tokens

base62-token.js Generate & Verify GitHub-style & npm-style Secure Base62 Tokens Works in Vanilla JS (Browsers), Node.js, and Webpack. Online Demo See

Jun 11, 2022

This is a project that is in partial fulfillment of our CSCI 318 - Programming Language Concepts class of the Fall 2022 semester in New York Institute of Technology

StreetEasier A hub to search for apartments and roommate matching This project was bootstrapped with Create React App. Want to Test Yourself? 1.) Clon

Dec 5, 2022

Project Cider. A new look into listening and enjoying Apple Music in style and performance. 🚀

Project Cider. A new look into listening and enjoying Apple Music in style and performance. 🚀

Links Wiki Request Feature Report Bug View The Releases Install Sources Compiling and Configuration For more information surrounding configuration, co

Jan 5, 2023

MySQL meets Jupyter notebooks. Grasp provides a new way to learn and write SQL, by providing a coding-notebook style with runnable blocks, markdown documentation, and shareable notebooks. ✨

MySQL meets Jupyter notebooks. Grasp provides a new way to learn and write SQL, by providing a coding-notebook style with runnable blocks, markdown documentation, and shareable notebooks. ✨

A New Way to Write & Learn SQL Report Bug · Request Feature Table of Contents About The Project Built With Getting Started Prerequisites Installation

Sep 1, 2022
Comments
  • towards v1

    towards v1

    • [x] unit testing
      • [x] mount
      • [x] on.click
      • [x] on(selector).click
      • [x] on.outside.click
      • [x] is
      • [x] innerHTML
      • [x] pub, sub
      • [x] emit bubbles
      • [x] __unmount__
      • [x] query, queryAll
      • [x] assert errors
    • [x] debug log
    • [x] dnt
    • [x] bmp
    • [x] __unmount__
    • [x] outside event
    • [x] prep -> mount
    • [x] API docs
    • [x] monaco-editor text highlight in web page
    • [x] type on correctly
    • [x] CI
    • [x] Add live examples
      • [x] event delegation
      • [x] outside events
      • [x] emit bubbles
      • [x] preventDefault
    • [x] port todomvc2 https://github.com/capsidjs/capsule-todomvc
    opened by kt3k 0
Owner
capsid
Frontend UI framework
capsid
io-ts Typed Event Bus for the runtime of your Node.js application. A core for any event-driven architecture based app.

Typed Event Bus Based on io-ts types, this bus provides a handy interface to publish and consume events in the current runtime of the Node.js process.

Konstantin Knyazev 3 May 23, 2022
Examples of how to do query, style, dom, ajax, event etc like jQuery with plain javascript.

You (Might) Don't Need jQuery Frontend environments evolve rapidly nowadays and modern browsers have already implemented a great deal of DOM/BOM APIs

NEFE 20.3k Dec 24, 2022
An event-driven architecture wrapper for Wechaty that applies the CQS principle by using separate Query and Command messages to retrieve and modify the bot state, respectively.

CQRS Wechaty An event-driven architecture wrapper for Wechaty that applies the CQS principle by using separate Query and Command messages to retrieve

Wechaty 3 Mar 23, 2022
awsrun 189 Jan 3, 2023
An AWS Cloud Native application using CDK that defines a Serverless Event Driven application for interacting with Twitter and utilising Machine Learning / AI as a Service.

AWS Serverless Event Driven Twitter Bot An AWS Cloud Native application using CDK (Written in TypeScript) that defines a Serverless Event Driven appli

null 4 Dec 18, 2022
Get event details of competitive programming contests, hackathons etc.

UpCoding Mobile App: (https://github.com/sahanmndl/UpCoding-Demo) This project was bootstrapped with Create React App. Available Scripts In the projec

Sahan Mondal 5 Nov 4, 2022
Adds `long-press` event to the DOM in 1k of pure JavaScript

long-press-event A 1k script that adds a long-press event to the DOM using CustomEvent and pure JavaScript. Works in IE9+, Chrome, Firefox, Safari as

John Doherty 262 Jan 2, 2023
Provides event handling and an HTMLElement mixin for Declarative Shadow DOM in Hotwire Turbo.

Turbo Shadow Provides event handling and an HTMLElement mixin for Declarative Shadow DOM support in Hotwire Turbo. Requires Turbo 7.2 or higher. Quick

Whitefusion 17 Sep 28, 2022
When a person that doesn't know how to create a programming language tries to create a programming language

Kochanowski Online Spróbuj Kochanowskiego bez konfiguracji projektu! https://mmusielik.xyz/projects/kochanowski Instalacja Stwórz nowy projekt przez n

Maciej Musielik 18 Dec 4, 2022