Sprinkle JS
Sprinkle JS is the open source alternative to...well nothing!
Joking aside, is a javascript library that let you sprinkle reactivity inside
your vanilla imperative javascript. It uses a model very similar (yet far less
powerful) than SolidJS to handle reactivity
and it was heavily inspired by
this post
from Ryan Carniato the main developer behind it.
Before diving in the APIs and all the cool stuff it's useful to clarify what is
Sprinkle JS philosophy and what Sprinkle JS is not.
Philosophy
Have you ever been fiddling around in Codepen or in a
local html file and wondered "Man I wish I could have a bit of reactivity for
this stupid app i'm messing with".
Well Sprinkle JS is exactly what you need!
The main philosophy behind is to build a supersmall library that requires no
bundler that you can just drop in a script tag or import from a cdn and add a
bit a reactivity to your app. We made a bunch of utility functions to bind your
variables to your DOM in some way and that's all is needed. Open a new file,
drop in Sprinkle JS, declare your variables, declare your bindings and than
proceed to write your code without caring about updating the DOM...that's
Sprinkle JS work!
What is not
- Sprinkle JS is not the next big thing after React.
- Sprinkle JS is not for your big project...i mean if you want to use it
feel free to do so but it's not what's meant to be.
- Sprinkle JS is not for perf aficionados: we as a community are trying to
our best and maybe it will become the best version of itself but keep always
keep in mind the main philosophy behind it.## Installation To install Sprinkle
JS you can run
npm i sprinkle-js
This will install the npm package in your project and will let you import the
various exported methods with
import SprinkleJS from "sprinkle-js";
Tip: it's better to actually import the naming exports to allow for tree
shaking 🌳
import { createVariable } from "sprinkle-js";
Installing the library from npm will download a copy into your node_modules
and will be shipped together with your application when you publish it. This
will kind of negate the meaning of this library but you are free to use it like
this if you prefer.
To use the library for what is meant to be go on codepen or in a local html file
and paste this in the JS tab:
import { createVariable } from "https://cdn.skypack.com/sprinkle-js";
Sprinkle JS is available from all major cdn's
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sprinkle JS App</title>
<script src="https://unpkg.com/sprinkle-js/dist/sprinkle-js.iife.js"></script>
<script>
const { createVariable, createEffect } = SprinkleJS;
</script>
</head>
<body>
</body>
</html>
If you append dist/sprinkle-js.iife.js
to each of those links you can also
embed it in a script tag.
On Codepen
The easiest way to get started with Sprinkle JS is just by going on Codepen by
clicking
this.
This will bring you to codepen.io with the Sprinkle JS template.
On Stackblitz
You can quickly initialize a Stackblitz project setup with typescript and
Sprinkle JS by going to
this
template.
Tip: we try to keep this templates up to date but it's safer to always
update the dependencies as soon as you fork it.
On Codesandbox
You can quickly initialize a Code Sandbox project setup with typescript and
Sprinkle JS by going to
this
template.
Tip: we try to keep this templates up to date but it's safer to always
update the dependencies as soon as you fork it.
Demo
You can see this library in use here.
Authors
Contributing
Any contribution is welcomed, you can either open a
New Issue or read the
list of the open ones to
work on them.
A Contributing guide will be up ASAP.
//⚠ WIP
Documentation
You can check the docs here.
If you want to contribute to the docs website you can
file an issue to this
repository.
FAQ
Can i use Sprinkle JS in production?
Obviously you can...but i don't recommend it. The main focus of Sprinkle JS is
not to give you a fully fledget, bleeding edge, blazingly fast framework. Is
mainly to let you fiddle around in your fun little projects without the need to
setup a bundler or importing a huge codebase just to get a bit of reactivity.
How can i create a component in Sprinkle JS?
You can't. You could, in theory write a function that returns an array of child
nodes but components in the strict term are not part of the Sprinkle JS
philosophy. We want to mantain a super small bundle size.
Why i can't create a variable that is not an Object?
Sprinkle uses javascript
Proxyes
to handle reactivity and unfortunately you can't create a Proxy from a primitive
value.
Why i need to wrap everything in a function to use Sprinkle JS?
Sprinkle JS uses the same model of Vue or Solid to handle the reactivity. To
keep track of the dependencies of a function it saves that function in a stack
before calling it. This allows every variable accessed in that function to know
that it's used in that function. This has some drawbacks. If you don't wrap
everything inside a function the variable will be accessed before the effect can
save himself into the stack. If you want to understand this better you can check
this article
from Ryan Carniato or watch
this video from VueMastery on Vue
reactivity.
Why my createEffect does not re-run? I've used a reactive variable inside it.
It could be a lot of different things but probably is because you are running
asynchronous code inside of it. You can run asynchronous code inside a
createEffect but for it to track your dependencies you have to make sure to use
them before the async part. You can even just access it just by writing
variablename.fieldname;
and it will be correctly tracked.
Usage/Examples
setup
Given that Sprinkle JS uses the two core methods to do everything with this
method you can plug your own reactivity system inside Sprinkle JS. Why should
you do this? I don't know! For fun, or maybe because you already have
@vue/reactivity
in use in your application and you want to use it with Sprinkle JS methods.
To plug your own reactivity system into Sprinkle JS you need to provide a method
to createVariable and one for createEffect. Optionally you can provide an
override for createComputed. This will override the core functionalities and the
reactivity system of Sprinkle JS and every other method will use your reactivity
system instead.
Obviously there are constraints:
createEffect
needs to automatically check its own dependencies- ideally you want to return an object or a Proxy from
createVariable
- if you don't return a Proxy from
createVariable
you'll probably need to
provide also a createComputed
override
@vue/reactivity
provides a very similar set of core functionalities to Sprinkle JS so it's very
easy to plug it into it.
import { effect, reactive } from "@vue/reactivity";
import { setup } from "sprinkle-js";
setup({
createVariable: reactive,
createEffect: effect,
});
And that's it...from this moment on all the methods of Sprinkle JS will use
@vue/reactivity
as their reactivity system.
Warning while pretty similar the reactivity system of @vue/reactivity it's
a bit different so you might found some differences in behavior for some of
your applications.
The setup function returns a function to reset the setup.
import { effect, reactive } from "@vue/reactivity";
import { createEffect, createVariable, setup } from "sprinkle-js";
const reset = setup({
createVariable: reactive,
createEffect: effect,
});
const vueReactivityVar = createVariable({ whosCool: "you" });
createEffect(() => {
console.log(vueReactivityVar.whosCool);
});
reset();
const sprinkleVar = createVariable({ whosCool: "you" });
createEffect(() => {
console.log(sprinkleVar.whosCool);
});
N.B. once created inside a reactivity system the variable will continue to
use that reactivity system even after the reset.
While returning an object or a Proxy from createVariable
is preferable it's
not required. This means that if you want to use signals from
solid-js you can do something like this
import { createEffect as solidEffect, createSignal } from "solid-js";
import {
bindTextContent,
createEffect,
createVariable,
setup,
} from "sprinkle-js";
setup({
createVariable: (...props) => {
return createSignal(...props);
},
createEffect: (...props) => {
solidEffect(...props);
},
});
const [state, setState] = createVariable(1);
createEffect(() => {
console.log(state());
});
bindTextContent("#app", () => `The content of the state is ${state()}`);
createRef
This method is used to create a reactive variable for a primitive. It wraps the
primitive in an object with a value property.
const ref = createRef(1);
console.log(ref.value);
if you use this variable inside a createEffect or inside another method whenever
you'll update the value the method will re-run.
You can also pass an equality function that will determine how to check for
equality for this ref. By default it will use Object.is
.
const ref = createRef(1, (before, after) => before > after);
console.log(ref.value);
The above ref will not trigger an effect re-run if the previous value is greater
than the new value.
Please note: if you are using typescript you can't pass something different than
a primitive value to the ref and if you pass an equality function is recommended
to also use the generic version of the fucntion
createRef<number>(1, (before, after)=> before > after)
otherwise Typescript
will narrow the type to a constant. If you are using this in Javascript you are
allowed to pass object to the ref (although is not recommended since you'll have
to access it unnecessarly via the .value
property), the object passed will be
deeply reactive.
createVariable
This method is used to create a reactive variable for an object. It'll throw if
you try to pass a primitive value to it.
const variable = createVariable({ whosCool: "you" });
console.log(variable.whosCool);
if you use this variable inside a createEffect or inside another method whenever
you'll update the value the method will re-run.
You can also pass an object containing an equality function for every field of
the object that will determine how to check for equality for that field. By
default it will use Object.is
.
const variable = createVariable({ whosCool: "you" }, {
whosCool: (before, after) => before.length === after.length,
});
console.log(variable.whosCool);
The above variable will not trigger an effect re-run if the previous value has
the same length as the new value.
createVariable works with nested property too. You can pass object inside
objects and updating a values inside those object will trigger the rerun of the
effects where it's been used. To pass an equality function relative to a nested
object you should pass an object containing the properties of the object you
want a particular equality function. If you pass a built in object tho (like
Set, Map or HTMLElement) it will not become reactive. If you use a nested object
you can pass an object with the same structure to the equality functions object
to specify an equality function for every field. If you pass a function the
equality function will not be applied to the nested object.
const variable = createVariable({ whosCool: { name: "you" } }, {
whosCool: { name: (before, after) => before.length === after.length },
});
const variable = createVariable({ whosCool: { name: "you" } }, {
whosCool: (before, after) => before.length === after.length,
});
const variable = createVariable({ whosCool: { pronoun: "you" } }, {
whosCool: { pronoun: (before, after) => before.length === after.length },
});
console.log(variable.whosCool.pronoun);
in the above example variable.whosCool.pronoun
is still reactive.
createCssVariable
This method is used to create a reactive variable for an object and map every
field to a css variable. It'll throw if you try to pass a primitive value to it.
In typescript you can pass only string or numbers as values of the object but
every value will be stringified before assigning it to the css variable so if
you pass a complex object you'll get "[object Object]" instead.
const variable = createCssVariable({ x: 0, y: 0 });
variable.x = 200;
if you use this variable inside a createEffect or inside another method whenever
you'll update the value the method will re-run.
You can also pass an object containing an equality function for every field of
the object that will determine how to check for equality for that field. By
default it will use Object.is
.
const variable = createCssVariable({ whosCool: "you" }, {
whosCool: (before, after) => before.length === after.length,
});
The above variable will not trigger an effect re-run if the previous value has
the same length as the new value.
A third argument of this method is a selector or an HTMLElement of the element
you want to apply the css variables to. It default to :root
. If the provided
selector does not select anything it will apply the css variable to the :root
createStored
This method is used to create a reactive variable for an object also persisting
it in localStorage or sessionStorage. It'll throw if you try to pass a primitive
value to it. It will also automatically add a listener for the storage to update
the variable whenever the storage changes. It will take a key and an initial
value as input but will discard the initial value if the key is already present
in the storage. It will also throw if the object in the storage is not
Object-like
const variable = createStored("cool-stored", { whosCool: "you" });
console.log(variable.whosCool);
console.log(window.localStorage.getItem("cool-stored"));
if you use this variable inside a createEffect or inside another method whenever
you'll update the value the method will re-run.
You can also pass an object containing an equality function for every field of
the object that will determine how to check for equality for that field. By
default it will use Object.is
.
const variable = createStored("cool-stored", { whosCool: "you" }, {
whosCool: (before, after) => before.length === after.length,
});
console.log(variable.whosCool);
The above variable will not trigger an effect re-run if the previous value has
the same length as the new value.
Differently from createVariable
a stored object is not deeply reactive.
createComputed
This method is used to create a reactive computed variable. You need to pass a
function that will return a value. The return value of the function will be
inside of the .value
field of the returned computed. If you use some other
variable to compute the this value it will always be in sync.
const variable = createVariable({ whosCool: "you" });
const computed = createComputed(() => `${variable.whosCool} is cool!`);
console.log(computed.value);
variable.whosCool = "whoever uses SprinkleJS";
console.log(computed.value);
if you use this variable inside a createEffect or inside another method it will
be rerunned whenever this computed changes. You can't set the value of a
computed value and trying would still result in the same value being inside that
computed.
You can also pass an equality function that will determine how to check for
equality for this computed. By default it will use Object.is
.
const variable = createVariable({ whosCool: "you" });
const computed = createComputed(
() => `${variable.whosCool} is cool!`,
(before, after) => before.length === after.length,
);
console.log(computed.value);
variable.whosCool = "whoever uses SprinkleJS";
console.log(computed.value);
The above ref will not trigger an effect re-run if the previous value has the
same length as the new value.
Another simple way to create a computed value if by defining a function that
return a value using a reactive variable like this
const variable = createVariable({ whosCool: "you" });
const computed = () => `${variable.whosCool} is cool!`;
console.log(computed());
variable.whosCool = "whoever uses SprinkleJS";
console.log(computed());
As you can see in the example, this require you to call the function to access
the value (instead of accessing it by .value
)
However this second method will rerun the effect it's used in even if it has the
same value as before.
createEffect
This method is used to create an effect that will keep track of it's
dependencies and re-run every time they changed
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
createEffect(() => {
console.log(variable.whosCool, ref.value);
});
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
You can also return a function that will be run before the new function to clean
up the previous effect.
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
createEffect(() => {
console.log(variable.whosCool, ref.value);
return () => {
console.log("cleaning up");
};
});
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
untrack
This function can be used inside a createEffect to untrack a dependency. This
way you can use a reactive variable inside a create effect without triggering
the re-run when that variable changes. It takes a function as input and it
return anything returned from that function.
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
createEffect(() => {
const refValue = untrack(() => ref.value);
console.log(variable.whosCool, refValue);
});
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
batch
This function can be used to avoid running effects multiple times when changing
multiple variables. It's as simple as calling batch and passing a function that
will change some variables.
const variable = createVariable({ name: "John", lastName: "Doe" });
createEffect(() => {
console.log(`The full name is ${variable.name} ${variable.lastName}`);
});
batch(() => {
variable.name = "Albert";
variable.lastName = "Einstein";
});
bindTextContent
This function is used to bind a string value to the text content of an element.
It takes a dom element or a selector as the first argument and a function
returning the value to bind to the text content as the second argument.
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
bindTextContent("#div-to-bind", () => `${ref.value} ${variable.whosCool}`);
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
The callback you pass in also takes the element as the first argument, in
Typescript you can pass a generic type specifying what kind of element you are
expecting
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
bindTextContent<HTMLDivElement>(
"#div-to-bind",
(element: HTMLDivElement) =>
`${element?.textContent} ${ref.value} ${variable.whosCool}`,
);
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
If you need to have access to the selected element (to add event listeners for
example), the element is returned from the function.
const divToBind = bindTextContent<HTMLDivElement>(
"#div-to-bind",
(element: HTMLDivElement) =>
`${element?.textContent} ${ref.value} ${variable.whosCool}`,
);
bindInnerHTML
Warning Sprinkle JS does not sanitize the content of the innerHTML. If you
use this function with user input make sure to sanitize it first to avoid
expose yourself to XSS attacks.
This function is used to bind a string value to the innerHTML of an element. It
takes a dom element or a selector as the first argument and a function returning
the value to bind to the innerHTML as the second argument.
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
bindInnerHTML(
"#div-to-bind",
() => `<span>${ref.value} <strong>${variable.whosCool}</strong></span>`,
);
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
The callback you pass in also takes the element as the first argument, in
Typescript you can pass a generic type specifying what kind of element you are
expecting
const variable = createVariable({ whosCool: "you" });
const ref = createRef(1);
bindInnerHTML<HTMLDivElement>(
"#div-to-bind",
(element: HTMLDivElement) =>
`<span>${element?.textContent} ${ref.value} <strong>${variable.whosCool}</strong></span>`,
);
ref.value++;
variable.whosCool = "whoever uses Sprinkle JS";
If you need to have access to the selected element (to add event listeners for
example), the element is returned from the function.
const divToBind = bindInnerHTML<HTMLDivElement>(
"#div-to-bind",
(element: HTMLDivElement) =>
`${element?.textContent} ${ref.value} ${variable.whosCool}`,
);
bindInputValue
This function is used to bind a string value to the value of an input element.
It takes an input dom element or a selector as the first argument and a function
returning the value to bind to the input value as the second argument.
The following code will bind the input value to the variable.whosCool
field.
Note that is a one-way binding so make sure to also add an event listener on the
input to complete the flow. We are saving the return value of the function into
bindInputValue
to later add the event listener to it.
const variable = createVariable({ whosCool: "you" });
const inputToBind = bindInputValue("#input-to-bind", () => variable.whosCool);
inputToBind.addEventListener("input", (e) => {
variable.whosCool = e.target.value;
});
The callback you pass in also takes the element as the first argument.
const variable = createVariable({ whosCool: "you" });
const inputToBind = bindInputValue(
"#input-to-bind",
(element) => element.innerText + " " + variable.whosCool,
);
inputToBind.addEventListener("input", (e) => {
variable.whosCool = e.target.value;
});
bindDom
This function is used to bind an object that describe some DOM properties to the
actual DOM properties. It takes a dom element or a selector as the first
argument and a function returning the object as the second argument.
The following code will bind the ariaLabel value to the variable.whosCool
field and the checked value for the checkbox.
const variable = createVariable({ whosCool: "you" });
bindDom("#checkbox", (element) => ({
ariaLabel: variable.whosCool,
checked: variable.whosCool === "you",
}));
bindClass
This function is used to bind a class to the actual element. It takes a dom
element or a selector as the first argument, the class that you want to apply
and a function returning a boolean as the third argument.
const variable = createVariable({ count: 0 });
bindClass("#div-to-bind", "dark", (element) => variable.count % 2 === 0);
variable.count++;
variable.count++;
bindClasses
This function is used to bind multiple classes to the actual element. It takes a
dom element or a selector as the first argument, and an object containing the
classes that you want to apply as the keys and a boolean as the value. Each
class will be applied only if the corrispondent value is true.
const variable = createVariable({ count: 1 });
bindClasses("#to-bind", () => ({
one: variable.count === 1,
two: variable.count === 2,
three: variable.count === 3,
lessThanFive: variable.count < 5,
}));
variable.count++;
variable.count++;
variable.count++;
variable.count++;
bindStyle
This function is used to bind an object that describe the style of an element to
the actual element style. It takes a dom element or a selector as the first
argument and a function returning the object as the second argument.
The following code will bind color property and the backgroundColor property
value to the variable.color
and variable.bg
field.
const variable = createVariable({ color: "black", bg: "#BADA55" });
bindStyle("#div-to-bind", (element) => ({
color: variable.color,
backgroundColor: variable.bg,
}));
variable.bg = "#C0FFEE";
variable.color = "white";
bindChildren
This function is used to bind html as the children of an element. How is this
different than bindInnerHTML? Each item can have a key attribute and if the key
attribute does not change the element will not change either (it will have the
same reference in the dom). It takes a dom element or a selector as the first
argument, a function returning a document fragment or a string or an array of document fragments of an array of strings as the second argument and an option function to run after the diffing as the third parameter.
The third argument can be useful to bind something to the newly created element. It takes the root element and a Map object as parameter where the keys are all the keys you've specified in the template and the values are the element associated with that key that is on the DOM. Every element has an isNew flag that specifies if it's a newly added element or an old one
Given that sometimes is difficult to build a document fragment from scratch Sprinkle JS an html
tagged template to transform your string into an actual document fragment. Inside this tagged template literal is possible to include events with the syntax on:eventname
and there's a special event on:bind
to run code whenever the element will be binded to the DOM by sprinkle JS. This event will get the actual element as the parameter and it's possible to use this to call other Sprinkle JS methods on that specific element. For example:
const variable = createVariable({inputVal: ""});
bindChildren("#div-to-bind", ()=> html`
<input
on:bind=${(inputElement)=>{
bindInputValue(inputElement, ()=> variable.inputVal)
}}
on:input=${(e)=>{
variable.inputVal= e.target.value;
}} />
`);
will bound to the div #div-to-bind
an input as children and will bind the inputValue of it variable.inputVal
.
Tip: this is a better way than the afterRun function that bindChildren takes as a third argoument to bind something to an element.
Every array inside the tagged template literal will be treated as separate elements. So for example
html`${[1,2,3,4].map(num => html`
<button>${num}</button>
`)}`
will return a fragment with 4 buttons.
As you've seen from this previous example one small caveat of this function is that you have to repeat the tag at each new "level" (you can skip the first one since everything you return will still get passed through the html
function automatically but you have to include it inside the map function) but this allow for enaugh composability that you can actually start to create some sort of components.
A component it's simply a function that return the return value of an html
tagged template literal. The above example could be rewritten like so
const MagicButton = ({num})=>{
return html`<button>${num}</button>`
}
html`${[1,2,3,4].map(num => MagicButton({num}))}`
Those are all small examples so here's a fully featured one
const variable = createVariable({
listOfCoolThings: [
"you",
"sprinkle-js",
"javascript",
],
});
bindChildren(
"#ul-to-bind",
(element) => variable.listOfCoolThings.map((coolThing) => html`
<li key="${coolThing}">
<button
key="${coolThing}-button"
on:click=${()=>{
console.log(`You've pressed the ${coolThing} button`);
}}
>Log ${coolThing}</button>
</li>`
),
(element, elements) => {
const youElement = elements.get("you");
bindClasses(youElement, ()=>({
"classForYou": true,
});
},
);
variable.listOfCoolThings = [...variable.listOfCoolThings, "npm"];
A small caveat is that given that every nested element get's diffed you should add a key to every element that you want to preserve the reference to.
Warning bindChildren changed api's over time so make sure you are on the latest version o refer to previous versions of this readme for the old documentation.
Running Tests
To run tests, run the following command
npm run test