重啟北護課程查詢系統 Day 3 - HTMX 小試身手 - counter

Posted on Sat, Feb 17, 2024 Node.JS Web JS

重啟北護課程查詢系統 Day 2 - Fastify-HTMX-EJS-Tailwind當中,筆者比較有興趣一點的是 HTMX,在 Day 3,我們就小小的玩一下,做一個 counter 吧!

安裝套件

在實作 counter 時,我們要把 count 給儲存起來,並渲染給 user,在這我們可以使用 session 達成這項任務

pnpm i @fastify/session @fastify/cookie

設定 session

.env

SERVER_HOST="127.0.0.1"
SERVER_PORT=3000
SESSION_SECRET="請自行產生超過32字元的 secret 數值"

env-class.js

// 抓取 env 的設定
// 使用 env-var 來驗證以及針對數值轉換 env 的設定

import envVar from "env-var";
import dotenv from "dotenv";

dotenv.config();

export const ServerEnv = function () {
    return {
        host: envVar.get("SERVER_HOST").required().default("0.0.0.0").asString(),
        port: envVar.get("SERVER_PORT").required().default(3000).asPortNumber(),
        sessionSecret: envVar.get("SESSION_SECRET").required().asString()
    };
};

server/plugins/session.js

import fp from "fastify-plugin";
import fastifySession from "@fastify/session";
import fastifyCookie from "@fastify/cookie";
import { ServerEnv } from "../env-class.js";


export default fp(async (fastify, opts) => {
    fastify.register(fastifyCookie);
    fastify.register(fastifySession, {
        cookieName: "ntunhs-session-id",
        secret: ServerEnv().sessionSecret,
        cookie: {
            path: "/",
            secure: false
        }
    });
}, {
    name: "f-session"
});

建立 counter 頁面

server/views/counter.ejs

<main class="container" id="main">
    <div class="flex flex-wrap flex-1">
        <h1 class="text-3xl block w-full">counter!</h1>
        <p class="block w-full mb-5" id="p-count">count: <%= count %></p>
        <button
            type="button"
            class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-full text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            hx-post="/counter/increment"
            hx-select="#p-count"
            hx-target="#p-count"
            hx-swap="outerHTML"
        >
            Increment
        </button>
        <button
            type="button"
            class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-full text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
            hx-post="/counter/increment-p"
            hx-target="#p-count"
        >
            Increment - only paragraph
        </button>
    </div>
</main>

routes

server/modules/counter/index.js

import autoLoad from "@fastify/autoload";
import { join } from "desm";

/**
 * 
 * @param {import("fastify").FastifyInstance} fastify 
 * @param {import("fastify").FastifyPluginOptions} opts 
 */
export default async function (fastify, opts) {
    fastify.register(autoLoad, {
        dir: join (import.meta.url, "routes"),
        options: {
            prefix: opts.prefix
        }
    });
}

server/modules/counter/routes/counter.js

/**
 * 
 * @param {import("fastify").FastifyInstance} fastify 
 * @param {import("fastify").FastifyPluginOptions} opts 
 */
export default async function (fastify, opts) {
    fastify.get("/", {
        preHandler: async (request, reply) => {
            if (!request.session.count && request.session.count !== 0) {
                request.session.count  = 0;
            }
        }
    }, async (request, reply) => {
        return reply.view("components/layout", {
            page: {
                file: "../counter.ejs",
                data: {
                    count: request.session.count
                }
            }
        });
    });
}

server/modules/counter/routes/increment.js

/**
 * 
 * @param {import("fastify").FastifyInstance} fastify 
 * @param {import("fastify").FastifyPluginOptions} opts 
 */
export default async function (fastify, opts) {
    fastify.post("/increment", {
        preHandler: async (request, reply) => {
            if (!request.session.count && request.session.count !== 0) {
                request.session.count = 0;
            }
        }
    }, async (request, reply) => {
        request.session.count++;
        return reply.view("components/layout", {
            page: {
                file: "../counter.ejs",
                data: {
                    count: request.session.count
                }
            }
        });
    });
}

server/modules/counter/routes/increment-p.js

/**
 * 
 * @param {import("fastify").FastifyInstance} fastify 
 * @param {import("fastify").FastifyPluginOptions} opts 
 */
export default async function (fastify, opts) {
    fastify.post("/increment-p", {
        preHandler: async (request, reply) => {
            if (!request.session.count && request.session.count !== 0) {
                request.session.count = 0;
            }
        }
    }, async (request, reply) => {
        request.session.count++;
        return reply.type("text/html").send(`count: ${request.session.count}`);
    });
}

counter 最終成果

結語

今天玩了一點點的 HTMX 功能,去替換網頁中的元素,過程中作者對 hx-trargethx-select 都有點小困惑,可以看看這部影片 HTMX - hx-select and hx-select-oob Attributes in HTMX 了解一下別人是怎麼使用 hx-target、hx-select 和 hx-select-oob

參考資料