What are your thoughts on my lib/framework syntax and structure?

Okay, so I have been wanting to get a deeper grasp and understanding of how frontend frameworks, web servers and caches ect… Work underneath the hood? Being inspired by GitHub - nicbarker/clay: High performance UI layout library in C.. C layout library, I have decided to attempt to build my own frontend framework from scratch in zig, which compiles to wasm. Below is an example the framework in use, the aim to be developer friendly and somewhat related to other frontend frameworks. Curious what all of your takes are on this?

Main goals
only zig, no tsx or transpiling ect…
should be somewhate zig idiomatic
highly flexible, ie no file naming syntax like +page.server.ts, or +layout.svelte, ect…
easy to read and maintain, should be explicit.
composable and modular

const std = @import("std");
// Import the Fabric UI framework
const Fabric = @import("fabric");
const Theme = @import("../../main.zig").theme;

// Select component 
const Select = @import("../../lib/Select.zig").Select;

// Signals are used to hold reactive state that automatically triggers UI updates
const Signal = Fabric.Signal;

// Fabric provides multiple component types depending on how dynamic the content is:
// Static: never updates after initial render
const Static = Fabric.Static;
// Pure: updates when its props change
const Pure = Fabric.Pure;
// Dynamic: binds directly to signals, auto-updates when signal changes
const Dynamic = Fabric.Dynamic;
// Binded: directly binds to low-level HTML/DOM elements, for two-way binding
const Binded = Fabric.Binded;


// Declare colors
var primary: [4]f32 = undefined;
var secondary: [4]f32 = undefined;
var btn_color: [4]f32 = undefined;
var border_color: [4]f32 = undefined;
var alternate_tint: [4]f32 = undefined;
var text_color: [4]f32 = undefined;

// --- DATA STRUCTURES ---

// Each project contains title, author, and image fields
const Project = struct {
    title: []const u8,
    author: []const u8,
    img: []const u8,
};

// Reactive list of projects to be displayed
var projects: Signal([]Project) = undefined;

// UI State flags (dialog and filter visibility toggles)
var show_dialog: Signal(bool) = undefined;
var show_filter_menu: Signal(bool) = undefined;

// Filter dropdown list data structure
const Item = struct {
    label: []const u8,
    value: void = {}, // Placeholder for value if needed later
};

// List of filter options
var filter_list: std.ArrayList(Item) = undefined;

// Select component instance
var select: Select(Item, &filter_list) = undefined;

// Input element reference for search bar (binded allows direct interaction with DOM)
var search_bar: Fabric.Element = Fabric.Element{};

// --- INITIALIZATION ---

pub fn init() void {
    // Initialize colors from the Theme
    primary = Theme.instance.primary;
    secondary = Theme.instance.secondary;
    btn_color = Theme.instance.btn_color;
    border_color = Theme.instance.border_color;
    text_color = Theme.instance.text_color;
    alternate_tint = Theme.instance.alternate_tint;

    // Initialize projects signal with empty list
    projects.initv2(&.{});

    // Initialize boolean toggle flags
    show_dialog.initv2(false);
    show_filter_menu.initv2(false);

    // Fetch project data from server API and update state when response arrives
    // takes a url, a argument for the callback, a callback function for the response, and details on the request
    Fabric.Kit.fetchWithParams("http://localhost:8443/projects", {}, setProjectsList, .{
        .method = "GET",
        .credentials = "include", // Attach cookies/session
        .headers = .{ .content_type = "text/html" }, // Adjust if your backend returns JSON
    });

    // Initialize filter options and dropdown menu
    filter_list = std.ArrayList(Item).init(Fabric.lib.allocator_global);
    filter_list.appendSlice(&.{ Item{ .label = "Activity" }, Item{ .label = "Name" } }) catch return;
    select.init(&Fabric.lib.allocator_global, "Sort", null);

    // Mount page using Fabric's page mounting system
    // Still thinking about how I want to handle creating pages?
    // current the page function takes a source and uses this as the routes ie
    // /routes/app/users -> /app/users
    // /routes/auth/login -> /auth/login
    // render function is passed and stored for rerendering and reconciliation
    // can pass null or a deinit function when a page is destroyed
    Fabric.Page(@src(), render, null, .{
        .width = .percent(1),
        .height = .percent(1),
    });
}

// --- API RESPONSE HANDLER ---

// Callback that will receive the HTTP response
fn setProjectsList(_: void, resp: Fabric.Kit.Response) void {
    Fabric.println("{s}", .{resp.body});
}

// --- REUSABLE COMPONENTS ---

