Air

Air::Component

This raku module is one of the core libraries of the raku Air distribution.

It is a scaffold to build dynamic, reusable web components.

SYNOPSIS

The synopsis is split so that each part can be annotated. First, we import the Air core libraries.

use Air::Functional :BASE;      # import all HTML tags as raku subs
use Air::Base;					# import Base components (site, page, nav...)
use Air::Component;

HTMX functions

We prepare some custom HTMX actions for our Todo component. This declutters class Todo and keeps our hx-attrs tidy and local.

role HxTodo {
    method hx-create(--> Hash()) {
        :hx-post("todo"),
        :hx-target<table>,
        :hx-swap<beforeend>,
    }
    method hx-delete(--> Hash()) {
        :hx-delete($.url-id),
        :hx-confirm<Are you sure?>,
        :hx-target<closest tr>,
        :hx-swap<delete>,
    }
    method hx-toggle(--> Hash()) {
        :hx-get("$.url-id/toggle"),
        :hx-target<closest tr>,
        :hx-swap<outerHTML>,
    }
}

Key features are:

class Todo

The core of our synopsis. It does Component to bring in the scaffolding.

The general idea is that a raku class implements a web Component, multiple instances of the Component are represented by objects of the class and the methods of the class represent actions that can be performed on the Component in the browser.

class Todo does Component {
    also does HxTodo;

    has Bool $.checked is rw = False;
    has Str  $.text;

    method toggle is controller {
        $!checked = !$!checked;
        respond self;
    }

    multi method HTML {
        tr
            td( input :type<checkbox>, |$.hx-toggle, :$!checked ),
            td( $!checked ?? del $!text !! $!text),
            td( button :type<submit>, |$.hx-delete, :style<width:50px>, '-'),
    }
}

Key features of class Todo are:

The result is a concise, legible and easy-to-maintain component implementation.

sub SITE

Now, we can make a website as follows:

my &index = &page.assuming(
    title       => 'hÅrc',
    description => 'HTMX, Air, Red, Cro',
    footer      => footer p ['Aloft on ', b 'Åir'],
);

my @todos = do for <one two> -> $text { Todo.new: :$text };

sub SITE is export {
    site :components(@todos),
        index
            main [
                h3 'Todos';
                table @todos;
                form  |Todo.hx-create, [
                    input  :name<text>;
                    button :type<submit>, '+';
                ];
            ]
}

Key features of sub SITE are:

Run Cro service.raku

Component automagically creates some cro routes for Todo when we start our website…

> raku -Ilib service.raku
theme-color=green
bold-color=red
adding GET todo/<#>
adding POST todo
adding DELETE todo/<#>
adding PUT todo/<#>
adding GET todo/<#>/toggle
adding GET page/<#>
Build time 0.67 sec
Listening at http://0.0.0.0:3000

DESCRIPTION

The rationale for Air Components is rooted in the powerful raku code composition capabilities. It builds on the notion of Locality of Behaviour (aka LOB) and the intent is that a Component can represent and render every aspect of a piece of website behaviour.

As Air evolves, it is expected that common code idioms will emerge to make each dimensions independent (ie HTML, CSS and JS relating to Air::Theme::Font would be local, and distinct from HTML, CSS and JS for Air::Theme::Nav).

Air is an integral part of the hArc stack (HTMX, Air, Red, Cro). The Synopsis shows how a Component can externalize and consume HTMX attributes for method actions, perhaps even a set of Air::HTMX libraries can be anticipated. One implication of this is that each Component can use the hx-swap-oob attribute to deliver Content, Style and Script anywhere in the DOM (except the html tag). An instance of this could be a blog website where a common Red model Post could be harnessed to populate each blog post, a total page count calculation for paging and a post summary list in an aside.

In the Synopsis, both raku class inheritance and role composition provide coding dimensions to greatly improve code clarity and evolution. While simple samples are shown, raku has comprehensive encapsulation and type capabilities in a friendly and approachable language.

Raku is a multi-paradigm language for both Functional and Object Oriented (OO) coding styles. OO is a widely understood approach to code and state encapsulation - suitable for code evolution across many aspects - and is well suited for Component implementations. Functional is a surprisingly good paradigm for embedding HTML standard and custom tags into general raku source code. The example below illustrates the power of Functional tags inline when used in more intricate stanzas.

While this kind of logic can in theory be delivered in a web app using web template files, as the author of the Cro Template language comments Those wishing for more are encouraged to consider writing their logic outside of the template.

    method nav-items {
        do for @.items.map: *.kv -> ($name, $target) {
            given $target {
                when * ~~ External | Internal {
                  $target.label = $name;
                  li $target.HTML
                }
                when * ~~ Content {
                    li a(:hx-get("$.name/$.serial/" ~ $name), safe $name)
                }
                when * ~~ Page {
                    li a(:href("/{.name}/{.serial}"), safe $name)
                }
            }
        }
    }

    multi method HTML {
        self.style.HTML ~ (

        nav [
            { ul li :class<logo>, :href</>, $.logo } with $.logo;

            button( :class<hamburger>, :serial<hamburger>, safe '&#9776;' );

            ul( :$!hx-target, :class<nav-links>,
                self.nav-items,
                do for @.wserialgets { li .HTML },
            );

            ul( :$!hx-target, :class<menu>, :serial<menu>,
                self.nav-items,
            );
        ]

        ) ~ self.script.HTML
    }

From the implementation of the Air::Base::Nav component.

TIPS & TRICKS

When writing components:

role Component

has UInt $!serial

assigns and tracks instance serials

has Str $!base

optional attr to specify url base

method holder

method holder() returns Hash

populates an instance holder [class method], may be overridden for external instance holder

method all

method all() returns Mu

get all instances in holder

method name

method name() returns Mu

get url safe name of class doing Component role

method url

method url() returns Str

get url (ie base/name)

method url-id

method url-id() returns Str

get url-id (ie base/name/serial)

method id

method id() returns Str

get html friendly id (ie name-serial), eg for html id attr

method MYLOAD

method MYLOAD(
    $serial
) returns Mu

Default load action (called on GET) - may be overridden

method MYCREATE

method MYCREATE(
    *%data
) returns Mu

Default create action (called on POST) - may be overridden

method MYDELETE

method MYDELETE() returns Mu

Default delete action (called on DELETE) - may be overridden

method MYUPDATE

method MYUPDATE(
    *%data
) returns Mu

Default update action (called on PUT) - may be overridden

method add-routes

method add-routes(
    $component is copy,
    :$comp-name = Code.new
) returns Mu

Meta Method ^add-routes typically called from Air::Base::Site in a Cro route block

multi sub respond

multi sub respond(
    $comp
) returns Mu

calls Cro: content ‘text/html’, $comp.HTML

multi sub respond

multi sub respond(
    Str $html
) returns Mu

calls Cro: content ‘text/html’, $html

AUTHOR

Steve Roe librasteve@furnival.net

The Air::Component module provided is based on an early version of the raku Cromponent module, author Fernando Corrêa de Oliveira fco@cpan.com, however unlike Cromponent this module does not use Cro Templates.

COPYRIGHT AND LICENSE

Copyright(c) 2025 Henley Cloud Consulting Ltd.

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.