JSX Templating

Sapling utilizes JSX for defining UI components and rendering HTML content. This approach allows you to write HTML-like syntax directly within your JavaScript or TypeScript code, providing a powerful and familiar way to build interfaces. Sapling leverages Hono's JSX implementation (@hono/hono/jsx) for this purpose. It is based on Hono's JSX implementation, which you can learn more about here.

Basic JSX Syntax

JSX lets you write markup that looks similar to HTML:

import { FC } from "@hono/hono/jsx"; // Optional for typing components

// Define a simple functional component
function Greeting({ name }: { name: string }) {
  // Return JSX elements
  return (
    // Tailwind CSS classes are used for styling
    <div className="flex flex-col items-center justify-center h-screen">
      <h1 className="text-2xl font-bold">Hello, {name}!</h1>
    </div>
  );
}

// Usage:
// <Greeting name="World" />

Key differences from HTML:

  • Attribute names use camelCase (e.g., className instead of class, onClick instead of onclick).
  • JavaScript expressions can be embedded using curly braces {}.
  • Components are typically defined as functions or classes.

Dynamic Content Interpolation

Embed JavaScript expressions directly within your JSX using curly braces:

function UserProfile({ user }: { user: { name: string; age: number } }) {
  const profileUrl = `/users/${user.name.toLowerCase()}`;
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Age: {user.age}</p>
      <a href={profileUrl}>View Profile</a>
    </div>
  );
}

Safety Features

JSX automatically escapes interpolated string values to prevent XSS attacks when rendering them as text content.

const userInput = '<script>alert("XSS")</script>';
// Renders as plain text: <div>&lt;script&gt;alert("XSS")&lt;/script&gt;</div>
<div>{userInput}</div>

Note: This automatic escaping only applies to content within curly braces {} used as element children or attribute values. Setting HTML content directly requires specific methods (see dangerouslySetInnerHTML).

Components

Break down your UI into reusable components. Components are typically functions that accept props (properties) as an argument and return JSX.

import { FC } from "@hono/hono/jsx"; // Optional but recommended for type safety

interface HeaderProps {
  siteTitle: string;
}

const Header: FC<HeaderProps> = ({ siteTitle }) => {
  return <header><h1>{siteTitle}</h1></header>;
};

const Page: FC = () => {
  return (
    <div>
      <Header siteTitle="My Awesome Site" />
      <main>Page Content</main>
    </div>
  );
};

Fragments

If you need to return multiple elements from a component without adding an extra wrapper div, use Fragments:

import { Fragment } from "@hono/hono/jsx";

function UserInfo({ name, email }) {
  return (
    <Fragment>
      <h2>{name}</h2>
      <p>{email}</p>
    </Fragment>
    // Short syntax: <>...</> also works
    // <>
    //  <h2>{name}</h2>
    //  <p>{email}</p>
    // </>
  );
}

Rendering Raw HTML

Sometimes you need to render HTML content that comes from an external source (like a CMS or markdown renderer) without escaping. Use this feature with extreme caution as it can expose your application to XSS attacks if the content is not properly sanitized beforehand.

JSX provides the dangerouslySetInnerHTML prop for this:

import { renderMarkdown } from "@sapling/markdown"; // Example markdown renderer

async function Article({ markdownContent }: { markdownContent: string }) {
  // Assume renderMarkdown sanitizes the output or the source is trusted
  const htmlContent = await renderMarkdown(markdownContent);

  return (
    <article
      dangerouslySetInnerHTML={{ __html: htmlContent }} // Content is inserted as-is
    />
  );
}

Key Points:

  • The prop name includes "dangerously" to remind you of the risks.
  • It accepts an object with a __html key, whose value is the raw HTML string.
  • Always sanitize HTML content before using dangerouslySetInnerHTML unless it comes from a completely trusted source.

Conditional Rendering

Use standard JavaScript conditional logic (like ternary operators or &&) within your JSX:

function ConditionalContent({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    <div>
      {isLoggedIn
        ? <button>Logout</button>
        : <button>Login</button>
      }

      {/* Render only if logged in */}
      {isLoggedIn && <p>Welcome back!</p>}
    </div>
  );
}

List Rendering

Render lists of items by mapping over an array and returning JSX for each item. Remember to include a unique key prop for each list item to help efficiently update the DOM.

function ItemList({ items }: { items: { id: string; text: string }[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li> // Use a stable, unique key
      ))}
    </ul>
  );
}

Best Practices

  1. Component Composition: Build complex UIs by composing smaller, reusable components.
  2. Props for Data Down: Pass data down from parent to child components via props.
  3. Type Safety: Use TypeScript with FC or interface definitions for props to catch errors early.
  4. Keys for Lists: Always provide stable key props when rendering lists.
  5. Minimize Raw HTML: Avoid dangerouslySetInnerHTML whenever possible. If used, ensure rigorous sanitization.
  6. Keep Components Focused: Aim for components that do one thing well.

Security Considerations

  • JSX automatically escapes dynamic text content, preventing many common XSS vulnerabilities.
  • dangerouslySetInnerHTML bypasses this escaping. Only use it with trusted, sanitized HTML.
  • Always validate and sanitize any user-generated content before rendering it, especially if using dangerouslySetInnerHTML.
  • Be cautious when rendering content fetched from external APIs or third-party sources.