Stores

Stores are state containers that allow sharing state between the sheet and your JavaScript code. They solve many problems around data sharing both within a sheet, within a single behavior , and for sharing state between behaviors.

Concepts

Stores are created within a sheet using the store-root property. A store is given a name, which is referenced both within the sheet and within JavaScript to get/set values.

A sheet can have multiple stores, and a store can be set on any selector (so any DOM element within the sheet). When a value is get/set within the sheet, it looks at parent elements that have the store-root property for that store name. This works similar as var() lookup.

#app {
  store-root: app;
}

In JavaScript a store is a Map that is reactive. Setting values on that map within a behavior results in that behavior being rebound.

To get a store within JavaScript, get the stores property of the behavior context. The context is passed as the second argument to the behavior's constructor and bind methods.

Stores created in your behavior's sheet will not exist the first time bind() is called, so you should protect against that using optional chaining.

import sheet, { mount } from 'https://cdn.corset.dev/v2';

mount(document, class {
  bind(props, { stores }) {
    let app = stores.get('app');
    let name = app?.get('name');

    return sheet`
      #app {
        store-root: app;
      }

      .name {
        text: ${name};
      }
    `;
  }
})

Setting values

Values can be set in stores both within a sheet and in JavaScript. In both cases the parent behavior (if inside of a mount) will rebind when a value is set.

To set a value in a store within the sheet you can use the store-set property.

store-set takes a name of a store, and then a key and value pair within a space-separated declaration.

#app {
  store-root: app;
}

.person {
  store-set: app name var(--name);
}

To set a value in a store within a behavior, call map.set(key, value) after obtaining the store.

import sheet, { mount } from 'https://cdn.corset.dev/v2';

mount(document, class {
  bind(props, { stores }) {
    const setFoo = ev => {
      stores.get('app')?.set('foo', 'bar');
    };

    return sheet`
      #app {
        store-root: app;

        event: loaded ${setFoo};
      } 
    `;
  }
})

Getting values

Values can get got from a store both within a sheet and in JavaScript.

To get a value from within a sheet, use the store-get() function. It takes the name of the store and the key to get.

#app {
  store-root: app;
  store-set: app foo bar;
}

.item {
  text: store-get(app, foo);
}

To get a value within a behavior's JavaScript, use the Map's get method. Here the store's values are being read within an event listener.

import sheet, { mount } from 'https://cdn.corset.dev/v2';

mount(document, class {
  bind(props, { stores }) {
    const updateAPI = () => {
      let foo = stores.get('app')?.get('foo');
      // call fetch() maybe.
    };

    return sheet`
      #app {
        store-root: app;
        store-set: app foo bar;
        event: update ${updateAPI};
      }
    `;
  }
});

Use cases

Stores are a powerful feature of Corset that have many uses. The following are common scenarios where using stores makes sense.

Receiving state from a child behavior

Behaviors some times create state that you would be interested in observing from the parent. This can be achieved a few other ways in Corset such as:

  • Passing a callback function to the behavior through its inputProperties. The behavior can call the callback with data, and then the parent can trigger its rebind().
  • Triggering an event within the child behavior. The parent behavior can listen to the event and receive data that way.

However stores are the best option for sharing this type of data. Simply create a store within the parent sheet using store-root. Then pass that store down using an inputProperty as you would pass any other value to a child behavior. Use store() to get the store within your sheet.

The child can then trigger rebinds within the parent by setting values on that store.

#app {
  store-root: app;
}

.counter {
  --store: store(app);
  behavior: mount(counter);
}

.sibling {
  text: "Count: " store-get(app, count);
}

As the child behavior changes and sets value to the store with map.set(key, value), the parent behavior will update to reflect those changes.

Custom functions that reflect state changes

Custom functions can be a nice way to control state outside of a behavior. A custom function can create a store with the createStore function on the FunctionContext and return that store. Changes will reflect back into the sheet as state changes.

Here the --fetch Custom Function is returning a state that gets updating as the state of the fetch request changes. The sheet is able to use that state to control a class.

This pattern makes it simple to express loading states in Corset. See the example in action.

#movies {
  --request: --fetch("/api/movies");
  --fetch-state: get(var(--request), state);
  --fetch-value: get(var(--request), value);

  /* Set a dynamic class from the fetch state */
  class-toggle: var(--fetch-state) true;
}

#movies.pending {
  text: "Loading movies";
}

#movies.resolved {
  /* Use the value once the fetch resolves */
  each: var(--fetch-value) select(#movies-template);
}

Storing initial DOM data

Since Corset is a progressive enhancement based library, you often have values you need within your JavaScript that are initially within the DOM.

A store can be used to extract those initial values and then share them within the JavaScript side. Here is an example of a counter where the initial count is stored as a data property. That property value is saved to a store, and then it can be read within event listeners.

The initial DOM might look like this:

<div id="app" data-initial="10">
  <button type="button">Increment</button>
</div>

And then the Corset bring it to life like this:

import sheet, { mount } from 'https://cdn.corset.dev/v2';

mount(document, class {
  bind(props, { stores }) {
    const increment = () => {
      let current = stores?.get('app').get('count');
      stores?.get('app').set('count', current + 1);
    };

    return sheet`
      #app {
        store-root: app;
        store-set: app count data(initial);
      }

      button {
        event: click ${increment};
      }
    `;
  }
});