Skip to content

Commit

Permalink
feat: native form parser
Browse files Browse the repository at this point in the history
  • Loading branch information
ido-pluto committed Dec 18, 2023
1 parent 18b825c commit dbef97d
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 46 deletions.
4 changes: 2 additions & 2 deletions examples/simple-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"dependencies": {
"@astro-utils/express-endpoints": "^0.0.1",
"@astro-utils/forms": "^0.0.1",
"@astrojs/react": "^3.0.6",
"astro": "^3.5.5",
"@astrojs/react": "^3.0.7",
"astro": "^4.0.6",
"bootstrap": "^5.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
4 changes: 2 additions & 2 deletions examples/simple-form/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ let showSubmitText: string;
function formSubmit(){
Astro.locals.session.counter ??= 0;
Astro.locals.session.counter++;
showSubmitText = `You name is ${form.name}, you are ${form.age} years old. `;
showSubmitText = `Your name is ${form.name}, you are ${form.age} years old. `;
}
---
<Layout title="Welcome to Astro Metro.">
<Layout title="Form + Session">
<BindForm bind={form}>
<FormErrors/>
{showSubmitText}
Expand Down
38 changes: 38 additions & 0 deletions examples/simple-form/src/pages/upload.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
import Layout from '../layouts/Layout.astro';
import {BButton, Bind, BindForm, BInput, FormErrors} from '@astro-utils/forms/forms.js';
import {Button} from 'reactstrap';
import 'bootstrap/dist/css/bootstrap.css';
type Form = {
name: string;
file: File;
}
const form = Bind<Form>();
let showSubmitText: string;
function formSubmit() {
Astro.locals.session.counter ??= 0;
Astro.locals.session.counter++;
showSubmitText = `Your name is ${form.name}, you upload "${form.file.name}"`;
}
---
<Layout title="Upload file">
<BindForm bind={form}>
<FormErrors/>
{showSubmitText}

{Astro.locals.session.counter &&
<p>You have submitted {Astro.locals.session.counter} times.</p>
}

<h4>What you name*</h4>
<BInput type="text" name="name" maxlength={20} required/>

<h4>File to upload*</h4>
<BInput type="file" name="file" required/>

<BButton as={Button} props={{color: 'success'}} onClick={formSubmit} whenFormOK>Submit</BButton>
</BindForm>
</Layout>
3 changes: 1 addition & 2 deletions packages/forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
},
"dependencies": {
"@astro-utils/context": "0.0.1",
"@astro-utils/formidable": "0.0.1",
"await-lock": "^2.2.2",
"cookie": "^0.5.0",
"csrf": "^3.1.0",
Expand All @@ -72,6 +71,6 @@
"zod": "^3.19.1"
},
"peerDependencies": {
"astro": "^3.5.5"
"astro": "^4.0.6"
}
}
11 changes: 5 additions & 6 deletions packages/forms/src/components/form-utils/parse.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { isFormidableFile } from "@astro-utils/formidable";
import { AstroGlobal } from "astro";
import { z } from "zod";
import { getFormMultiValue } from "../../form-tools/post.js";
import AboutFormName from "./about-form-name.js";
import {AstroGlobal} from 'astro';
import {z} from 'zod';
import {getFormMultiValue} from '../../form-tools/post.js';
import AboutFormName from './about-form-name.js';

const HEX_COLOR_REGEX = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i;

Expand Down Expand Up @@ -65,7 +64,7 @@ export async function parseFiles(about: AboutFormName, astro: AstroGlobal, multi
}

