diff --git a/.changeset/mean-pianos-cheer.md b/.changeset/mean-pianos-cheer.md
new file mode 100644
index 00000000..06e0b3f9
--- /dev/null
+++ b/.changeset/mean-pianos-cheer.md
@@ -0,0 +1,5 @@
+---
+"frog": patch
+---
+
+Added access to `previousState` and `previousButtonValues` in Image Handler.
diff --git a/playground/src/initial.tsx b/playground/src/initial.tsx
index 6be37879..ef4b83ca 100644
--- a/playground/src/initial.tsx
+++ b/playground/src/initial.tsx
@@ -1,11 +1,16 @@
import { Button, Frog } from 'frog'
import { Heading, VStack, vars } from './ui.js'
-export const app = new Frog({
+export const app = new Frog<{ State: { counter: number } }>({
ui: { vars },
+ initialState: { counter: 0 },
+ verify: false,
title: 'Initial',
})
.frame('/', (c) => {
+ c.deriveState((prev) => {
+ prev.counter++
+ })
return c.res({
image: '/refreshing-image/cool-parameter',
intents: [],
@@ -21,7 +26,8 @@ export const app = new Frog({
image: (
- Current time: {new Date().toISOString()}
+ Current time: {new Date().toISOString()}. Counter:{' '}
+ {c.previousState.counter}
),
diff --git a/site/pages/concepts/image-handler.mdx b/site/pages/concepts/image-handler.mdx
index bd0c7d8a..b62a8f86 100644
--- a/site/pages/concepts/image-handler.mdx
+++ b/site/pages/concepts/image-handler.mdx
@@ -23,6 +23,7 @@ app.frame('/', (c) => { // [!code focus]
})
app.image('/img', (c) => { // [!code focus]
+ /* Access frame's state via `c.previousState` */
return c.res({/* ... */})
})
```
@@ -51,6 +52,7 @@ app.frame('/', (c) => {
})
app.image('/img', (c) => {
+ /* Access frame's state via `c.previousState` */
return c.res({
headers: { // [!code focus]
'Cache-Control': 'max-age=0' // [!code focus]
diff --git a/site/pages/reference/frog-image-context.mdx b/site/pages/reference/frog-image-context.mdx
index 8fee984c..b3bfce7c 100644
--- a/site/pages/reference/frog-image-context.mdx
+++ b/site/pages/reference/frog-image-context.mdx
@@ -17,6 +17,57 @@ app.image('/', (c) => { // [!code focus]
An image handler can also be asynchronous (ie. `async (c) => { ... }{:js}`).
:::
+## previousButtonValues
+
+- **Type**: `string[]`
+
+The data of the previous intents.
+
+```tsx twoslash
+// @noErrors
+/** @jsxImportSource frog/jsx */
+// ---cut---
+import { Button, Frog } from 'frog'
+
+export const app = new Frog({
+ title: 'Frog Frame'
+})
+
+app.frame('/', (c) => {
+ const { previousButtonValues } = c // [!code focus]
+ return c.res({/* ... */})
+})
+```
+
+## previousState
+
+- **Type**: `State`
+
+The state of the previous frame.
+
+```tsx twoslash
+// @noErrors
+/** @jsxImportSource frog/jsx */
+// ---cut---
+import { Button, Frog } from 'frog'
+
+type State = {
+ values: string[]
+}
+
+export const app = new Frog<{ State: State }>({
+ initialState: {
+ values: []
+ },
+ title: 'Frog Frame',
+})
+
+app.frame('/', (c) => {
+ const { previousState } = c // [!code focus]
+ return c.res({/* ... */})
+})
+```
+
## req
- **Type**: `Request`
diff --git a/src/frog-base.tsx b/src/frog-base.tsx
index 23077d00..499bde08 100644
--- a/src/frog-base.tsx
+++ b/src/frog-base.tsx
@@ -52,6 +52,7 @@ import { parseImage } from './utils/parseImage.js'
import { parseIntents } from './utils/parseIntents.js'
import { parsePath } from './utils/parsePath.js'
import { requestBodyToContext } from './utils/requestBodyToContext.js'
+import { requestBodyToImageContext } from './utils/requestBodyToImageContext.js'
import { serializeJson } from './utils/serializeJson.js'
import { toSearchParams } from './utils/toSearchParams.js'
import { version } from './version.js'
@@ -711,7 +712,15 @@ export class FrogBase<
return nonMiddlewareMatchedRoutes.length !== 0
})()
- if (isHandlerPresentOnImagePath) return `${baseUrl + parsePath(image)}`
+ if (isHandlerPresentOnImagePath)
+ return `${baseUrl + parsePath(image)}${
+ context.status !== 'initial'
+ ? `?${toSearchParams({
+ previousState,
+ previousButtonValues: buttonValues,
+ }).toString()}`
+ : ''
+ }`
return `${assetsUrl + parsePath(image)}`
})()
@@ -928,7 +937,10 @@ export class FrogBase<
const assetsUrl = origin + parsePath(this.assetsPath)
const { context } = getImageContext({
- context: c,
+ context: await requestBodyToImageContext(c, {
+ secret: this.secret,
+ }),
+ initialState: this._initialState,
})
const response = await handler(context)
diff --git a/src/types/context.ts b/src/types/context.ts
index 4df9a641..4120899b 100644
--- a/src/types/context.ts
+++ b/src/types/context.ts
@@ -294,6 +294,8 @@ export type ImageContext<
env extends Env = Env,
path extends string = string,
input extends Input = {},
+ //
+ _state = env['State'],
> = {
/**
* `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers.
@@ -308,6 +310,14 @@ export type ImageContext<
* @see https://hono.dev/api/context#env
*/
env: Context_hono['env']
+ /**
+ * Button values from the previous frame.
+ */
+ previousButtonValues?: FrameButtonValue[] | undefined
+ /**
+ * State from the previous frame.
+ */
+ previousState: _state
/**
* Hono request object.
*
diff --git a/src/utils/getImageContext.ts b/src/utils/getImageContext.ts
index 6f6369d8..f54d8615 100644
--- a/src/utils/getImageContext.ts
+++ b/src/utils/getImageContext.ts
@@ -1,5 +1,5 @@
-import type { Context as Context_hono, Input } from 'hono'
-import type { ImageContext } from '../types/context.js'
+import type { Input } from 'hono'
+import type { Context, ImageContext } from '../types/context.js'
import type { Env } from '../types/env.js'
type GetImageContextParameters<
@@ -9,7 +9,11 @@ type GetImageContextParameters<
//
_state = env['State'],
> = {
- context: Context_hono
+ context: Omit<
+ Context,
+ 'frameData' | 'verified' | 'status' | 'initialPath'
+ >
+ initialState?: _state
}
type GetImageContextReturnType<
@@ -19,7 +23,7 @@ type GetImageContextReturnType<
//
_state = env['State'],
> = {
- context: ImageContext
+ context: ImageContext
}
export function getImageContext<
@@ -31,12 +35,13 @@ export function getImageContext<
>(
parameters: GetImageContextParameters,
): GetImageContextReturnType {
- const { context } = parameters
- const { env, req } = context || {}
+ const { context, initialState } = parameters
+ const { env, previousState, req } = context || {}
return {
context: {
env,
+ previousState: previousState ?? (initialState as _state),
req,
res: (data) => ({ data, format: 'image', status: 'success' }),
var: context.var,
diff --git a/src/utils/requestBodyToImageContext.ts b/src/utils/requestBodyToImageContext.ts
new file mode 100644
index 00000000..d66c3e1d
--- /dev/null
+++ b/src/utils/requestBodyToImageContext.ts
@@ -0,0 +1,56 @@
+import type { Context as Context_hono, Input } from 'hono'
+import type { FrogConstructorParameters } from '../frog-base.js'
+import type { Context } from '../types/context.js'
+import type { Env } from '../types/env.js'
+import { fromQuery } from './fromQuery.js'
+import { getRequestUrl } from './getRequestUrl.js'
+import * as jws from './jws.js'
+
+type RequestBodyToImageContextOptions = {
+ secret?: FrogConstructorParameters['secret']
+}
+
+type RequestBodyToImageContextReturnType<
+ env extends Env = Env,
+ path extends string = string,
+ input extends Input = {},
+ //
+ _state = env['State'],
+> = Omit<
+ Context,
+ 'frameData' | 'verified' | 'status' | 'initialPath'
+>
+
+export async function requestBodyToImageContext<
+ env extends Env,
+ path extends string,
+ input extends Input,
+ //
+ _state = env['State'],
+>(
+ c: Context_hono,
+ { secret }: RequestBodyToImageContextOptions,
+): Promise> {
+ const { previousState, previousButtonValues } = await (async () => {
+ if (c.req.query()) {
+ let { previousState, previousButtonValues } = fromQuery(
+ c.req.query(),
+ ) as any
+ if (secret && previousState)
+ previousState = JSON.parse(await jws.verify(previousState, secret))
+ return { previousState, previousButtonValues }
+ }
+ return {} as any
+ })()
+
+ const url = getRequestUrl(c.req)
+
+ return {
+ env: c.env,
+ previousState,
+ previousButtonValues,
+ req: c.req,
+ url: url.href,
+ var: c.var,
+ }
+}