0.3.0 • Published 11 months ago

chocolatier v0.3.0

Weekly downloads
-
License
MIT
Repository
-
Last release
11 months ago

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

0.3.0

11 months ago

0.2.1

12 months ago

0.2.0

12 months ago

0.2.3

12 months ago

0.2.2

12 months ago

0.2.4

12 months ago

0.1.3

2 years ago

0.1.2

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago