Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
yesil committed Nov 14, 2024
0 parents commit 46b9434
Show file tree
Hide file tree
Showing 25 changed files with 7,941 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
quote_type = single
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

meta.json

*.tgz
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript"
]
}
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Pico State Manager

Live demo: https://yesil.github.io/picosm

## Introduction

This is an experimental lightweight, zero dependency state manager that attempts to replicate core MobX features without using Proxy objects.

It also provides a helper function to make Lit components observers and updates them on changes.

Currently, the non minified bundle size is `4938 bytes` and is around `~1.2kb` gzipped.

## Partially Supported Features

- `makeObservable` (instance level change detection)
- `observe` (callback when the instance of an observable class changes)
- `reaction` (react to specific changes)
- `computed` (cache computed values)

Also, an opinionated approach for tracking nested dependencies with `track/untrack` functions is added. If an observable wants to get notified by another one, these functions can be used.

## Why Yet Another State Manager

Libraries like Redux, Zustand, and Nanostores treat the state as a typeless object and require updating to the next version of the state manually. This results in verbose code with application logic scattered around in various functions, making it hard to understand.

## How to Use

See the demo app source code: https://github.com/yesil/picosm/tree/main/demo-app

## Contributing

Please feel free to:

Open a PR to contribute.
Create an issue to request a feature.

```
```
171 changes: 171 additions & 0 deletions app/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { LitElement, html, css } from 'https://jspm.dev/npm:lit@2';
import { StoreObservable } from './model.js';
import { makeObserver, reaction } from '../dist/picosm.js';

/**
* also see ../index.html
*/
export class App extends LitElement {
static properties = {
store: {},
lastProductTitle: {},
};

static styles = [
css`
:host {
display: flex;
flex-direction: column;
padding: 32px;
background-color: var(--spectrum-global-color-gray-100);
color: var(--spectrum-global-color-gray-900);
}
.cards {
display: flex;
justify-content: space-between;
flex: 1;
}
[slot="footer"] {
display: flex;
gap: 20px;
align-items: center;
}
.cart {
margin-top: 40px;
width: 800px;
}
sp-card {
width: 220px;
min-height: 350px;
}
sp-number-field {
width: 80px;
}
.reaction {
opacity: 0;
margin-top: 20px;
}
@keyframes fadeOut {
0% {
opacity: 1;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fadeOut {
animation-duration: 3s;
animation-name: fadeOut;
}
`,
];

constructor() {
super();
this.store = new StoreObservable();
this.reactionDisposer = reaction(
this.store,
({ lastProduct }) => [lastProduct?.title],
(title) => {
const reaction = this.shadowRoot.querySelector('.reaction');
reaction.classList.remove('fadeOut');
void reaction.offsetWidth; // Trigger reflow to restart the animation
reaction.classList.add('fadeOut');
this.lastProductTitle = title;
},
);
}

disconnectedCallback() {
super.disconnectedCallback();
this.reactionDisposer?.();
}

get cards() {
return this.store.products.map(
(product, index) => html`
<sp-card heading="${product.title}" subheading="$${product.price}">
<img
slot="preview"
src="https://picsum.photos/id/${(index + 1) * 10}/200/300"
alt="Product Image ${index}"
/>
<div slot="footer">
${product.quantity === 0
? html`
<sp-button
variant="accent"
@click="${() => this.store.addToCart(product)}"
>Add</sp-button
>
`
: ''}
${product.quantity > 0
? html`
<sp-field-label
side-aligned="start"
for="card-quantity-${index}"
>Quantity</sp-field-label
>
<sp-number-field
id="card-quantity-${index}"
value="${product.quantity}"
@change="${(e) =>
this.store.updateQuantity(product, e.target.value)}"
></sp-number-field>
`
: ''}
</div>
</sp-card>
`,
);
}

get cart() {
const cartItems = this.store.products
.filter((p) => p.quantity > 0)
.map(
(p) => html`
<sp-table-row>
<sp-table-cell>${p.title}</sp-table-cell>
<sp-table-cell>${p.quantity}</sp-table-cell>
<sp-table-cell>$${p.total}</sp-table-cell>
</sp-table-row>
`,
);
return html` <p>Cart Total: $${this.store.total}</p>
<sp-table size="m">
<sp-table-head>
<sp-table-head-cell>Name</sp-table-head-cell>
<sp-table-head-cell>Quantity</sp-table-head-cell>
<sp-table-head-cell>Total</sp-table-head-cell>
</sp-table-head>
<sp-table-body> ${cartItems} </sp-table-body>
</sp-table>`;
}
render() {
return html`
<div class="cards">${this.cards}</div>
<div class="cart">
${this.cart}
<div class="reaction">
Last interacted product:
<sp-badge variant="negative">${this.lastProductTitle}&nbsp;</sp-badge>
</div>
</div>
`;
}
}

customElements.define('pico-demo', makeObserver(App, ['store']));
11 changes: 11 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import './App.js';
import { ProductObservable } from './model.js';

const products = [
new ProductObservable('Product A', 10),
new ProductObservable('Product B', 15),
new ProductObservable('Product C', 22),
];

const app = document.querySelector('pico-demo');
app.store.setProducts(products);
53 changes: 53 additions & 0 deletions app/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { makeObservable } from '../dist/picosm.js';

class Product {
title = '';
price = 0;
quantity = 0;

constructor(title, price) {
this.title = title;
this.price = price;
}

get total() {
return this.price * this.quantity;
}

setQuantity(quantity) {
this.quantity = quantity;
}
}

class Store {
setProducts(products) {
this.products = products;
}

addToCart(product) {
this.lastProduct = product;
product.setQuantity(Math.max(product.quantity + 1, 1));
}

updateQuantity(product, value) {
this.lastProduct = product;
product.setQuantity(Math.max(value || 0, 0));
}

get total() {
return this.products
.filter((p) => p.quantity > 0)
.reduce((total, product) => {
return total + product.total;
}, 0);
}
}

const ProductObservable = makeObservable(Product, ['setQuantity'], ['total']);
const StoreObservable = makeObservable(
Store,
['setProducts', 'addToCart', 'updateQuantity'],
['total'],
);

export { Product, ProductObservable, Store, StoreObservable };
14 changes: 14 additions & 0 deletions app/swc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import '@spectrum-web-components/badge/sp-badge.js';
import '@spectrum-web-components/button/sp-button.js';
import '@spectrum-web-components/card/sp-card.js';
import '@spectrum-web-components/number-field/sp-number-field.js';
import '@spectrum-web-components/table/sp-table-body.js';
import '@spectrum-web-components/table/sp-table-cell.js';
import '@spectrum-web-components/table/sp-table-checkbox-cell.js';
import '@spectrum-web-components/table/sp-table-head-cell.js';
import '@spectrum-web-components/table/sp-table-head.js';
import '@spectrum-web-components/table/sp-table-row.js';
import '@spectrum-web-components/table/sp-table.js';
import '@spectrum-web-components/theme/scale-large.js';
import '@spectrum-web-components/theme/sp-theme.js';
import '@spectrum-web-components/theme/theme-dark.js';
Loading

0 comments on commit 46b9434

Please sign in to comment.