1.0.1 • Published 4 years ago

organic-lit v1.0.1

Weekly downloads
-
License
ISC
Repository
-
Last release
4 years ago

Organic Lit

A library to share messages in your lit-element organism without additional wiring. See the tabs for some examples

Note

This library is an experimental approach to state management, and is mostly me "thinking out loud". If you want to use it (or something like it) feel free, just be aware that I feel it's partly too implicit, needs to much thinking to do it right, and I can think of a dozen ways it would screw you over.

That being said, have fun.

Purpose

The most difficult part of every application is state management. There are sufficient solutions for local state (the element itself), for the application it's more difficult.

Properties/Events can help you there, but might lead to prop-drilling (passing properties down until they are needed), or via context in libraries like react which is tightly coupled to the UI itself.

This library uses an approach that focused on the global (organism) level, allowing you to send messages between cells (elements) of your page.

Wording/Domain

This library uses the wording that is coming from Biology, and at such aims to replicate patterns learned there with some abstraction on top of it.

NameDescription
OrganismThe application, or group of applications (microfrontends) that act as one
HormoneA specific message, send by special hormone producing entities that can released and carry some state
ReceptorAn interface on a LitElement that can receive one type of hormone and gets triggered every time the hormone is released
HypothalamusAllows to orchestrate hormones and trigger side-effects on a global (organism wide) level

The idea that organic-lit follows is different from other libraries because it means that hormones must not be triggered by components only, but basically by everything, and therefore allows you to, as an example, connect a web-socket service class with your components.

In difference to libraries like redux it means that you don't have one global state, but a state for each hormone that is orchestrated by events.

Usage

Hormones and receptors

The hormone is the base unit, and has to be defined first anywhere.

The defined hormone can then be used to be released, or to define a receptor.

const hormone = defineHormone("example", false);

pureLit("some-element", (element) => {
  // define receptor
  const receptorState = useReceptor(element, hormone);
  return html`<p>Receptor State: ${receptorState}</p>`;
});

// some-element: <p>Receptor State: false</p>

// release hormone
releaseHormone(hormone);

// some-element: <p>Receptor State: true</p>

Unlike the hormone you can find in nature, this one however can transport additional information if you need it. Let's take the example, but this time with a counter

const hormone = defineHormone("example", { count: 0 });

pureLit("some-element", (element) => {
  // define receptor
  const { count } = useReceptor(element, hormone);
  return html`<p>Receptor State: ${count}</p>`;
});

// some-element: <p>Receptor State: 0</p>

// release hormone
releaseHormone(hormone, (currentCount) => ({
  count: currentCount + 1,
}));

// some-element: <p>Receptor State: 1</p>

In theory, you can also release the hormone from the same component, but really you can release it everywhere as they are global in the organism.

Receptors can also have filters. This can be used to manage the global state drawbacks. Let's take the counter example, and a page where two counters are used.

const hormone = defineHormone("example", { count: 0, counter: undefined });

pureLit("some-element", (element) => {
  const { count } = useReceptor(
    element,
    hormone,
    // the filter that only applies the counter if the name===counter
    ({ counter }) => counter === element.name
  );
  return html`<p>Receptor State: ${count}</p>`;
});

// some-element[name=first]: <p>Receptor State: 0</p>
// some-element[name=other]: <p>Receptor State: 0</p>

// release hormone
releaseHormone(hormone, (currentCount) => ({
  count: currentCount + 1,
  counter: "first",
}));

// some-element[name=first]: <p>Receptor State: 1</p>
// some-element[name=other]: <p>Receptor State: 0</p>

If we wouldn't have added the filter, all counter instances would have been incremented. organic-lit also takes care that only the one element that receives the change is re-rendered, the other one remains untouched.

While this might look like a drawback at first (and it is something that can lead to bugs if not tested properly), it is actually one of the huge advantages of organic-lit. Think about a spreadsheet like application. We have thousands of identical cells, some of those with references to others. If a field changes, it releases a hormone, and the others can check if they are affected and change only if.

const cellChanged = defineHormone("cell/changed", {
  row: 0,
  column: "A",
  value: "",
});

pureLit("cell-element", (element) => {
  const { row, cell, value } = element;
  useReceptor(
    element,
    cellChanged,
    // the filter that only applies if the field has a formula and references the field that changed
    (cell) => isFormula(value) && hasReference(value, cell)
  );
  return isFormula(value)
    ? html`${calculatedField(value)}`
    : html`<input
        type="text"
        @change=${({ target }) =>
          releaseHormone(cellChanged, {
            value: target.value,
            row,
            cell,
          })}
        value=${value}
      />`;
});

