A plug-n-play reactive library for building web applications.
Using a CDN is the easiest way to get started with the library. You can include the following script tag in your HTML file to get started:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Wicked App</title>
</head>
<body>
<div *state="{ count: 0 }">
<h1 *text="count"></h1>
<button *on[click]="count++" type="button">Increment</button>
<button *on[click]="count--" type="button">Decrement</button>
</div>
<script type="module">
import { render } from 'https://esm.sh/wickedstate@0.1.2';
render(document.body).then(() => {
console.log('App is ready');
});
</script>
</body>
</html>
We are using esm.sh CDN in the example above, you can replace it with any other CDN of your choice that supports ES Modules e.g https://cdn.skypack.dev/wickedstate@0.1.2
.
If you don't have a Vite project already, you can create a new project with the package manager of your choice using the following commands:
- NPM:
npm create vite@latest my-wicked-app -- --template vanilla-ts
- Yarn:
yarn create vite my-wicked-app --template vanilla-ts
- PNPM:
pnpm create vite my-wicked-app --template vanilla-ts
- BUN:
bun create vite my-wicked-app --template vanilla-ts
- Deno:
deno run -A npm:create-vite@latest --template vanilla-ts my-wicked-app
You can replace my-wicked-app
with the name of your project and also replace vanilla-ts
with vanilla
if you prefer JavaScript.
Then navigate to the project directory:
cd my-wicked-app
Then install the library using the package manager of your choice:
- NPM:
npm install wickedstate
- Yarn:
yarn add wickedstate
- PNPM:
pnpm add wickedstate
- BUN:
bun add wickedstate
- Deno:
deno add jsr:@devhammed/wickedstate
And open src/main.ts
in your editor and replace the content with the following:
import { render } from 'wickedstate';
render(document.body).then(() => {
console.log('App is ready');
});
Then open the index.html
file and replace the content with the following:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + TS</title>
</head>
<body>
<div *state="{ count: 0 }">
<h1 *text="count"></h1>
<button *on[click]="count++" type="button">Increment</button>
<button *on[click]="count--" type="button">Decrement</button>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
And then run the following command to start the development server:
npm run dev
Go to http://localhost:5173
in your browser to see the app in action.
A directive is a special HTML attribute that is recognized by the library which instructs it what to do with the element.
The syntax for a directive is <tag *name[type].modifiers[value]="expression" />
where:
tag
is the HTML tag name e.g.div
,button
,input
, etc.name
is the name of the directive e.g.state
,on
,text
, etc.type
serves as an identifier for the directive, and it is optional but some directives likeon
requires it to differentiate between different events e.g*on[click]
,*on[submit]
, etc.modifiers
are used to modify the behavior of the directive, you can repeat modifiers but not on the same directive e.g.*on[click].once
,*on[click].prevent
,*on[click].once.prevent
, etc.value
is the value of a modifier for the ones that requires it e.g.*on[input].debounce[500ms]
,*on[resize].window.debounce[300ms]
, etc.expression
is the attribute value that will be evaluated as a JavaScript expression e.g.*state="{ count: 0 }"
,*on[click]="count++"
, etc.
You can also notice that the directive is prefixed with an asterisk *
to differentiate it from a regular HTML attribute.
Now let's take a look at the available built-in directives:
Declares a component and its data for a block of HTML elements.
<div *state="{ count: 0 }">
...
</div>
You can reuse a state object across multiple elements by using the data
function exported from the library.
import { data, render } from 'wickedstate';
data('postItem', (id) => ({
id,
postData: null,
init() {
// Fetch post from server
},
like() {
// like post
},
unlike() {
// unlike post
},
}));
render(document.body).then(() => {
console.log('App is ready');
});
Then call the name of the state object in the *state
directive like a function (this allows you to pass arguments to the state object).
<div *state="{ postIds: [1, 2, 3, 4] }">
<template *for="id in postIds : id">
<div *state="postItem(id)">
...
</div>
</template>
</div>
You can also define lifecycle hooks for your state object by adding the following methods:
init
- Called when the state object is initialized.destroy
- Called when the state object is destroyed.
Below is a timer example that increments a counter every second and clears the interval when the component is destroyed:
<div
*state="{
count: 0,
interval: null,
init() {
this.interval = setInterval(() => {
this.count++;
}, 1000);
},
destroy() {
clearInterval(this.interval);
}
}"
>
<h1 *text="count"></h1>
</div>
This library also supports single-element states, which means you can declare a state object for a single element and also use other directives right on the element.
<button *state="{ label: 'Click Here' }" *text="label" *on[click]="alert('Clicked')"></button>
Listen for browser events on an element
<button *on[click]="count++" type="button">Increment</button>
If you wish to access the native JavaScript event object from your expression, you can use $event
magic property:
<input *on[input]="console.log($event.target.value)" type="text" />
The on
directive also supports the following modifiers:
Listen to the event only once.
<button *on[click].once="count++" type="button">Increment</button>
Prevent the default behavior of the event.
<form *on[submit].prevent="alert('Form submitted!')">
<button type="submit">Submit</button>
</form>
Stop the propagation of the event.
<div *on[click]="alert('Div clicked!')">
<button *on[click].stop="alert('Button clicked!')" type="button">Click Me</button>
</div>
Listen for the event on the window object.
<input type="search" *on[keydown].esc.window="console.log('Escape key pressed!')"/>
Listen for the event on the document object.
<input type="search" *on[keydown].esc.document="console.log('Escape key pressed!')"/>
Listen for the event only if the event was dispatched from the element itself.
<button type="button" *on[click].self="alert('Div clicked!')">
Click Me
<img src="https://placekitten.com/200/300" alt="Kitten" />
</button>
With the self
modifier, the alert will only be triggered if the button itself is clicked and not the image inside the button.
Debounce or throttle the event listener.
<input type="search" *on[input].debounce[500ms]="console.log($event.target.value)"/>
<input type="search" *on[input].throttle[500ms]="console.log($event.target.value)"/>
The debounce
modifier will wait for the specified time before executing the expression while the throttle
modifier will execute the expression at most once every specified time.
For the duration, the time can be specified in milliseconds ms
or seconds s
or minutes m
e.g. 500ms
, 1s
, 2m
, etc.
You can add .passive
to your listeners to not block scroll performance when on touch devices.
<div *on[scroll].passive="console.log('Scrolling...')">
...
</div>
Execute the event listener during the capture phase of the event.
<div *on[click].capture="console.log('Capturing...')">
...
</div>
Listen for the event if it was dispatched outside the element.
<div *state="{ open: false }" *on[click].away="open = false">
<button *on[click]="open = ! open" type="button">Toggle</button>
<div *show="open">This is a dropdown</div>
</div>
You can also use the following keyboard modifiers to tweak your keyboard event listeners:
.esc
- Escape key.enter
- Enter key.space
- Space key.tab
- Tab key.meta
- Meta/Command/Windows/Super key (aliased tocmd
,super
).ctrl
- Control key.alt
- Alt key.shift
- Shift key.backspace
- Backspace key.delete
- Delete key.caps
- Caps Lock key.slash
- Slash key.period
- Period/Dot/Full Stop key.equal
- Equal/Plus key.comma
- Comma key.up
- Up arrow key.down
- Down arrow key.left
- Left arrow key.right
- Right arrow key
Sets the text content of an element.
<h1 *text="count"></h1>
Sets the inner HTML of an element (Only use on trusted content and never on user-provided content.
<div *html="post.content"></div>
Toggle the visibility of an element based on the truthiness of an expression.
<div *state="{ open: false }">
<div *show="open">This is a hidden content</div>
<button *on[click]="open = !open" type="button">Toggle</button>
</div>
Reference elements directly by their specified keys using the $refs
magic property.
<button data-text="Copy Me" *ref="copyButton" *on[click]="navigator.clipboard.writeText($refs.copyButton.dataset.text)">
Copy
</button>
Conditionally render an element based on the truthiness of an expression.
<div *state="{ loggedIn: false }">
<template *if="loggedIn">
<div>Welcome back!</div>
</template>
<template *if="! loggedIn">
<div>Please login to continue</div>
</template>
<button *on[click]="loggedIn = !loggedIn" type="button" *text="loggedIn ? 'Logout' : 'Login'"></button>
</div>
NOTE: *if
MUST be declared on a <template>
element and that <template>
element MUST contain only one root element.
Loop over an array or object and render a template for each item.
<template *for="post in posts">
<h2 *text="post.title"></h2>
</template>
You can also get the index of the current item by using the following syntax:
<template *for="(post, index) in posts">
<h2 *text="index + 1 + '. ' + post.title"></h2>
</template>
It is also important to specify a unique key for each item in the list to help the library keep track of the items and update the DOM efficiently. You can do this by adding a colon :
after the in
keyword followed by the key expression.
<template *for="post in posts : post.id">
<h2 *text="post.title"></h2>
</template>
NOTE: *for
MUST be declared on a <template>
element and that <template>
element MUST contain only one root element.
Two-way data binding for form elements.
<input *model="name" type="text" />
<p *text="name"></p>
Instruct the library to skip processing a node and all of its children.
<input *ignore type="datetime-local" onload="new Pikaday(this)" />
You can use the self
modifier to skip the element but process its children.
You can use this directive in conjunction with CSS to hide an element until it is ready to be processed to prevent UI flashes.
<style>
[\*cloak] {
display: none !important;
}
</style>
<div *state="{ open: false }">
<div *cloak *show="open">This is a hidden content</div>
<button *on[click]="open = !open" type="button">Toggle</button>
</div>
Magics are special properties that are available in the state object and can be used in expressions.
They are prefixed with the $
character to prevent conflicts with your normal state properties.
The $root
magic property gives you access to the element where the state object was declared.
<div *state="{}" data-message="Hello World!">
<button type="button" *on[click]="alert($root.dataset.message)">
Greet Me
</button>
</div>
The $el
magic property gives you access to the current element.
<button *on[click]="$el.innerHTML = 'Hello World!'">Replace me with "Hello World!"</button>
The $refs
magic property gives you access to the elements with the ref
directive.
<button data-text="Copy Me" *ref="copyButton" *on[click]="navigator.clipboard.writeText($refs.copyButton.dataset.text)">
Copy
</button>
The $parent
magic property gives you access to the parent state object.
<div *state="{ count: 0 }">
<h1 *text="count"></h1>
<button *state="{ text: 'Increment' }" *on[click]="$parent.count++" *text="text"></button>
</div>
The $get
magic property allows you to access the value of a state property using dot-syntax.
<div *state="{ user: { name: 'John Doe' } }">
<p *text="$get('user.name')"></p>
</div>
The $set
magic property allows you to update the value of a state property using dot-syntax.
<div *state="{ user: { name: 'John Doe' } }">
<input *model="name" type="text" />
<button *on[click]="$set('user.name', 'Jane Doe')" type="button">Update Name</button>
</div>
The $watch
magic property allows you to watch for changes on a state property.
<div
*state="{
count: 0,
init() {
this.$watch('count', (value, oldValue) => {
console.log(`Count changed from ${oldValue} to ${value}`);
});
},
}"
>
<button type="button" *on[click]="count++">
Trigger Watch
</button>
</div>
The $effect
magic property will run a function on mount and whenever one of the state properties used in it changes.
<div
*state="{
count: 0,
double: 0,
init() {
this.$effect(() => {
this.double = this.count * 2;
});
},
}"
>
<h1 *text="double"></h1>
<button type="button" *on[click]="count++">
Double Increment
</button>
</div>
The $data
magic property gives you access to the state object, useful for when you want to send the whole thing to an API.
<div *state="{ count: 0 }">
<button type="button" *on[click]="fetch('https://httpbin.org/post', { method: 'POST', body: JSON.stringify($data) })">
Increment
</button>
</div>
- Hammed Oyedele - Author
- AlpineJS - Inspiration
- VueJS - Inspiration