honojs / hono

Web framework built on Web Standards
https://hono.dev
MIT License
19.78k stars 561 forks source link

JSX Renderer Middleware with name #2202

Open Code-Hex opened 8 months ago

Code-Hex commented 8 months ago

What is the feature you are proposing?

Hello! 👋

I propose providing a middleware for jsxRendererWithName(name: string, component, options) and an API like c.renderWithName(name, <h1>Hello, World!</h1>).

This would be useful in cases where we want to create multiple layouts within a single application.

Currently, I believe jsxRenderer is designed to handle only one template. As a workaround to enable the above case, we would have to dynamically switch the rendering content based on the request path from the context. However, this approach leads to the problem of extending the values passed as options for each template.

I believe this proposal can solve these issues.

https://hono.dev/middleware/builtin/jsx-renderer

yusukebe commented 8 months ago

Hi @Code-Hex !

How about this approach? This way, the template has a type (A | B) and the implementation of the handler is clean.

// renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'

export type Template = 'A' | 'B'

declare module 'hono' {
  interface ContextRenderer {
    (content: string | Promise<string>, props?: { title?: string; template: Template }): Response
  }
}

export const renderer = jsxRenderer(({ children, template }) => {
  if (template === 'A') {
    return (
      <html>
        <body>
          <h1>Template A</h1>
          {children}
        </body>
      </html>
    )
  }
  if (template === 'B') {
    return (
      <html>
        <body>
          <h1>Template B</h1>
          {children}
        </body>
      </html>
    )
  }
  return (
    <html>
      <body>
        <h1>Template Default</h1>
        {children}
      </body>
    </html>
  )
})
// index.tsx
import { Hono } from 'hono'
import { renderer, type Template } from './renderer'

const app = new Hono()

app.get('*', renderer)

app.get('/', (c) => {
  const template = c.req.query('template') as Template | undefined
  if (template === 'A') {
    return c.render(<h1>Hello!</h1>, {
      template: 'A'
    })
  }
  if (template === 'B') {
    return c.render(<h1>Hello!</h1>, {
      template: 'B'
    })
  }
  return c.render(<h1>Hello!</h1>)
})

export default app
Code-Hex commented 8 months ago

@yusukebe The problem with that approach, as described in the description, is that it cannot handle branching when each template has different props.

For example, template A might have only a title, while B might have name, items, and so on. The concept is something like the following code (I think it can be written more cleanly):

const jsxRendererWithName<Tmpls extends Record<string, any>> = (
  templates: Tmpls,
  component?: FC<PropsWithChildren<PropsForRenderer & { Layout: FC }>>,
  options?: unknown
): MiddlewareHandler => { ... }

// --- User Side

type Templates = {
  A: { title: string };
  B: { name: string; items: any[] };
}
type TemplatesKey = keyof Templates;

declare module 'hono' {
  interface ContextRenderer {
    <K extends TemplatesKey, P extends Templates[K]>(
      name: K,
      content: string | Promise<string>,
      props: P,
    ): Response | Promise<Response>;
  }
}

app.get("/", async(c) => {
    c.render("A", <div></div>, { title: ""})
    c.render("B", <div></div>, {name: "aa", items: []})
})
yusukebe commented 8 months ago

The problem with that approach, as described in the description, is that it cannot handle branching when each template has different props.

Ah, I see. I can't give you an answer right now, but I am trying to think of a good way to do it!