npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

bloom-router

v0.0.16

Published

Bloom is an experimental approach to building applications using Web Components and Asynchronous Generators, powered by `bloom-router` and `webjsx`.

Downloads

512

Readme

Bloom: An Experimental UI Framework

Bloom is an experimental approach to building applications using Web Components and Asynchronous Generators, powered by bloom-router and webjsx.

  1. Web Components: All components are Web Components, which you can re-use natively or from any framework.
  2. Asynchronous Generators: Rendering is done with an asynchronous generator, yielding dynamic JSX views as the state changes.
  3. Declarative Routing: Define routes and associate them with components using the bloom-router API.

Installation

To use Bloom in your project:

npm install bloom-router webjsx

TypeScript

Ensure your tsconfig.json is set up to handle JSX.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "webjsx"
  }
}

Advanced instructions can be found on WebJSX.

Component API

Declaring Components

Use the component function to define reusable UI elements:

import { component } from "bloom-router";

component("example-component", async function* (component) {
  let count = 0;

  while (true) {
    yield (
      <div>
        <p>Count: {count}</p>
        <button
          onclick={() => {
            count++;
            component.render();
          }}
        >
          Increment
        </button>
      </div>
    );
  }
});

Building an HN Clone

Let's build a Hacker News (HN) clone using Bloom. This example demonstrates how to create a full-featured web application with components, routing, data fetching, and state management.

If you want to jump right into the code, you can edit the HN example on StackBlitz.

Story List - The Home Page

The home page displays a curated list of top stories from Hacker News. When the component mounts, it fetches the IDs of top stories from the HN API, then retrieves detailed data for the top 20 stories. Each story is displayed with its title, score, author link, and comment count. The component handles loading states and provides clear feedback to users while data is being fetched.

