DEV Community

artydev
artydev

Posted on

Mitosis pattern - State Management in Mithril

I have already posts on this subject.
Here is a reminder

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mithril.js E-commerce - State/Action Pattern</title>
    <!-- Mithril Library -->
    <script src="https://unpkg.com/mithril/mithril.js"></script>
    <style>
        body { font-family: system-ui, sans-serif; margin: 0; background: #f4f4f4; }
        header { 
            background: #2c3e50; color: white; padding: 1rem 2rem; 
            display: flex; justify-content: space-between; align-items: center;
            position: sticky; top: 0; z-index: 10;
        }
        .container { max-width: 1100px; margin: 2rem auto; padding: 0 1rem; }
        .products-grid { 
            display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 
            gap: 1.5rem; 
        }
        .card { 
            background: white; border-radius: 8px; overflow: hidden; 
            box-shadow: 0 2px 8px rgba(0,0,0,0.1); display: flex; flex-direction: column;
        }
        .card img { width: 100%; height: 200px; object-fit: cover; }
        .card-body { padding: 1rem; flex-grow: 1; }
        .card-footer { padding: 1rem; border-top: 1px solid #eee; }
        button { 
            background: #3498db; color: white; border: none; padding: 0.6rem 1rem; 
            border-radius: 4px; cursor: pointer; width: 100%; font-weight: bold;
        }
        button:hover { background: #2980b9; }
        button.secondary { background: #e74c3c; margin-top: 5px; }

        /* Cart Sidebar */
        .cart-overlay {
            position: fixed; top: 0; right: 0; width: 350px; height: 100%;
            background: white; box-shadow: -5px 0 15px rgba(0,0,0,0.1);
            transform: translateX(100%); transition: 0.3s ease-in-out;
            z-index: 20; padding: 1.5rem; display: flex; flex-direction: column;
        }
        .cart-overlay.open { transform: translateX(0); }
        .cart-item { display: flex; justify-content: space-between; margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem; }
        .close-cart { background: #95a5a6; margin-bottom: 1rem; }
    </style>
</head>
<body>

<script>
/**
 * 1. STATE FACTORIES
 */
const ProductState = () => ({
    list: [],
    loading: false
});

const CartState = () => ({
    items: [],
    isOpen: false
});

/**
 * 2. ACTION FACTORIES (The Logic)
 */
const ProductActions = (state) => ({
    load: async () => {
        state.loading = true;
        try {
            const res = await m.request("https://dummyjson.com/products?limit=12");
            state.list = res.products;
        } finally {
            state.loading = false;
        }
    }
});

const CartActions = (state) => ({
    toggle: () => state.isOpen = !state.isOpen,
    add: (product) => {
        const existing = state.items.find(item => item.id === product.id);
        if (existing) {
            existing.qty++;
        } else {
            state.items.push({ ...product, qty: 1 });
        }
    },
    remove: (id) => {
        state.items = state.items.filter(item => item.id !== id);
    }
});

/**
 * 3. COMPONENTS (The View)
 */

// Helper to calculate total price
const calcTotal = (items) => items.reduce((sum, item) => sum + (item.price * item.qty), 0);

const ProductCard = (product, actions) => 
    m(".card", [
        m("img", { src: product.thumbnail }),
        m(".card-body", [
            m("h3", product.title),
            m("p", { style: "color: #7f8c8d; font-size: 0.9rem" }, product.description.substring(0, 60) + "..."),
            m("strong", `$${product.price}`)
        ]),
        m(".card-footer", [
            m("button", { onclick: () => actions.cart.add(product) }, "Add to Cart")
        ])
    ]);

const CartDrawer = (state, actions) =>
    m(".cart-overlay", { class: state.cart.isOpen ? "open" : "" }, [
        m("button.close-cart", { onclick: actions.cart.toggle }, "Close Cart"),
        m("h2", "Your Shopping Cart"),
        state.cart.items.length === 0 ? m("p", "Cart is empty...") : 
        state.cart.items.map(item => 
            m(".cart-item", [
                m("div", [
                    m("div", { style: "font-weight: bold" }, item.title),
                    m("small", `$${item.price} x ${item.qty}`)
                ]),
                m("button.secondary", { 
                    style: "width: auto; padding: 2px 8px", 
                    onclick: () => actions.cart.remove(item.id) 
                }, "Remove")
            ])
        ),
        m("hr"),
        m("h3", `Total: $${calcTotal(state.cart.items)}`),
        m("button", { onclick: () => alert("Proceeding to Checkout!") }, "Checkout")
    ]);

/**
 * 4. APP INITIALIZATION (Mitosis / Factory Setup)
 */
const App = () => {
    // Instantiate our states
    const pState = ProductState();
    const cState = CartState();

    // Instantiate our actions, binding them to the states
    const pActions = ProductActions(pState);
    const cActions = CartActions(cState);

    // Initial API call
    pActions.load();

    return {
        view: () => m("div", [
            // Header
            m("header", [
                m("h1", "JS Store"),
                m("button", { 
                    style: "width: auto", 
                    onclick: cActions.toggle 
                }, `Cart (${cState.items.length})`)
            ]),

            // Main Content
            m(".container", [
                pState.loading 
                    ? m("h2", "Loading Products...") 
                    : m(".products-grid", pState.list.map(p => ProductCard(p, { cart: cActions })))
            ]),

            // Sidebar
            CartDrawer({ cart: cState }, { cart: cActions })
        ])
    };
};

// Mount the app to the body
m.mount(document.body, App);
</script>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

You can test it here MitosisPattern

Top comments (0)