Checking out htmx
Tags: 100daystooffload, htmx, webdev, tictactoe
Published: 2023-07-13 20:11:41

There has been some buzz at least in my twitter feed around [htmx](https://htmx.org/) and is one project in the [GitHub Accelerator cohort](https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/), so it's time to dip my toes into this library.

Here is a short introduction from their documentation page; 
> htmx is a library that allows you to access modern browser features directly from HTML, rather than using javascript.

I will try to convert the [tutorial: Tic-Tac-Toe](https://react.dev/learn/tutorial-tic-tac-toe) from [Reacts](https://react.dev/) documentation to use htmx instead.

Lets begin with initializing a project for this and open it up in VS Code.

```bash
mkdir tic-tac-toe && cd tic-tac-toe
git init
npm init -y
touch .gitignore
code .
```

Ignore the `node_modules`-folder by adding it to the _.gitignore_-file.

```config
/node_modules
```

I will use `Express.js` with `TypeScript` as a backend, therefore we will need to add some npm packages:

```bash
npm install typescript express --save-dev
npm install @types/node @types/express --save-dev
```

Create a `tsconfig.json` like this:

```bash
npx tsc --init
```

and use another output directory for the artifacts from the compilation.

```json
{
  "compilerOptions": {
    // ...
    // ... find the outDir property in the tsconfig
    // ... and change it to this
    "outDir": "./dist",
    // ...
    // ...
    // ...
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}
```

Setup the backend server code:

```bash
mkdir src && touch src/index.ts
```

add the setup code for Express including a root route (/) in the `index.ts`-file.

```typescript
import express, { Express, Request, Response } from 'express';

const app: Express = express();
const port = 3000;

app.get('/', (req: Request, res: Response) => {
  res.send('Tic-Tac-Toe backend');
});

app.listen(port, () => {
  console.log(`🦄[backend]: Server is running at http://localhost:${port}`);
});
```

We need to add a script to the `package.json`-file  to make it easy to start the backend.

```json
  "scripts": {
    "dev": "npx tsc && node ./dist/index.js"
  },
``` 

and add the `dist`-folder to _.gitignore_.

```
/node_modules
/dist
```

Finally we can make a check point to run the backend and visit it at http://localhost:3000/ to see a Tic-Tac-Toe message.

```bash
npm run dev
```

Looks good so far, lets change the root route to deliver a html page instead.

```typescript
import express, { Express, Request, Response } from 'express';
import path from 'path';

const app: Express = express();
const port = 3000;

app.get('/', (req: Request, res: Response) => {
  // The change is here plus the import of path
  res.sendFile(path.join(__dirname, '../src/public/index.html'));
});

app.listen(port, () => {
  console.log(`🦄[backend]: Server is running at http://localhost:${port}`);
});
```

and now the html file, `index.html`.

```bash
mkdir src/public && touch src/public/index.html
```

and add the following code.

```html
<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title>Tic-Tac-Toe</title>
</head>
<body>
    <h1>Tic-Tac-Toe</h1>
</body>
```

I think we got everything at place to start to tinkering with htmx now.

Start with adding htmx via a CDN (since it is not a production project, [reasons to avoid Javascript CDNs](https://blog.wesleyac.com/posts/why-not-javascript-cdn)) in `index.html`-file header.

```html
<script src="https://unpkg.com/htmx.org@1.9.2" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous"></script>
```

Copy the css from the [Tic-Tac-Toe tutorial](https://codesandbox.io/s/x219pb?file=/styles.css) and add it to a `styles.css`-file.

```bash
touch src/public/styles.css
```

and the css code.

```css
* {
    box-sizing: border-box;
  }
  
  body {
    font-family: sans-serif;
    margin: 20px;
    padding: 0;
  }
  
  h1 {
    margin-top: 0;
    font-size: 22px;
  }
  
  h2 {
    margin-top: 0;
    font-size: 20px;
  }
  
  h3 {
    margin-top: 0;
    font-size: 18px;
  }
  
  h4 {
    margin-top: 0;
    font-size: 16px;
  }
  
  h5 {
    margin-top: 0;
    font-size: 14px;
  }
  
  h6 {
    margin-top: 0;
    font-size: 12px;
  }
  
  code {
    font-size: 1.2em;
  }
  
  ul {
    padding-left: 20px;
  }
  
  * {
    box-sizing: border-box;
  }
  
  body {
    font-family: sans-serif;
    margin: 20px;
    padding: 0;
  }
  
  .square {
    background: #fff;
    border: 1px solid #999;
    float: left;
    font-size: 24px;
    font-weight: bold;
    line-height: 34px;
    height: 34px;
    margin-right: -1px;
    margin-top: -1px;
    padding: 0;
    text-align: center;
    width: 34px;
  }
  
  .board-row:after {
    clear: both;
    content: '';
    display: table;
  }
  
  .status {
    margin-bottom: 10px;
  }
  .game {
    display: flex;
    flex-direction: row;
  }
  
  .game-info {
    margin-left: 20px;
  }
```

Add the a link to css file in `index.html`-file.

```html
<!DOCTYPE html>
<html lang="en-US">
<head>
    <script src="https://unpkg.com/htmx.org@1.9.2" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous"></script>
    <meta charset="UTF-8">
    <link href="styles.css" rel="stylesheet" />
    <title>Tic-Tac-Toe</title>
</head>
<body>
    <h1>Tic-Tac-Toe</h1>
