Mates
Mates is a lightweight framework that focuses on developer experience. It lets you build great web applications easily with it's awesome state management system. It's typescript First Framework. supports typescript all the way.
🚀 Features (MATES)
- Mutable State: Mutate state directly through setters to update components (views)
- Actions: Function utilities to help with business logic
- Templates: Template functions that return lit-html result
- Events: Event Utilites for event-driven development
- Setup functions: These are functions to intialise state needed for the view
📦 Installation
npm install mates
yarn add mates
🧠 Core Concepts
👁 Views: Components of Mates
Views are similar to components in react. They are closure functions with outer function aka Setup function (the S from the MATES), and inner function is called Template function (the T from MATES) which returns html template string.
import { renderView, setter, html } from "mates";
const CounterView = (props) => {
let count = 0;
const incr = setter(() => count++);
return () => html` <div>
<h1>Count: ${count}</h1>
<button @click=${incr}>Increment</button>
</div>`;
};
renderView(CounterView, "app");
count is a local let variable that holds value. which can only be changed froma setter function.
incr is a setter function, that when called, updates the view. it has to be non-async.
Scopes
Mates introduces a new feature called Scopes. an amazing way to share state with child views (components). using Scopes, child components can access parent's state directly without using props.
Formatting Support:
you can install lit-html plugin on your IDE like vs code or cursor for proper formatting for your template strings.
props()
In Mates, props can be passed to child views as object. props is a funciton and not an object but it's passed as object to child component. the view will have to call props() to get the projects object.
const CounterView = () => {
let count = 0;
let incr = setter(() => count++);
return () => html`
<div>count is: ${count}</div>
<div>${view(ChildView, { count })}</div>
`;
};
const ChildView = (props: Props<{ count: number }>) => {
return html`parent count is : ${props().count}`;
};
note please don't destructure props into local variables in the outer function, as it breaks connection from the parent object. but you can do this in the inner function as inner function gets executed everytime the parent view is updated. so you will have always the latest value.
⚛️ Atoms: Simple Reactive State
Atoms hold mutable values. they can hold any Javascript value like primitive or objects or maps or sets.etc They store a single value that can be read, set (replaced with new value), or updated (if it's an object). They support typescript fully.
import { atom } from "mates";
const username = atom("guest");
console.log(username());
console.log(username.get());
username.set("alice");
username.set((val) => val.toUpperCase());
const address = atom({ street: "" });
address.update((s) => (s.street = "newstreet"));
const nums = atom(new Map());
nums.update(m=>m.set(1, "one"));
const nums = atom(new Set([1,2,3]);
nums.update(s=>s.add(4));
nums();
Counter app using atoms
const CounterView = (props) => {
const count = atom(0);
const incr = count.set(count() + 1);
return () => html` <div>
<h1>Count: ${count}</h1>
<button @click=${incr}>Increment</button>
</div>`;
};
Units: Independent Object-Based State
Units are perfect for managing object-based state with methods and building store utilities.
Units have the following pieces
- Data: any type of javascript value
- Setters: methods whose name start with (_)
- Getters: methods that returns data without changing it
- Actions: async or non-async methods that call getters or setters for getting, setting data.
import { unit } from "mates";
const todoList = unit({
users: [],
isLoading = false,
_setIsLoading(value) {
this.isLoading = value;
},
_setUsers(newUsers) {
this.users = newUsers;
},
async loadUsers() {
this._setIsLoading(true);
this._setUsers(await fetch("/users").then((d) => d.json()));
this._setIsLoading(false);
},
getUsersCount() {
return this.users.length;
},
});
📊 Getters: Computed Values
Getters create computed values that only recalculate when their dependencies change.
import { atom, getter } from "mates";
const firstName = atom("John");
const lastName = atom("Doe");
const fullName = getter(() => {
return `${firstName()} ${lastName()}`;
});
console.log(fullName());
firstName.set("Jane");
console.log(fullName());
🧬 Molecules: group atoms or units or getters into one molecule
import { molecule, atom } from "mates";
class UserStore {
name = atom("Guest");
isLoggedIn = atom(false);
login(username) {
this.name.set(username);
this.isLoggedIn.set(true);
}
logout() {
this.name.set("Guest");
this.isLoggedIn.set(false);
}
}
const userStore = molecule(UserStore);
console.log(userStore().name());
userStore().login("Alice");
console.log(userStore().isLoggedIn());
🔄 XProvider: Context Management
XProvider allows you to provide and consume context across your application.
import { html } from "lit-html";
import { view, useContext } from "mates";
class ThemeContext {
theme = "light";
toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
}
}
const ThemeProvider = view(
(props) => {
const themeContext = new ThemeContext();
return () => html`
<x-provider .value=${themeContext}> ${props().children} </x-provider>
`;
},
{ children: [] }
);
const ThemedButton = view(() => {
const theme = useContext(ThemeContext);
return () => html`
<button class="${theme.theme}-theme" @click=${() => theme.toggleTheme()}>
Toggle Theme (Current: ${theme.theme})
</button>
`;
}, {});
🎮 Complete Example
Here's a complete todo list example that showcases Mates' features:
import { html } from "lit-html";
import { view, bubble, atom } from "mates";
const todos = bubble((setter) => {
let items = [];
let newTodoText = "";
const setNewTodoText = setter((text) => {
newTodoText = text;
});
const addTodo = setter(() => {
if (newTodoText.trim()) {
items.push({ text: newTodoText, completed: false });
newTodoText = "";
}
});
const toggleTodo = setter((index) => {
items[index].completed = !items[index].completed;
});
const deleteTodo = setter((index) => {
items.splice(index, 1);
});
return () => ({
items,
newTodoText,
setNewTodoText,
addTodo,
toggleTodo,
deleteTodo,
});
});
const TodoApp = view(() => {
return () => {
const {
items,
newTodoText,
setNewTodoText,
addTodo,
toggleTodo,
deleteTodo,
} = todos();
return html`
<div class="todo-app">
<h1>Todo List</h1>
<div class="add-todo">
<input
value=${newTodoText}
@input=${(e) => setNewTodoText(e.target.value)}
@keypress=${(e) => e.key === "Enter" && addTodo()}
placeholder="Add new todo"
/>
<button @click=${addTodo}>Add</button>
</div>
<ul class="todo-list">
${items.map(
(item, index) => html`
<li class=${item.completed ? "completed" : ""}>
<input
type="checkbox"
.checked=${item.completed}
@change=${() => toggleTodo(index)}
/>
<span>${item.text}</span>
<button @click=${() => deleteTodo(index)}>Delete</button>
</li>
`
)}
</ul>
<div class="todo-stats">
<p>${items.filter((item) => !item.completed).length} items left</p>
</div>
</div>
`;
};
}, {});
document.body.appendChild(TodoApp);
🔄 Why Mates?
Mates gives you the power and simplicity of React hooks without the React! As a complete framework, it's perfect for:
-
Building lightweight web apps without other heavy frameworks
-
Adding reactivity to existing applications
-
Creating reusable, reactive components
-
Prototyping ideas quickly
📚 Learn More
Check out our examples to see more usage patterns and advanced framework features.
📄 License
MIT