hono icon indicating copy to clipboard operation
hono copied to clipboard

JSX Renderer Middleware with name

Open Code-Hex opened this issue 1 year ago • 3 comments

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

Code-Hex avatar Feb 13 '24 03:02 Code-Hex

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

yusukebe avatar Feb 14 '24 09:02 yusukebe

@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: []})
})

Code-Hex avatar Feb 16 '24 05:02 Code-Hex

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!

yusukebe avatar Feb 16 '24 07:02 yusukebe