Handle forms (no-JS)
This is a full example of how to handle forms in Gracile. Server-only handling, no JS needed.
π /src/routes/form.ts
import { defineRouteimport defineRoute } from '@gracile/gracile/route';
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { documentconst document: () => any } from '../document.js';
// -----------------------------------------------------------------------------
let achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
= [{ namename: string : 'initial record', coolnessFactorcoolnessFactor: number : 5 }];
Just a bit of optional setup, for easier refactoring and to keep a bird-eye view.
const FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
= {
fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
: {
actionaction: "action" : 'action',
achievementNameachievementName: "achievement_name" : 'achievement_name',
coolnessFactorcoolnessFactor: "coolness_factor" : 'coolness_factor',
filterByNamefilterByName: "filter_by_name" : 'filter_by_name',
},
actionsactions: {
readonly deleteAll: "delete_all";
readonly add: "add";
}
: {
deleteAlldeleteAll: "delete_all" : 'delete_all',
addadd: "add" : 'add',
},
} as consttype const = {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
;
// -----------------------------------------------------------------------------
export default defineRouteimport defineRoute ({
Order matters! Handlers return inferred props. for doc/page afterward.
handlerhandler: {
GET: (context: any) => Response | {
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly filterByName: any;
};
POST: (context: any) => Promise<Response | {
readonly success: false;
readonly message: "Wrong form input.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly payload: {
readonly name: any;
readonly coolnessFactor: any;
};
} | {
readonly success: false;
readonly message: "Unknown form action.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly payload?: undefined;
}>;
}
: {
GETtype GET: (context: any) => Response | {
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly filterByName: any;
}
: (contextcontext: any ) => {
const filterByNameconst filterByName: any = contextcontext: any .url.searchParams.get(
FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.filterByNamefilterByName: "filter_by_name" ,
);
if (filterByNameconst filterByName: any ) {
const filteredconst filtered: {
name: string;
coolnessFactor: number;
}[]
= achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
.filterArray<{ name: string; coolnessFactor: number; }>.filter(predicate: (value: {
name: string;
coolnessFactor: number;
}, index: number, array: {
name: string;
coolnessFactor: number;
}[]) => unknown, thisArg?: any): {
name: string;
coolnessFactor: number;
}[] (+1 overload)
Returns the elements of an array that meet the condition specified in a callback function.
((achievementachievement: {
name: string;
coolnessFactor: number;
}
) =>
achievementachievement: {
name: string;
coolnessFactor: number;
}
.namename: string .includesString.includes(searchString: string, position?: number): booleanReturns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
(filterByNameconst filterByName: any ),
);
return { achievementsachievements: {
name: string;
coolnessFactor: number;
}[]
: filteredconst filtered: {
name: string;
coolnessFactor: number;
}[]
, filterByNamefilterByName: any } as consttype const = {
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly filterByName: any;
}
;
}
Clean-up empty search parameter
if (filterByNameconst filterByName: any === '') {
contextcontext: any .url.searchParams.delete(FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.filterByNamefilterByName: "filter_by_name" );
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.redirectfunction redirect(url: string | URL, status?: number): ResponseThe redirect() static method of the Response interface returns a Response resulting in a redirect to the specified URL.
(contextcontext: any .url);
}
return { achievementsachievements: {
name: string;
coolnessFactor: number;
}[]
: achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
, filterByNamefilterByName: any } as consttype const = {
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly filterByName: any;
}
;
},
POSTtype POST: (context: any) => Promise<Response | {
readonly success: false;
readonly message: "Wrong form input.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly payload: {
readonly name: any;
readonly coolnessFactor: any;
};
} | {
readonly success: false;
readonly message: "Unknown form action.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly payload?: undefined;
}>
: async (contextcontext: any ) => {
const formDataconst formData: any = await contextcontext: any .request.formData();
const actionconst action: any = formDataconst formData: any .get(FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.actionaction: "action" )?.toString();
switch (actionconst action: any ) {
case FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.actionsactions: {
readonly deleteAll: "delete_all";
readonly add: "add";
}
.addadd: "add" :
{
const nameconst name: any = formDataconst formData: any .get(FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.achievementNameachievementName: "achievement_name" )?.toString();
const coolnessFactorconst coolnessFactor: any = formDataconst formData: any
.get(FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.coolnessFactorcoolnessFactor: "coolness_factor" )
?.toString();
Basic form data shape validation.
if (nameconst name: any && coolnessFactorconst coolnessFactor: any && nameconst name: any .length >= 3) {
achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
.pushArray<{ name: string; coolnessFactor: number; }>.push(...items: {
name: string;
coolnessFactor: number;
}[]): number
Appends new elements to the end of an array, and returns the new length of the array.
({
namename: string ,
coolnessFactorcoolnessFactor: number : Numbervar Number: NumberConstructor
(value?: any) => number
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
(coolnessFactorconst coolnessFactor: any ),
});
} else {
contextcontext: any .responseInit.status = 400;
We want the user data to be repopulated in the page after a failed `POST`.
return {
successsuccess: false : false,
messagemessage: "Wrong form input." : 'Wrong form input.',
achievementsachievements: {
name: string;
coolnessFactor: number;
}[]
: achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
,
payloadpayload: {
readonly name: any;
readonly coolnessFactor: any;
}
: { namename: any , coolnessFactorcoolnessFactor: any },
} as consttype const = {
readonly success: false;
readonly message: "Wrong form input.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
readonly payload: {
readonly name: any;
readonly coolnessFactor: any;
};
}
;
}
}
break;
case FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.actionsactions: {
readonly deleteAll: "delete_all";
readonly add: "add";
}
.deleteAlldeleteAll: "delete_all" :
achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
= [];
break;
default:
contextcontext: any .responseInit.status = 422;
return {
successsuccess: false : false,
messagemessage: "Unknown form action." : 'Unknown form action.',
achievementsachievements: {
name: string;
coolnessFactor: number;
}[]
: achievementsDblet achievementsDb: {
name: string;
coolnessFactor: number;
}[]
,
} as consttype const = {
readonly success: false;
readonly message: "Unknown form action.";
readonly achievements: {
name: string;
coolnessFactor: number;
}[];
}
;
}
Using the "POST/Redirect/GET" pattern to avoid duplicate form submissions
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
The Response interface of the Fetch API represents the response to a request.
.redirectfunction redirect(url: string | URL, status?: number): ResponseThe redirect() static method of the Response interface returns a Response resulting in a redirect to the specified URL.
(contextcontext: any .url, 303);
},
},
documentdocument: (context: any) => any : (contextcontext: any ) => documentfunction document(): any (contextcontext: any ),
templatetemplate: (context: any) => TemplateResult<1> : (contextcontext: any ) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<h1>Achievements manager</h1>
<form method="post">
<input
type="hidden"
name=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.actionaction: "action" }
value=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.actionsactions: {
readonly deleteAll: "delete_all";
readonly add: "add";
}
.addadd: "add" }
/>
<input
type="text"
name=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.achievementNameachievementName: "achievement_name" }
value=${contextcontext: any .props.POST?.payload?.name ?? ''}
required
/>
<input
type="number"
value=${contextcontext: any .props.POST?.payload?.coolnessFactor ?? 1}
name=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.coolnessFactorcoolnessFactor: "coolness_factor" }
/>
<footer>
<button>Add an achievement</button>
${contextcontext: any .props.POST?.success === false
? htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<strong>Something went wrong!</strong>
${contextcontext: any .props.POST?.message
? htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
` <strong>${contextcontext: any .props.POST.message}</strong> `
: null}
`
: null}
</footer>
</form>
<hr />
<form>
<input
type="text"
name=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.filterByNamefilterByName: "filter_by_name" }
value=${contextcontext: any .props.GET?.filterByName ?? ''}
/>
<button>Filter by name</button>
</form>
<ul>
${(contextcontext: any .props.GET || contextcontext: any .props.POST)?.achievements?.map(
(achievementachievement: any ) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<li>
<!-- -->
<em>${achievementachievement: any .coolnessFactor}</em> -
<strong>${achievementachievement: any .name}</strong>
</li>
`,
)}
</ul>
<hr />
<form method="post">
<input
type="hidden"
name=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.fieldsfields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
}
.actionaction: "action" }
value=${FORMconst FORM: {
readonly fields: {
readonly action: "action";
readonly achievementName: "achievement_name";
readonly coolnessFactor: "coolness_factor";
readonly filterByName: "filter_by_name";
};
readonly actions: {
readonly deleteAll: "delete_all";
readonly add: "add";
};
}
.actionsactions: {
readonly deleteAll: "delete_all";
readonly add: "add";
}
.deleteAlldeleteAll: "delete_all" }
/>
<button>Delete all</button>
</form>
<hr />
<footer>
<pre>${JSONvar JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
.stringifyJSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
({ propsprops: any : contextcontext: any .props }, null, 2)}</pre>
</footer>
`,
});