htmdart

Htmdart combines the power of shelf, htmx, sometimes hyperscript and htmleez to give you the simplest and fastest way to build web applications in pure Dart (with a great DX too).

Before Starting

  • Make sure you're familiar with shelf as Htmdart is built on top of it

    shelf_router is not necessary, htmdart has its own router

  • Make sure you're familiar with hypermedia and htmx
  • Check out htmleez in order to understand how you can compose and render your HTML directly in dart
  • And make sure to read this essay in order to have a better idea of when to use htmx

P.S. Check out all the very informative memes about htmx and hypermedia in here

In case you'd like to add interactivity to your app you can check out:

Install

Add to your pubspec.yaml:

dart pub add htmdart

Hello World Example

import 'dart:math';
import 'package:htmdart/htmdart.dart'; /// htmleez is automatically imported with htmdart
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;

final router = Router()
  ..get("/", homePage)
  ..get("/random", randomNumber);

Future<void> main() async {
  final server = await io.serve(router.call, 'localhost', 8080);
  print('Listening on http://${server.address.host}:${server.port}');
}

Response homePage(Request _) {
  return html([
    body([
      div([$id("number"), "0".t]),
      button([
        $hx.get("/random"),
        "Get Random Number".t,
      ]),
    ])
  ]).response;
}

Response randomNumber(Request _) {
  final n = Random().nextInt(100);
  return div([
    $id("number"),
    $hx.swapOob.yes,
    n.toString().t,
  ]).response;
}

Core Concepts

1. HTML Responses

  • HtmlResponse
    Wraps a list of HTML components into a shelf.Response with Content-Type: text/html; charset=utf-8.
    Response myPage(Request req) {
      return HtmlResponse([
        html([
          h1(["Hello, world!".t]),
        ]),
      ]);
    }
    
    //You can also use the extensions
    Response myPage(Request req) {
      return html([
        h1(["Hello, world!".t])
      ]).response;
    }
    
  • Extensions
    • On a single HTML: myElement.response
    • On a List<HTML>: myListOfElements.response

2. HTMX utilities

The hx class provides all the standard HTMX attributes

  • Simple verbs
    hx.get("/path")
    hx.post("/submit")
    hx.put(...)
    hx.delete(...)
    
  • Dynamic handler binding
    // Let's imagine this handler
    Response handleNoteDetails(Request req, String noteId) {...}
    
    final router = Router()..get("/notes/<noteId>",  handleNoteDetails);
    
    // Then in your elements you can automatically pick up the HTTP verb and route
    $hx.handle(handleNoteDetails)
    
    // In case your route has path parameters (like in this case)
    // you can pass them in order as a List<String>
    $hx.handler(handleNoteDetails, [note.id])
    
    // You can also pass query parameters
    $hx.handler(handleNoteDetails, [note.id], {"fromQP": true})
    
    // It renders to
    // hx-get="/notes/myNoteId?fromQP=true";
    
  • Swap controls
    $hx.swap.innerHTML
    $hx.swapOob.yes      // out-of-band swaps
    
  • Extras
    $hx.vals("js:{ count: … }")      // inject JSON values
    $hx.select(".result")            // response selector
    $hx.confirm("Are you sure?")
    
    All the available hx attributes from htmx have been added, you can see them here

3. Hyperscript attribute

Built-in support for hyperscript's attribute

div([
  className("btn"),
  $_("on click add .active to me then wait 500ms then remove .active"),
  "Click me".t,
])

Renders ->

<div class="btn" _="on click add .active to me then wait 500ms then remove .active">Click me</div>

4. Request Extensions

On any shelf.Request you can check HTMX metadata:

if (request.isHx)         // true if HX-Request header == "true"
if (request.hxTarget != null)
print(request.hxTriggerName);

Routing

Use the built-in Router (from htmdart/src/router/router.dart) to declare routes:

final router = Router()
  ..notFoundHandler((req) => html([h1(["404".t])]).response)
  ..get("/", homePageHandler)
  ..post("/increment", incrementHandler);

void main() async {
  final server = await io.serve(router.call, 'localhost', 8080);
  print("Listening on ${server.port}");
}
  • Path parameters
    Response handler(Request req, String param) {...}
    
    final router = Router()..get("/handler/<param>", handler);
    
  • Any Method
    Response handler(Request req, String param) {
      switch(req.method) {
        case "GET": ...
        case "POST": ...
      }
    }
    
    final router = Router()..any("/any", handler);
    
  • Grouping
    final apiGroup = router.group("/api");
    apiGroup.get("/items", listItems);
    apiGroup.post("/items", createItem);
    
  • Static files
    router.static("/public", "web/public");
    
  • Middleware
    • Built-in support for middlewares with router.use(myMiddleware)
    • Built-in pretty-logging: prettyLogMiddleware()
  • Redirects Redirect not defined routes with
    router.redirect("/", "/login"),
    

Libraries

htmdart
Htmdart