DEV Community

Cover image for Build an App with HellaJS
omilli
omilli

Posted on • Edited on

Build an App with HellaJS

Overview

HellaJS is a lightweight, reactive JavaScript library for building fast, tiny interfaces. Reactivity comes from a fork of Alien Signals, and templating is inspired by the granular reactivity found in SolidJS.

It offers a familiar JSX experience with extra flexibility and impressive benchmarks. You can also work without any compile step using HTML element proxies.

Let's create a text-based image app using all the HellaJS packages. We'll use the Vite plugin for JSX and the dummyjson.com to generate images.

⚠️ Heads up:
The API is still evolving and not production ready.

Setup

Create a Vite project and choose the Vanilla template:

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

cd into your project and install the HellaJS packages:

npm install @hellajs/core @hellajs/css @hellajs/dom @hellajs/resource @hellajs/router @hellajs/store vite-plugin-hellajs
Enter fullscreen mode Exit fullscreen mode

Create/Update vite.config.js:

import { defineConfig } from 'vite'
import viteHellaJS from 'vite-plugin-hellajs'

export default defineConfig({
  plugins: [viteHellaJS()]
})
Enter fullscreen mode Exit fullscreen mode

For TypeScript, add to tsconfig.json:

{
  "compilerOptions": {
    "jsx": "preserve",
    "types": ["@hellajs/dom"]
  }
}
Enter fullscreen mode Exit fullscreen mode

File structure:

src/
  components/
    Header.jsx
  pages/
    Collection.jsx
    Create.jsx
  App.jsx
  store.js
Enter fullscreen mode Exit fullscreen mode

Point the index.html file to App.jsx.