for(const value of values){
if(!isFormidableFile(value)){
if (!(value instanceof File)) {
about.pushErrorManually('upload-not-file', 'The upload value is not a file');
break;
}
Expand Down
38 changes: 10 additions & 28 deletions packages/forms/src/form-tools/post.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,34 @@
import parseAstroForm, {ExtendedFormData, FormDataValue, VolatileFile} from '@astro-utils/formidable';
import {FORM_OPTIONS} from '../settings.js';
import AwaitLockDefault from 'await-lock';
import {AstroLinkHTTP} from '../utils.js';
import {validateFrom} from './csrf.js';
import AwaitLock from 'await-lock';

const AwaitLock = AwaitLockDefault.default || AwaitLockDefault;

export function isPost(astro: {request: Request}){
return astro.request.method === "POST";
}

function extractDeleteMethods(formData: ExtendedFormData | FormData){
return [...formData].map(([_, value]) => {
if (value instanceof VolatileFile) {
return value.destroy.bind(value);
}
}).filter(Boolean);
}

export function deleteFormFiles(request: AstroLinkHTTP['request']) {
//@ts-ignore
request.formData?.deleteFiles?.forEach(fn => fn());
}

export async function parseFormData(request: Request): Promise<ExtendedFormData> {
export async function parseFormData(request: Request): Promise<FormData> {
//@ts-ignore
const lock = request.formDataLock ??= new AwaitLock();
await lock.acquireAsync();

try {
if(request.formData.name === ''){ // this is the anonymous function we created
return await request.formData() as any;
}

const formData = await parseAstroForm(request, FORM_OPTIONS.forms);
request.formData = () => <any>Promise.resolve(formData);
//@ts-ignore
request.formData.deleteFiles = extractDeleteMethods(formData);
const formData = await request.formData();
request.formData = () => Promise.resolve(formData);

return formData;
} finally {
lock.release();
}
}

export async function getFormValue(request: Request, key: string): Promise<FormDataValue | null>{
export async function getFormValue(request: Request, key: string): Promise<FormDataEntryValue | null> {
const data = await parseFormData(request);
return data.get(key) as any;
return data.get(key);
}

export async function getFormMultiValue(request: Request, key: string): Promise<FormDataValue[]> {
export async function getFormMultiValue(request: Request, key: string): Promise<FormDataEntryValue[]> {
const data = await parseFormData(request);
return data.getAll(key);
}
Expand Down
10 changes: 4 additions & 6 deletions packages/forms/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {APIContext, MiddlewareEndpointHandler, MiddlewareNextResponse} from 'astro';
import {APIContext, MiddlewareHandler, MiddlewareNext} from 'astro';
import {DEFAULT_SETTINGS as DEFAULT_SETTINGS_CSRF, ensureValidationSecret} from './form-tools/csrf.js';
import {JWTSession} from './jwt-session.js';
import {FORM_OPTIONS, FormsSettings} from './settings.js';
import {v4 as uuid} from 'uuid';
import defaults from 'defaults';
import {deleteFormFiles} from './form-tools/post.js';
import {timeout} from 'promise-timeout';

const DEFAULT_FORM_OPTIONS: FormsSettings = {
Expand All @@ -29,7 +28,7 @@ const DEFAULT_FORM_OPTIONS: FormsSettings = {
export default function astroForms(settings: Partial<FormsSettings> = {}){
Object.assign(FORM_OPTIONS, defaults(settings, DEFAULT_FORM_OPTIONS));

return async function onRequest ({ locals, request, cookies }: APIContext , next: MiddlewareNextResponse) {
return async function onRequest({locals, request, cookies}: APIContext, next: MiddlewareNext) {
const session = new JWTSession(cookies);
locals.session = session.sessionData;

Expand All @@ -39,7 +38,6 @@ export default function astroForms(settings: Partial<FormsSettings> = {}){
locals.__formsInternalUtils = {
onWebFormClose() {
session.setCookieHeader(response.headers);
deleteFormFiles(request);
pageFinished();
}
};
Expand All @@ -52,10 +50,10 @@ export default function astroForms(settings: Partial<FormsSettings> = {}){
const pageFinishedPromise = new Promise(resolve => pageFinished = resolve);
await timeout(pageFinishedPromise, FORM_OPTIONS.pageLoadTimeoutMS);
} catch {
throw new Error('WebForms is not used in this page');
console.warn('WebForms page load timeout (are you sure you are using WebForms?)');
}
}

return response;
} as MiddlewareEndpointHandler;
} as MiddlewareHandler;
}

0 comments on commit dbef97d

Please sign in to comment.