Hypothalamus

As your application grows, you will get to the point where you need some orchestration. For that you can use the hypothalamus, which allows you to trigger side-effects on released hormones.

Let's take a look at an example todo-list

const todoAdd = defineHormone<string>("todo/add");
const todoList = defineHormone<string[]>("todo/list", { defaultValue: [] });

hypothalamus.on(todoAdd, (todo) =>
  releaseHormone(todoList, (todos) => [...todos, todo])
);

pureLit("component-list", (element) => {
  const list = useReceptor(element, element.receptor);
  return html`<ul>
    ${list.map((item) => html`<li>${item}</li>`)}
  </ul>`;
});

export default pureLit("todo-app", () => {
  return html`
    <input-with-button @onSubmit=${(value) => releaseHormone(todoAdd, value)}>
      Add todo
    </input-with-button>
    <h2>Your todos</h2>
    <div><component-list .receptor=${todoList}></component-list></div>
  `;
});

The two hormones are separating the information of the change, and the state of the list. When we release the todoAdd Hormone, the hypothalamus releases the todoList, which will be received by the component-list that uses the receptor.

The hypothalamus allows you to collect multiple hormones, and trigger only after all were released.

const corticoliberin = defineHormone("Corticoliberin", { defaultValue: false });
const adrenocorticotropin = defineHormone("Adrenocorticotropin", {
  defaultValue: false,
});

const receiver = jest.fn();

hypothalamus.on([corticoliberin, adrenocorticotropin], (result) => {
  receiver(result);
});

it("does not trigger when only no hormone is released", () => {
  expect(receiver).not.toBeCalled();
});

it("does not trigger when only one hormone is released", () => {
  releaseHormone(corticoliberin);
  expect(receiver).not.toBeCalled();
});

it("triggers the connection when all hormones are released", () => {
  releaseHormone(adrenocorticotropin);
  releaseHormone(corticoliberin);
  expect(receiver).toBeCalled();
});

it("doesn't trigger the connection again after all hormones are released but only after all hormones are released a second time", () => {
  releaseHormone(adrenocorticotropin, true);
  releaseHormone(corticoliberin, true);
  expect(receiver).toBeCalledTimes(1);
  // check the result directly
  expect(receiver).toBeCalledWith({
    Adrenocorticotropin: true,
    Corticoliberin: true,
  });
  // or use the getValue helper function that allows passing the hormone
  const result = receiver.mock.calls[0][0];
  expect(getValue(adrenocorticotropin, result)).toBe(true);
  expect(getValue(corticoliberin, result)).toBe(false);

  releaseHormone(corticoliberin);
  expect(receiver).toBeCalledTimes(1);
  releaseHormone(adrenocorticotropin);
  expect(receiver).toBeCalledTimes(2);
});

Read Once Hormone

Read Once Hormone are a special types of hormone that reset their state to the initial value after all receivers have received the new value. An example can be a form that changes it's value to true once submitted, and back to false immediately after, or the spreadsheet example where every hormone is a Read Once Hormone.

const doSubmit = defineReadOnceHormone("submit");
const allValidators = ["firstName", "lastName"]
    .map(field => defineHormone(`validate/${field}`, {
        field,
        isValid: false,
    });
const validationFailed = defineReadOnceHormone("validationFailed")
const submitDone = defineReadOnceHormone("submitted")

pureLit("input-element", (el) => {
  const submitting = useReceptor(el, doSubmit);
  // this will be true only the first time this hormone is released
  if (submitting) {
    releaseHormone({ name: `validate/${field}` },
        { field: el.name, isValid: validate(el) });
  }
  // ...
});

hypothalamus.on(allValidators, (result) => {
    if (Object.entries(allValidators).some(({isValid}) => !isValid)) {
        releaseHormone(validationFailed, true)
    } else {
        releaseHormone(submitDone, true)
    }
});

pureLit("my-form", (el) => {
    // those receptors will return their value only the render cycle
    //  after they were set, and in any other case return undefined
    const error = useReceptor(el, validationFailed) ?? false
    const success = useReceptor(el, submitDone) ?? false
    return html`
        <error-text show=${error}>Validation failed</error-text>
        <success-text show=${success}>Form submitted</success-text>
        <input-element name="firstName">FirstName<input-element>
        <input-element name="lastName">LastName<input-element>
        <button @click=${() => releaseHormone(doSubmit, true)}>submit</button>
    `
});