</body>
```

We need to do some changes to the backend so it can serve the css file by adding the `public`-folder as a static resource in Express.

```typescript
// right after the line: const port = 3000;
app.use(express.static(path.join(__dirname, '../src/public')));
```

Now it's time to update the body of the `index.html`-file to include the game board and some htmx magic.

```html
<!DOCTYPE html>
<html lang="en-US">
<head>
    <script src="https://unpkg.com/htmx.org@1.9.2" integrity="sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
    <meta charset="UTF-8">
    <link href="styles.css" rel="stylesheet" />
    <title>Tic-Tac-Toe</title>
</head>
<body>
    <h1>Tic-Tac-Toe</h1>
    <div class="status" hx-ext="sse" sse-connect="/status" sse-swap="message"></div>
    <div class="board-row">
      <button class="square" hx-get="/move?pos=0" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=1" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=2" hx-swap="innerHTML"></button>
    </div>
    <div class="board-row">
      <button class="square" hx-get="/move?pos=3" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=4" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=5" hx-swap="innerHTML"></button>
    </div>
    <div class="board-row">
      <button class="square" hx-get="/move?pos=6" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=7" hx-swap="innerHTML"></button>
      <button class="square" hx-get="/move?pos=8" hx-swap="innerHTML"></button>
    </div>
</body>
```

The html layout is the same as the React version, however here we have added some hx-\<attribute\>, example

```html
<button class="square" hx-get="/move?pos=0" hx-swap="innerHTML"></button>
```

* hx-get - make a http get request to _/move?pos=0_
* hx-swap - will replace the inner html of the button with  the response from the get request

__Note__ that with htmx the response from the server side usually is HTML and not JSON. The reason behind this is explained in the documentation;

> This keeps you firmly within the [original web programming model](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm), using [Hypertext As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS) without even needing to really understand that concept.

If you look at the status div element there is another feature from htmx, server side events. This is enabled by an extension and is an extra library that is imported with a script tag

```html
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
```

and this is how it is used in this example.

```html
<div class="status" hx-ext="sse" sse-connect="/status" sse-swap="message"></div>
```

* hx-ext - use the _sse_ extension here
* sse-connect - the URL to the SSE endpoint on the backend
* sse-swap - use the data from the _type_ message to replace the content of the div element

_message_ is the default type for an unnamed event.

There is of course some backend code needed for keeping state, user interaction and handle the server side events. This is the spaghetti code for that 😆

```typescript
import express, { Express, Request, Response } from 'express';
import path from 'path';

const app: Express = express();
const port = 3000;

type Client = {
  id: number;
  res: Response;
}

let clients: Client[] = [];

type Player = 'X' | 'O' | '';

const initializeGame = () => {
  app.locals.board = [
    '', '', '',
    '', '', '',
    '', '', ''
  ] as Player[];
  app.locals.nextPlayer = 'X' as Player;
  app.locals.status = 'Next player: X';
}

const switchPlayer = () => {
  app.locals.nextPlayer = app.locals.nextPlayer === 'X' ? 'O' : 'X';
}

const isWinner = (currentBoard: Player[]): boolean => {
  const winnerLines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < winnerLines.length; i++) {
    const [a, b, c] = winnerLines[i];
    if (currentBoard[a] && currentBoard[a] === currentBoard[b] && currentBoard[a] === currentBoard[c]) {
      return true;
    }
  }
  return false;
}

const sendStatusEvent = (status: string) => {
  clients.forEach(client => client.res.write(`data: ${JSON.stringify(status)}\n\n`))
}

initializeGame();

app.use(express.static(path.join(__dirname, '../src/public')));

app.get('/', (req: Request, res: Response) => {
  res.sendFile(path.join(__dirname, '../src/public/index.html'));
});

app.get('/move', (req: Request, res: Response) => {
  const boardIndex = parseInt(req.query.pos as string);
  const moveByPlayer = app.locals.nextPlayer;

  if (app.locals.board[boardIndex] !== '') {
    res.send(app.locals.board[boardIndex])
    return;
  }

  if (isWinner(app.locals.board)) {
    return;
  }

  app.locals.board[boardIndex] = moveByPlayer;
  switchPlayer();

  if (isWinner(app.locals.board)) {
    app.locals.status = `Winner: ${moveByPlayer}`;
  } else {
    app.locals.status = `Next player: ${app.locals.nextPlayer}`;
  }  
  sendStatusEvent(app.locals.status);
  res.send(moveByPlayer);
});

app.get('/status', (req: Request, res: Response) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  clients.push({
    id: Date.now(),
    res,
  });

  sendStatusEvent(app.locals.status);
});

app.listen(port, () => {
  console.log(`🦄[backend]: Server is running at http://localhost:${port}`);
});
```

Most of the things is happening in the endpoints _move_ and _status_. As you can see we are just sending back __X__ or __O__ in the response in the move endpoint.

```typescript
res.send(moveByPlayer);
```

Which htmx will use to replace the text for the clicked button.

The sendStatusEvent function is where we send an update of status to the client.

```typescript
const sendStatusEvent = (status: string) => {
  clients.forEach(client => client.res.write(`data: ${JSON.stringify(status)}\n\n`))
}
```

Summary
-------

It was fun to play with htmx, but maybe this tac-tac-toe game was not the best first application to try htmx with. However it was really easy to get started with htmx and most time was spended on the backend. The final project can be found on [GitHub](https://github.com/stefanalfbo/tic-tac-toe-htmx)