Accel Web
This is a library for integrating Astro and Accel Record.
paginate
is a function that generates pagination components. It takes a query and options as arguments.
---
import { paginate } from "accel-web";
import { Account } from "src/models";
const url = new URL(Astro.request.url);
const page = Number(url.searchParams.get("p")) || 1;
const { Nav, PageEntriesInfo } = paginate(Account.order("id", "desc"), { page, per: 10, window: 1 });
---
<!-- nav -->
<Nav />
<!-- info -->
<PageEntriesInfo />
The above code will render a result like:
<nav>
<ul class="pagination">
<li class="page-item first">
<a href="/?p=1" class="page-link">« First</a>
</li>
<li class="page-item prev">
<a rel="prev" href="/?p=2" class="page-link">‹ Prev</a>
</li>
<li class="page-item disabled gap">
<span class="page-link">…</span>
</li>
<li class="page-item page">
<a href="/?p=2" class="page-link"> 2 </a>
</li>
<li class="page-item page active">
<span class="page-link">3</span>
</li>
<li class="page-item page">
<a href="/?p=4" class="page-link"> 4 </a>
</li>
<li class="page-item disabled gap">
<span class="page-link">…</span>
</li>
<li class="page-item next">
<a rel="next" href="/?p=4" class="page-link">Next ›</a>
</li>
<li class="page-item last">
<a href="/?p=6" class="page-link">Last »</a>
</li>
</ul>
</nav>
<div class="page-entries-info">Displaying <b>21 - 30</b> of <b>55</b> in total</div>
The following styles will be applied when Bootstrap 5 is used. You can override the styles by adding your own CSS.
Form and Request Parameters
formFor
is a function that generates form components. It takes a record as an argument.
RequestParameters
is a class that parses the request parameters.
These tools make it easy to create forms and retrieve request parameters.
Below is an example of a sign-up form.
---
// src/pages/signup.astro
import { formFor, RequestParameters } from "accel-web";
import Layout from "../layouts/Layout.astro";
import { Account } from "../models";
import { z } from "astro/zod";
const account = Account.build({});
if (Astro.request.method === "POST") {
const params = await RequestParameters.from(Astro.request, Astro.params);
// Using zod, you can validate request parameters and extract them with type information
const accountParams = params.require("account").parseWith(
z.object({
email: z.string(),
password: z.string(),
passwordConfirmation: z.string(),
})
);
// If strict type information is not required, you can write it as follows
// const accountParams = params.require("account").permit("email", "password", "passwordConfirmation");
if (account.update(accountParams)) {
return Astro.redirect("/");
}
}
// `formFor` is a helper function that creates a form object for the model
const { Form, Submit, Label, TextField, PasswordField } = formFor(account);
---
<Layout title="Sign Up">
<main class="container" style="max-width: 700px; min-width: 400px;">
<h1>Sign Up</h1>
<Form method="post" class="form">
{
account.errors.fullMessages.length > 0 && (
<div class="alert alert-danger">
{account.errors.fullMessages.map((message) => (
<div>{message}</div>
))}
</div>
)
}
<div class="mb-3">
<!--
The value of Label will be set according to the i18n settings configured in i18next.
`accelrecord.attributes.Account.email` is the key to be translated.
-->
<Label for="email" />
<!-- The value of TextField will be pre-filled with the value of account.email -->
<TextField attr="email" class="form-control" />
</div>
<div class="mb-3">
<Label for="password" />
<PasswordField attr="password" class="form-control" />
</div>
<div class="mb-3">
<Label for="passwordConfirmation" />
<PasswordField attr="passwordConfirmation" class="form-control" />
</div>
<div class="d-flex justify-content-between">
<Submit class="btn btn-secondary">Sign Up</Submit>
<a href="/" class="btn btn-link">Back</a>
</div>
</Form>
</main>
</Layout>
SearchFormFor
searchFormFor
is a function that generates a search form component.
The following is an example of a search form using searchFormFor
. You can implement a search form and pagination by combining it with the previously introduced paginate
.
---
// src/pages/todos/index.astro
import { paginate, RequestParameters, searchFormFor } from "accel-web";
import { Todo } from "src/models";
import Layout from "../../layouts/Layout.astro";
const params = await RequestParameters.from(Astro.request);
const page = Number(params.p) || 1;
// Model.search() returns a search object.
// params.q is a search query object from query parameters.
// Example:
// Path like "/todos?q.id_eq=1&q.title_cont=foo" is parsed as follows:
// { q: { id_eq: 1, title_cont: "foo" } }
const search = Todo.search(params.q);
// searchFormFor() returns form fields for search.
const { NumberField, TextField } = searchFormFor(search);
const { Nav, PageEntriesInfo, records } = paginate(search.result().order("id", "desc"), { page });
---
<Layout title="TODOs">
<main class="container" style="max-width: 700px; min-width: 550px;">
<h1 class="text-center">All TODOs</h1>
<div class="d-flex justify-content-end">
<a href="/todos/new" class="btn btn-link">Add a new todo</a>
</div>
<form method="get" class="row">
<div class="col mb-3">
<NumberField attr="id_eq" placeholder="Search by ID" class="form-control" />
</div>
<div class="col mb-3">
<TextField attr="title_cont" placeholder="Search by Title" class="form-control" />
</div>
<div class="col-md-auto mb-3">
<button type="submit" class="btn btn-dark">Search</button>
<a href="/todos" class="btn btn-secondary">Clear</a>
</div>
</form>
<ul>
{
records.map((todo) => (
<li>
<span>{todo.id}</span>
<span>{todo.title}</span>
{todo.estimate ? <span>|{todo.estimate}</span> : null}
{todo.dueDate ? <span>{todo.dueDate.toISOString().slice(0, 10)}迄</span> : null}
<span>({todo.status})</span>
</li>
))
}
</ul>
<div class="d-flex justify-content-center">
<Nav />
</div>
<div class="d-flex justify-content-center">
<PageEntriesInfo />
</div>
</main>
</Layout>
Session
createCookieSessionStorage
is a function that generates a session storage object. This is a thin wrapper around the function of the same name provided by astro-cookie-session, so please refer to its documentation for basic usage. The difference from the original is that it adds the ability to save Accel Record models as sessions.
import { createCookieSessionStorage } from "accel-web";
import type { Account } from "./models";
export type SessionData = {
account: Account;
};
export const { getSession } = createCookieSessionStorage<SessionData>();
export type Session = ReturnType<typeof getSession>;
---
// src/pages/login.astro
import { getSession } from "../session";
import { Account } from "../models";
const session = getSession(Astro.cookies);
// Read an Account model from the session
if (session.account) {
return Astro.redirect("/");
}
let message = '';
if (Astro.request.method === "POST") {
const params = await RequestParameters.from(Astro.request, Astro.params);
// Account model is assumed to have password authentication implemented.
// https://github.com/koyopro/accella/tree/main/packages/accel-record#password-authentication
const account = Account.findBy({ email: params['email'] });
if (account?.authenticate(params['password'])) {
// Save an Account model in the session
session.account = account;
return Astro.redirect("/");
} else {
message = "Invalid email or password";
}
}
---
<form method="post">
{message && <div>{message}</div>}
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="password">Password</label>
<input type="password" id="password" name="password" required />
</div>
<div>
<button type="submit">Login</button>
</div>
</form>