// Define a reusable Card component
inline fn Card() Fabric.Component {
    return Static.FlexBox(.{
        .border_radius = .all(8),
        .border_color = border_color,
        .padding = .all(8),
        .height = .fixed(200),
        .width = .grow,
        .direction = .column,
        .border_thickness = .all(1),
        .child_gap = 12,
    });
}

// --- EVENT HANDLERS ---

// Simple functions to toggle UI state (used as button handlers)
fn openDialog() void {
    show_dialog.toggle();
}

fn openFilter() void {
    show_filter_menu.toggle();
}

// --- MAIN RENDER FUNCTION ---

pub fn render() void {
    // Top-level vertical FlexBox container
    Static.FlexBox(.{
        .width = .percent(1),
        .height = .percent(1),
        .direction = .column,
        .child_alignment = .{ .x = .start, .y = .center },
    })({

        Static.FlexBox(.{
            .width = .percent(0.5),
            .child_alignment = .{ .x = .between, .y = .center },
            .padding = .horizontal(12),
        })({

            // Search bar input, binded to DOM for two-way data binding
            Binded.Input(&search_bar, .{ .string = .{ .default = "Search...", .required = true } }, .{
                .background = primary,
                .text_color = text_color,
                .outline = .none,
                .font_size = 16,
                .border_color = border_color,
                .border_radius = .all(8),
                .border_thickness = .all(1),
                .padding = .{ .left = 12, .right = 12, .bottom = 10, .top = 10 },
                .width = .percent(0.7),
            });

            Static.FlexBox(.{
                .width = .percent(0.3),
                .child_gap = 4,
            })({

                // Render dropdown select component
                Static.FlexBox(.{
                    .width = .percent(0.6),
                })({
                    select.render();
                });

                // Add Button that opens the dialog (plus icon)
                Static.Button(openDialog, .{
                    .border_color = border_color,
                    .border_thickness = .all(1),
                    .border_radius = .all(6),
                    .width = .fixed(41),
                    .height = .fixed(41),
                })({
                    Static.Icon("bi bi-plus-lg", .{
                        .text_color = text_color,
                        .font_size = 16,
                    });
                });
            });
        });

        // Render each project in projects list
        for (projects.get()) |project| {
            Card()({
                Static.FlexBox(.{
                    .direction = .column,
                })({
                    Static.Text(project.title, .{});
                    Static.Text(project.author, .{});
                    Static.Text(project.img, .{});
                });
            });
        }
    });
}
4 Likes

This is really interesting! It reminds me of other non-js frontend frameworks like leptos with its use of signals for reactivity.
I think making use of reasonable defaults for elements through patterns like decl literals could make the syntax more concise. Also, making initializers that allow you to omit most reasonable defaults and only specify what you want to be set would be good.
I am also curious about how dom manipulation works in this as well as creating handlers for elements being added or removed from the dom.

2 Likes

Additionally, allowing elements to take a theme as input or as a special initializer would be nice, especially if also permitting an optional override parameter

2 Likes

This is a cool idea, I’m glad you’re working on it. A similar concept has been in the back of my mind for a while now, so I’ve thought a bit about how something like this could work in zig.

The example you give won’t be doable as-is. You emulate a similar pattern to the c library in having imperative code within braces as an argument to each component, which is only possible because of the CLAY macro which hides implicit open and close functions that wrap the logic, letting the engine know where in the element tree the logic is being executed. I haven’t looked into the CLAY code too deep, but I assume it keeps a global element tree and calling the open function creates a new node in the tree and traverses to it, while calling the close function traverses to the current node’s parent. You could try to replicate CLAY’s strategy without macros, but that would lead to quite boilerplate-heavy code with less discernable structure than the c equivalent.

One thing I’ve thought about is simply trying to make everything function-based, where components are just functions that return a DOM node (which can contain handler functions) and a slice of state memory that can be freed when the node is dropped. However, this does put basically all of the burden of implementation onto the components themselves, enforcing no standard way to do child passing, styling, etc.

I think what I would do to get a feel for how it could be implemented is to first try to make a structural representation of HTML in zig that you can convert to an HTML string, which would look something like this:

const my_dom_node: html.Node = html.div(.{
    .style = "background-color:blue;",
}, &.{
    html.p(.{
        .style = "color:yellow;",
    }, &.{
        html.textFragment("abcd"),
    }),
    html.div(.{
        .style = "height:10rem;",
    }, &.{}),
    html.textFragment("1234"),
});

Which would generate:

<div style="background-color:blue;">
    <p style="color:yellow;">abcd</p>
    <div style="height:10rem;"></div>
    1234
</div>

Then, I would try to:

  • Integrate handlers that have the generated HTML call WASM code (zig functions).
  • Make structural representation of CSS so you don’t need to use strings for style.

Once you have this, I think it would be easier to experiment with different ways of constructing components, as they can all just generate this core representation.

1 Like