npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

chocolatier

v0.3.0

Published

a minimal UI library

Downloads

5

Readme

chocolatier

Table of Contents

Overview

const helloWorld = createElement("p", addChild(createText("Hello, world!")));
const count = createState(0);

const counter = createElement(
  "button",
  addEventListener("click", () => setState(count, getState(count) + 1)),
  addChild(createText(() => getState(count), [count]))
);

See counter on CodeSandbox

Pipe Operator

const count = createState(0);

const counter =
  createElement("button")
  |> addEventListener("click", () => setState(count, getState(count) + 1))(%)
  |> addChild(createText(() => getState(count), [count]))(%);

See pipe operator on CodeSandbox

Installation

npm

npm install chocolatier

CDN

<script src="https://unpkg.com/chocolatier/dist/index.umd.js"></script>

Introduction

chocolatier is a lightweight, reactive JavaScript library for pragmatic state management and effective DOM manipulation. It uses a powerful combination of Symbols, WeakMaps, and Sets to offer precise control over even the most granular aspects of the DOM. It also handles dependencies and side effects in a transparent and predictable manner, improving code readability and maintainability.

chocolatier offers a refreshing level of predictability by not adopting the "component" concept in a traditional sense, as seen in component-based frameworks. Instead, it allows for the composition of UI by assigning DOM elements to variables, and using plain functions that return DOM elements. This avoids unexpected re-renders, function calls, or side effects that are common pitfalls in other libraries. Additionally, defining states and effects is not restricted to components. For a practical example of UI composition using chocolatier, refer to Composing UI.

Another noteworthy feature of chocolatier is that state updates are synchronous. This ensures that they happen immediately and in the order they are called, mitigating the risk of state inconsistency.

Unlike many modern libraries that require transpiling and bundling processes, chocolatier is written in plain JavaScript and can be included directly in a web page using a script tag. For TypeScript users, types are provided out of the box through JSDoc comments.

Here's how chocolatier works:

State Management

createState

createState(value) creates a new state with the given initial value. It returns a unique symbol as the key for this state.

const count = createState(0);

getState

getState(symbol) retrieves the current value of the state identified by the given symbol.

console.log(getState(count)); // 0

setState

setState(symbol, value) sets the value of the state identified by the given symbol and invokes all the dependent effects. setState is synchronous.

setState(count, getState(count) + 1);

Effects

createEffect

createEffect(callback, symbols) creates a new effect with the given callback function and an array of state symbols that this effect depends on. The callback function is invoked whenever any of the dependent states change. The callback function is invoked immediately if no state symbols are provided.

createEffect(() => {
  console.log(getState(count));
}, [count]);

createGuardedEffect

createGuardedEffect(callback, condition, symbols) creates a new guarded effect. This effect only triggers its callback function when its condition function returns true.

createGuardedEffect(
  () => {
    console.log(getState(count));
  },
  () => getState(count) > 0,
  [count]
);

DOM Element Creation and Manipulation

createElement

createElement(elementType, ...modifiers) creates a new HTML element of the given type and applies the provided modifiers to it.

createElement(
  "button",
  addEventListener("click", () => setState(count, getState(count) + 1)),
  addChild(createText("Increment"))
);

createSvgElement

createSvgElement(elementType, ...modifiers) creates a new SVG element of the given type and applies the provided modifiers to it.

createText

createText(text, symbols) creates a new text node with the given text, which can be a static string, a static number, a function that returns a number, or a function that returns a string. When the text is a function, symbols should be provided to track the dependent states. The text node is updated whenever any of the dependent states change.

createElement("p", addChild(createText("Hello, world!")));
createElement(
  "button",
  addEventListener("click", () => setState(count, getState(count) + 1)),
  addChild(createText(() => getState(count), [count]))
);

createRef

createRef(callback) creates a new reference to a DOM element and invokes the provided callback function with this reference.

createElement(
  "p",
  createRef((ref) => console.log(ref.deref())),
  addChild(createText("Hello, world!"))
);

setProperty

setProperty(key, value, symbols) sets the property of a DOM element to the given value. The value can be a static value or a function that returns a value. When the value is a function, symbols should be provided to track the dependent states. The value of the property is updated whenever any of the dependent states change.

createElement("input", setProperty("id", "name"));
createElement(
  "button",
  addEventListener("click", () => setState(count, getState(count) - 1)),
  setProperty("disabled", () => getState(count) <= 0, [count]),
  addChild(createText("Decrement"))
);

setAttribute

setAttribute(key, value, symbols) sets the attribute of a DOM element to the given value. The value can be a static value or a function that returns a value. When the value is a function, symbols should be provided to track the dependent states. The value of the attribute is updated whenever any of the dependent states change.

createElement(
  "label",
  setAttribute("for", "name"),
  addChild(createText("Name"))
);

addEventListener

addEventListener(eventType, listener, options) adds an event listener to a DOM element.

createElement(
  "button",
  addEventListener("click", () => setState(count, getState(count) + 1)),
  addChild(createText("Increment"))
);

