闭环
一个路由、一个表单、三种结果:空提交以 422 重渲染并回显,合法提交以 303 回答到 PRG 目标,命名 action 在同一表单上提供第二个动词。这是路由与数据中的应用闭环,压缩到最小可运行形态。
组件
import { element, OpenElement, property } from '@openelement/element';
@element('contact-page')
export default class ContactPage extends OpenElement {
@property({ reflect: false, attribute: false })
error = '';
@property({ reflect: false, attribute: false })
echoed = '';
render() {
return (
<main>
<h1>contact</h1>
<form method='post' data-open-enhance>
<input name='message' type='text' />
<button type='submit'>Send</button>
<button type='submit' formaction='?/shout'>Shout</button>
</form>
{this.error ? <p role='alert'>{this.error}</p> : <span></span>}
{this.echoed ? <p>echo={this.echoed}</p> : <span></span>}
</main>
);
}
}
路由
import { definePage, fail, redirect } from '@openelement/router';
import ContactPage from '../components/page-contact.tsx';
export function action(ctx: { formData: FormData }) {
const message = String(ctx.formData.get('message') ?? '').trim();
if (!message) {
return fail(422, { error: 'message is required' });
}
throw redirect('/contact?echoed=' + encodeURIComponent(message));
}
export const actions = {
shout(ctx: { formData: FormData }): never {
const message = String(ctx.formData.get('message') ?? '').trim() || 'silence';
throw redirect('/contact?echoed=' + encodeURIComponent(message.toUpperCase()));
},
};
export default definePage(ContactPage, {
renderIntent: { mode: 'dynamic' },
props({ actionData, request }) {
const data = actionData as { error?: string } | undefined;
const echoed = request ? new URL(request.url).searchParams.get('echoed') : undefined;
return { error: data?.error ?? '', echoed: echoed ?? '' };
},
});
为什么不需要 JavaScript 也能工作
表单是纯 HTML:浏览器 POST,action 以 422 或 303 回答,路由重渲染。data-open-enhance 只在 island 已 hydrate 的地方升级体验(fetch + 就地 morph)。action 必须在校验失败后可安全重跑——正是这个约束让无 JS 路径与增强路径跑同一份代码。