component(
  "story-list",
  async function* (component: HTMLElement & BloomComponent) {
    let stories: Story[] | null = null;

    const fetchTopStories = async (limit = 20): Promise<Story[]> => {
      const topIds = await fetch(
        "https://hacker-news.firebaseio.com/v0/topstories.json"
      ).then((res) => res.json());
      const sliced = topIds.slice(0, limit);
      const stories = await Promise.all(
        sliced.map((id: number) =>
          fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`).then(
            (r) => r.json()
          )
        )
      );
      return stories as Story[];
    };

    stories = await fetchTopStories();

    while (true) {
      if (!stories) {
        yield <div>Loading top stories...</div>;
      } else {
        yield (
          <div>
            {stories.map((story: Story) => (
              <div class="story-list-item">
                <a href="#" onclick={() => bloom.goto(`/story/${story.id}`)}>
                  {story.title}
                </a>
                <div class="meta">
                  {story.score} points by <user-link username={story.by} /> |{" "}
                  {story.descendants || 0} comments
                </div>
              </div>
            ))}
          </div>
        );
      }
    }
  }
);

Story Detail Page

When a user clicks on a story title, they're taken to the story detail page. This component fetches and displays comprehensive information about a single story, including its title (linked to the original URL), score, author, and the full comment thread. It provides a back navigation link and gracefully handles cases where the story might not be found.

component(
  "story-detail",
  async function* (
    component: HTMLElement & BloomComponent & { storyid: number }
  ) {
    let story: Story | null = null;

    const fetchData = async (): Promise<Story | null> => {
      try {
        const result = await fetchItem<Story>(component.storyid);
        return result;
      } catch {
        return null;
      }
    };

    story = await fetchData();

    while (true) {
      if (!story?.id) {
        yield (
          <div>
            <a class="back-link" href="#" onclick={() => bloom.goto("/")}>
              Back
            </a>
            <div>Story not found.</div>
          </div>
        );
      } else {
        yield (
          <div>
            <a class="back-link" href="#" onclick={() => bloom.goto("/")}>
              &larr; Back
            </a>
            <h2>
              <a href={story.url} target="_blank" rel="noopener noreferrer">
                {story.title}
              </a>
            </h2>
            <div class="meta">
              {story.score} points by <user-link username={story.by} /> |{" "}
              {story.descendants || 0} comments
            </div>
            <hr />
            <comment-thread parentid={story.id} />
          </div>
        );
      }
    }
  },
  { storyid: 0 }
);

Comment System

The comment system consists of two main components that work together to display threaded discussions:

Comment Thread Component

This component manages the top-level structure of a story's comment thread. It fetches the list of comments for a given story and renders them as a cohesive discussion thread. If there are no comments, it displays an appropriate message.

component(
  "comment-thread",
  async function* (
    component: HTMLElement & BloomComponent & { parentid: number }
  ) {
    let parentData: CommentData | null = null;

    const fetchData = async (): Promise<CommentData | null> => {
      try {
        const result = await fetchItem<CommentData>(component.parentid);
        return result;
      } catch {
        return null;
      }
    };

    parentData = await fetchData();

    while (true) {
      if (!parentData?.kids?.length) {
        yield <div>No comments yet.</div>;
      } else {
        yield (
          <div class="comments-container">
            {parentData.kids.map((kidId: number) => (
              <comment-item commentid={kidId} />
            ))}
          </div>
        );
      }
    }
  },
  { parentid: 0 }
);

Comment Item Component

This component handles the display of individual comments, including any nested replies. It supports HTML content in comment text and implements proper indentation for nested comments. The component gracefully handles deleted comments and missing content.

component(
  "comment-item",
  async function* (
    component: HTMLElement & BloomComponent & { commentid: number }
  ) {
    let commentData: CommentData | null = null;

    const fetchData = async (): Promise<CommentData | null> => {
      try {
        const result = await fetchItem<CommentData>(component.commentid);
        return result;
      } catch {
        return null;
      }
    };

    commentData = await fetchData();

    while (true) {
      if (!commentData?.id) {
        yield <div class="comment">(deleted)</div>;
      } else {
        return (
          <div class="comment">
            <div class="comment-meta">
              {commentData.by ? (
                <user-link username={commentData.by} />
              ) : (
                "(deleted)"
              )}
            </div>
            <div
              class="comment-text"
              {...{ innerHTML: commentData.text || "(no text)" }}
            ></div>
            {commentData.kids && commentData.kids.length > 0 && (
              <div class="nested-comments" style="margin-left: 20px;">
                {commentData.kids.map((kidId: number) => (
                  <comment-item commentid={kidId} />
                ))}
              </div>
            )}
          </div>
        );
      }
    }
  },
  { commentid: 0 }
);

User Profile Page

The user profile page provides detailed information about a user's presence on the platform. It displays the user's karma score, account creation date, about section (if available), and a list of their recent submissions. The component implements type guards to ensure data integrity and handles missing or invalid user data appropriately.

component(
  "user-profile",
  async function* (
    component: HTMLElement & BloomComponent & { username: string }
  ) {
    let userData: UserData | null = null;
    let userStories: Story[] = [];

    const fetchUser = async (username: string): Promise<UserData> => {
      return fetch(
        `https://hacker-news.firebaseio.com/v0/user/${username}.json`
      ).then((r) => r.json());
    };

    const fetchData = async (): Promise<[UserData | null, Story[]]> => {
      try {
        const user = await fetchUser(component.username);
        const submissions = user.submitted || [];
        const stories = await Promise.all(
          submissions.slice(0, 10).map((id) => fetchItem<Story>(id))
        );
        return [
          user,
          stories.filter(
            (s): s is Story =>
              typeof s?.id === "number" &&
              typeof s?.title === "string" &&
              typeof s?.score === "number" &&
              typeof s?.by === "string"
          ),
        ];
      } catch {
        return [null, []];
      }
    };

    [userData, userStories] = await fetchData();

    while (true) {
      if (!userData) {
        yield (
          <div>
            <a class="back-link" href="#" onclick={() => bloom.goto("/")}>
              Back
            </a>
            <div>User not found.</div>
          </div>
        );
      } else {
        return (
          <div>
            <a class="back-link" href="#" onclick={() => bloom.goto("/")}>
              &larr; Back
            </a>
            <h2>User: {userData.id}</h2>
            <div class="user-info">
              <p>Karma: {userData.karma}</p>
              <p>
                Created:{" "}
                {new Date(userData.created * 1000).toLocaleDateString()}
              </p>
              {userData.about && (
                <div class="about">
                  <h3>About</h3>
                  <div {...{ innerHTML: userData.about }}></div>
                </div>
              )}
            </div>
            <div class="user-submissions">
              <h3>Recent Submissions</h3>
              {userStories.map((story) => (
                <div class="story-list-item">
                  <a href="#" onclick={() => bloom.goto(`/story/${story.id}`)}>
                    {story.title}
                  </a>
                  <div class="meta">
                    {story.score} points | {story.descendants || 0} comments
                  </div>
                </div>
              ))}
            </div>
          </div>
        );
      }
    }
  },
  { username: "" }
);

User Link Component

A utility component used throughout the application to create consistent user profile links. It takes a username prop and renders a clickable link that navigates to that user's profile page.

component(
  "user-link",
  async function* (
    component: HTMLElement & BloomComponent & { username: string }
  ) {
    while (true) {
      yield (
        <a href="#" onclick={() => bloom.goto(`/user/${component.username}`)}>
          {component.username}
        </a>
      );
    }
  },
  { username: "" }
);

###Application Types

The application uses TypeScript interfaces to ensure type safety across components:

type Story = {
  id: number;
  title: string;
  url?: string;
  score: number;
  by: string;
  descendants?: number;
  kids?: number[];
};

type CommentData = {
  id: number;
  by: string;
  text: string;
  kids?: number[];
};

type UserData = {
  id: string;
  created: number;
  karma: number;
  about?: string;
  submitted?: number[];
};

Routing Setup

Finally, we configure the routes for our application:

const bloom = new Bloom("app");

bloom.page("/", async function* () {
  while (true) {
    yield <story-list />;
  }
});

bloom.page("/story/:id", async function* (params) {
  const storyId = parseInt(params.id, 10);
  while (true) {
    yield <story-detail storyid={storyId} />;
  }
});

bloom.page("/user/:id", async function* (params) {
  while (true) {
    yield <user-profile username={params.id} />;
  }
});

bloom.goto("/");

This example demonstrates how Bloom components work together to create a full-featured web application, handling data fetching, state management, routing, and complex UI interactions.

License

MIT