addChild

addChild(child, symbols) adds a child to a DOM element. The child can be a static node or a function that returns a node. When the child is a function, symbols should be provided to track the dependent states. The child is updated whenever any of the dependent states change.

createElement("p", addChild(createText("Hello, world!")));

addGuardedChild

addGuardedChild(child, condition, symbols) conditionally adds a child to a DOM element. The child must be a function that returns a node. Symbols must be provided to track the dependent states. When the condition function returns true, the child is added to the DOM element. When the condition function returns false, the child is removed from the DOM element.

createElement(
  "p",
  addGuardedChild(
    () => createText("Count is greater than or equal to 10"),
    () => getState(count) >= 10,
    [count]
  )
);

addKeyedChildren

addKeyedChildren(createItem, getKey, symbol) adds a list of children to a DOM element, where each child is identified by a unique key.

const list = createState([
  { id: "foo", text: "Foo" },
  { id: "bar", text: "Bar" },
  { id: "baz", text: "Baz" },
]);

createElement(
  "ul",
  addKeyedChildren(
    (item) => createElement("li", addChild(createText(item.text))),
    (item) => item.id,
    list
  )
);

onMount

onMount(callback) adds a 'mount' event listener to a DOM element.

createElement(
  "p",
  onMount(() => console.log("mounted")),
  addChild(createText("Hello, world!"))
);

onUnmount

onUnmount(callback) adds an 'unmount' event listener to a DOM element.

createElement(
  "p",
  onUnmount(() => console.log("unmounted")),
  addChild(createText("Hello, world!"))
);

Styling

CSS class

.btn {
  border-radius: 0.25rem;
  border-style: none;
  padding: 0.5rem 1rem;
}
createElement(
  "button",
  setAttribute("class", "btn"),
  addChild(createText("Button"))
);

Tailwind CSS

createElement(
  "button",
  setAttribute(
    "class",
    "rounded border-none bg-indigo-700 px-4 py-2 font-sans text-white"
  ),
  addChild(createText("Button"))
);

Composing UI

const users = createState([]);
const selectedUserId = createState();
const posts = createState([]);
const selectedPostId = createState();
const comments = createState([]);

createEffect(() => {
  fetch("https://jsonplaceholder.typicode.com/users")
    .then((response) => response.json())
    .then((data) => setState(users, data));
});

createGuardedEffect(
  () => {
    setState(posts, []);
    fetch(
      `https://jsonplaceholder.typicode.com/users/${getState(
        selectedUserId
      )}/posts`
    )
      .then((response) => response.json())
      .then((data) => setState(posts, data));
  },
  () => getState(selectedUserId) !== undefined,
  [selectedUserId]
);

createGuardedEffect(
  () => {
    setState(comments, []);
    fetch(
      `https://jsonplaceholder.typicode.com/posts/${getState(
        selectedPostId
      )}/comments`
    )
      .then((response) => response.json())
      .then((data) => setState(comments, data));
  },
  () => getState(selectedPostId) !== undefined,
  [selectedPostId]
);

const selectLabel = createElement(
  "label",
  setAttribute("for", "users"),
  addChild(createText("Select a user"))
);

const selectOption = (user) =>
  createElement(
    "option",
    setProperty("value", user.id),
    addChild(createText(user.name))
  );

const selectInput = createElement(
  "select",
  setProperty("id", "users"),
  setProperty("value", () => getState(selectedUserId), [selectedUserId]),
  addEventListener("change", (e) => setState(selectedUserId, e.target.value)),
  addKeyedChildren(users, (user) => [user.id, selectOption(user)], [users])
);

const viewCommentButton = (post) =>
  createElement(
    "button",
    addEventListener("click", () => setState(selectedPostId, post.id)),
    addChild(createText("View comments"))
  );

const commentItem = (comment) =>
  createElement("li", addChild(createText(comment.body)));

const commentList = addKeyedChildren(
  comments,
  (comment) => [comment.id, commentItem(comment)],
  [comments]
);

const postItem = (post) =>
  createElement(
    "li",
    addChild(createText(post.title)),
    addChild(viewCommentButton(post)),
    addGuardedChild(
      () =>
        createElement(
          "section",
          addChild(createElement("h3", addChild(createText("Comments")))),
          commentList
        ),
      () =>
        getState(comments).length > 0 && getState(selectedPostId) === post.id,
      [comments, selectedPostId]
    )
  );

const postList = createElement(
  "ul",
  addKeyedChildren(posts, (post) => [post.id, postItem(post)], [posts])
);

const userPosts = createElement(
  "section",
  addChild(createElement("h2", addChild(createText("User Posts")))),
  addChild(selectLabel),
  addChild(selectInput),
  addGuardedChild(
    () => postList,
    () => getState(posts).length > 0,
    [posts]
  )
);

const root = document.getElementById("root");
root.appendChild(userPosts);

See Composing UI on CodeSandbox

Examples