<!-- Before -->
<script type="module" src="/src/main.js"></script>`

<!-- After -->
<script type="module" src="/src/App.jsx"></script>
Enter fullscreen mode Exit fullscreen mode

App & Router

In App.jsx, set up our router and global css styles, Then mount it to the DOM.

You don't have to style like this, it's just to demo the css package.

import { router } from "@hellajs/router";
import { mount } from "@hellajs/dom";
import { css } from "@hellajs/css";

import { Header } from "./components/Header";
import { Collection } from "./pages/Collection";
import { Create } from "./pages/Create";

// A simple function that takes a JSX element and mounts it
const routerMount = (Page: JSX.Element) => mount(Page, "#router")

// Set the page signal when the route changes
router({
  routes: {
    "/": () => routerMount(<Collection />),
    "/create": () => routerMount(<Create />),
  }
});

// Set some global styles
css({
  "*": {
    boxSizing: "border-box",
  },
  body: {
    margin: 0,
    fontFamily: "sans-serif",
  },
  a: {
    cursor: 'pointer',
    fontWeight: 'bold',
    color: "mediumslateblue"
  },
  ".container": {
    maxWidth: "800px",
    margin: "0 auto",
    padding: "1rem",
  }
}, { global: true });


const App = () => {
  return (
    <>
      <Header />
      <main class="container" id="router">
        Loading...
      </main>
    </>
  );
}

// Looks for "#app" automatically
mount(App, "#app");

Enter fullscreen mode Exit fullscreen mode

⚠️ Use {page} (not {page()}) to keep it reactive.

Header Component

In components/Header.jsx, create a simple navigation bar:

import { forEach } from "@hellajs/dom";
import { navigate } from "@hellajs/router";
import { css } from "@hellajs/css";

const links = [
  { path: '/', label: 'Collection' },
  { path: '/create', label: 'Create' }
];

const style = css({
  padding: "1rem",
  backgroundColor: "#f0f0f0",
  textAlign: "center",
  nav: {
    display: "flex",
    justifyContent: "center",
    gap: "1rem"
  },
  h1: {
    marginTop: 0
  }
});

export const Header = () => (
  <header class={style}>
    <h1>Text Images</h1>
    <nav>
      {forEach(links, link => (
        <a onclick={() => navigate(link.path)}>{link.label}</a>
      ))}
    </nav>
  </header>
);
Enter fullscreen mode Exit fullscreen mode

Note: Use The HellaJS forEach for reactive lists, not regular array methods.

Store

In store.js, set up a reactive store with a http resource for creating images:

import { computed, effect } from "@hellajs/core";
import { store } from "@hellajs/store";
import { resource } from "@hellajs/resource";
import { navigate } from "@hellajs/router";

const API_URL = "https://dummyjson.com/image/800x400";

const createImageStore = () => {
  const storeRef = store({
    backgrounds: ["red", "green", "blue", "yellow", "purple", "orange"],
    fonts: ["bitter", "cairo", "comfortaa", "cookie", "dosis", "gotham", "lobster"],
    color: "",
    font: "",
    text: "",
    loading: false,
    error: null,
    collection: []
  });

  const key = computed(() => ({
    color: storeRef.color(),
    font: storeRef.font(),
    text: storeRef.text()
  }));

  const storeResource = resource(({ color, font, text }) =>
    fetch(`${API_URL}/${color}?fontFamily=${font}&text=${text}`.replace(/\s/g, '+')),
    { key }
  );

  effect(() => {
    if (storeResource.error()) return alert("Error");

    const url = storeResource.data()?.url;
    if (!url) return;

    storeRef.collection([url, ...storeRef.collection()]);
    storeResource.abort();

    storeRef.color("");
    storeRef.font("");
    storeRef.text("");

    navigate("/");
  });

  return { store: storeRef, resource: storeResource };
};

export const imageStore = createImageStore();
Enter fullscreen mode Exit fullscreen mode

Pages

Collection

In pages/Collection.jsx, display the image collection:

import { forEach } from "@hellajs/dom";
import { imageStore } from "../store";

export const Collection = () => {
  const { store } = imageStore;
  return (
    <>
      {forEach(store.collection, url => <img src={url} alt="Generated" />)}
      {() => store.collection().length === 0 && <p style="text-align:center">No images generated yet.</p>}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Create

In pages/Create.jsx, build a form for creating images:

import { forEach } from "@hellajs/dom"
import { postStore } from "../store"
import { css } from "@hellajs/css"


const styles = css({
  "> div": {
    marginBottom: "1rem",
  },
  label: {
    display: "block",
  },
  "input, select": {
    width: "100%",
    padding: "0.5rem",
  },
  button: {
    padding: "0.5rem 1rem",
    backgroundColor: "mediumslateblue",
    color: "white",
    border: "none",
    cursor: "pointer",
    width: "100%",
    "&:disabled": {
      opacity: 0.5,
    }
  }
})

export const Create = () => {
  const { store, resource } = postStore;

  const submit = (e: SubmitEvent) => {
    e.preventDefault();
    const inValid = ![store.color, store.font, store.text].every((val) => val() !== '');
    if (inValid) {
      return alert("Please fill all fields");
    }
    resource.fetch();
  }

  return (
    <form class={styles} onsubmit={submit}>
      <div>
        <label for="color">
          Background Color:
        </label>
        <select value={store.color} id="color" onchange={(e) => store.color((e.target as HTMLSelectElement).value)}>
          <option value="" disabled={store.color() !== ""}>Select</option>
          {forEach(store.backgrounds, (color: string) =>
            <option value={color}>{color}</option>
          )}
        </select>
      </div>

      <div>
        <label for="font">
          Font Family:
        </label>
        <select value={store.font} id="font" onchange={(e) => store.font((e.target as HTMLSelectElement).value)}>
          <option value="" disabled={store.font() !== ""} >Select</option>
          {forEach(store.fonts, (font: string) =>
            <option value={font}>{font}</option>
          )}
        </select>
      </div>

      <div>
        <label for="text">
          Text Content:
        </label>
        <input value={store.text} type="text" id="text" placeholder="e.g. Hello World!" oninput={(e) => store.text((e.target as HTMLInputElement).value)} />
      </div>

      <button type="submit" disabled={resource.loading}>Create</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now run npm run dev.

That’s it! You’ve got a reactive app for generating text-based images. Check the HellaJS docs for more information or visit the HellaJS Github and support the project with a star ⭐.

Top comments (0)