Grains.js is a tiny, reactive state management library designed for HTML. It lets you create isolated state containers directly within your HTML using custom attributes, eliminating the need for a build step or complex setup. Simply include the library via a <script> tag and start managing your component states declaratively. Grains.js offers a simple, intuitive API that leverages HTML attributes for all functionality.
- 🎯 Micro-states: Create isolated state containers for specific HTML segments, e.g.
<div>sections. - 🔄 Reactive: Automatic UI updates whenever your state changes.
- 🪶 Lightweight: No dependencies, single file library.
- 🔌 No Build Step: Include directly via a
<script>tag. - 🎨 Flexible: Easily share state between components.
- 🛠️ Simple API: Uses intuitive HTML attributes.
- ↩️ Undo/Redo: Built-in support for state history.
- ✨ Transitions/Animations: Easily integrate animations with state changes.
- đźź° Expression Evaluation: Use JavaScript expressions directly in your directives.
Include the minified version via a <script> tag:
<script src="https://mk0y.github.io/grains.js/dist/grains.min.js"></script># Install dependencies
npm install
# Run tests
npm test
# Build library
npm run buildGrains.js uses custom attributes prefixed with g- to manage state and define reactive behavior. Here's a quick overview:
g-state="stateName": Defines a state container. "stateName" is used to identify the state object.g-init="jsonString"org-init="varName": Specifies the initial state as a JSON object. This attribute is optional; if omitted, the initial state will be an empty object ({}). You can also use a globally scoped variable.g-text="statePath": Binds the text content of an element to a value within the state. "statePath" uses dot notation to access nested properties (e.g., {user.name}).g-model="{statePath}": Enables two-way data binding for form elements (inputs, textareas, selects). Support for other form elements such as files will be added incrementally.g-class="[expressions]": Conditionally applies CSS classes based on the evaluated expression.g-attr="attrName:expression": Dynamically sets or updates an HTML attribute based on the evaluated expression. Multiple attributes can be set using comma-separated pairs (e.g., g-attr="disabled:isDisabled, value:inputValue").g-for="item in items": Creates a new element for each item in the array and applies the value totextContent.g-show="expression": Shows or hides an element based on whether the expression evaluates to true or false and the previous value. It usesdisplaycss property.g-on:event="handlerName": Attaches an event listener. "handlerName" must refer to a globally defined JavaScript function.g-action="action": Triggers undo/redo actions ("undo" or "redo"). Requires a g-state definition.g-disabled="expression": Disables or enables an element based on the evaluated expression.
<div g-state="counter" g-init='{"count": 0}'>
<p g-text="count">0</p>
<p g-text="f('Formatted value: ${count}')"></p>
<button g-on:click="increment">+1</button>
</div>
<script>
function increment({ get, set }) {
set({ count: get("count") + 1 });
}
</script><div g-state="theme" g-init='{"isDark": false}' g-class='{"dark-mode": isDark}'>
<button g-on:click="toggleTheme">Toggle Theme</button>
</div>
<script>
function toggleTheme({ set, get }) {
set({ isDark: !get("isDark") });
}
</script><div g-state="input" g-init='{"value":"", "placeholderText":"Enter Text"}'>
<input
type="text"
g-state="input"
g-model="value"
g-attr="placeholder:placeholderText"
/>
<p g-text="value"></p>
</div><div g-state="fruits" g-init='["Apple", "Banana", "Cherry"]'>
<ul>
<li g-for="fruit in fruits"></li>
</ul>
</div><div g-state="visibility" g-init='{"isVisible": true}'>
<div g-show="isVisible">This div is visible</div>
<button g-on:click="toggleVisibility">Toggle Visibility</button>
</div>
<script>
function toggleVisibility({ set, get }) {
set({ isVisible: !get("isVisible") });
}
</script><input type="text" g-state="myInput" g-model="value" />
<p g-text="value"></p><button g-state="button" g-init='{"isEnabled": true}' g-disabled="!isEnabled">
Click Me
</button>
<button g-on:click="toggleEnabled">Toggle Enabled</button>
<script>
function toggleEnabled({ set, get }) {
set({ isEnabled: !get("isEnabled") });
}
</script>Several example files demonstrate Grains.js usage:
examples/minimal.html: A basic counter example showcasing core directives. (See it in action)examples/class.html: Demonstrates the g-class directive for conditional class binding. (See it in action)examples/form.html: Shows how to use g-model for two-way data binding in forms. (See it in action)examples/transitions.html: Illustrates integrating animations with state changes using CSS transitions. (See it in action)examples/loops.html: Demonstrates the g-for directive for iterating over arrays. (See it in action)
States can be shared between components by using the same state name:
<div g-state="sharedState" g-init='{"value": 0}'>
<p g-text="value"></p>
</div>
<div g-state="sharedState">
<button g-click="increment">Increment Shared Value</button>
</div>Grains.js provides several built-in utility functions that can be used directly within expressions in your directives. These functions simplify common state checks and comparisons, making your directives more concise and readable. The following utility functions are available:
canUndo: Checks if an undo operation is possible for the given element's state. Returns true if there is history available for undo, false otherwise.canRedo: Checks if a redo operation is possible for the given element's state. Returns true if there is history available for redo, false otherwise.isPositive(path): Checks if the value at the specified state path in the element's grain state is greater than 0. "path" uses dot notation (e.g., "user.age").isNegative(path): Checks if the value at the specified path in the element's grain state is less than 0. "path" uses dot notation (e.g., "user.balance").isEmpty(path): Checks if the value at the specified state path in the element's grain state is considered empty (0, "", null, or undefined). "path" uses dot notation (e.g., "user.address").equals(path, compareValue): Checks if the value at the specified path in the element's grain state is strictly equal (===) to the provided compareValue. "path" uses dot notation (e.g., "user.status").
These functions are called directly within your expressions, for example:
<div g-state="myState" g-init='{"count": 5}'>
<button g-disabled="!isPositive('count')">
Click me if count is positive
</button>
</div>The test suite provides comprehensive coverage of the core functionality and directives.
Tests are located in the src/tests directory.
Grains.js uses Vitest for testing. To run the tests:
- Clone the repository: git clone https://github.com/mk0y/grains.js.git
- Navigate to the project directory: cd grains.js
- Install dependencies:
npm install - Run tests:
npm test
We welcome contributions! Please see the CONTRIBUTING file for guidelines.
For any questions or inquiries, please contact: [email protected].
Or join our Discord channel.
MIT License. See LICENSE file for details.
- Improved Form Element Support: Add support for more form elements, starting with file inputs and potentially date pickers. This will involve extending the g-model directive and adding necessary validation and error handling.
- Enhanced Error Handling: Improve error messages and logging for invalid expressions and directive usage.
- Advanced Expression Support: Explore adding support for more advanced JavaScript expressions, such as ternary operators and more complex logical operations within the directives.
- Custom Directive Support: Allow developers to create and register their own custom directives, extending the core functionality of Grains.js. This will involve designing a clear and consistent API for custom directive creation.
- Performance Optimization: Further optimize performance for large-scale applications, focusing on reducing DOM manipulation and improving update efficiency.