One set of rules
The rules live in the page's PHP: padValidate checks the post with them, padValidateClient hands them to Alpine, and {validator} checks them in the browser first - the same messages on both sides.
padValidateClientpadValidate{validator}
@blurpadValidate.check
Live
Booked in your session
Nobody yet.
Requests to PAD
Nothing yet - use the example above, and every request a component makes to PAD shows here.
How it is built
PAD, on the server Alpine, in the browser
<div class="cols-2" x-data="signup({^start})">
<form novalidate @submit.prevent="send()" class="stack-fields">
<label class="label">Name <input class="input" name="name" x-model="form.name" @blur="leave('name')" :class="errors.name && 'is-invalid'"></label>
<span class="error" x-text="errors.name" x-show="errors.name" x-cloak></span>
<label class="label">E-mail <input class="input" name="email" type="email" x-model="form.email" @blur="leave('email')" :class="errors.email && 'is-invalid'"></label>
<span class="error" x-text="errors.email" x-show="errors.email" x-cloak></span>
<label class="label">Team
<select class="input" name="team" x-model="form.team" @change="leave('team')" :class="errors.team && 'is-invalid'">
<option value="">Choose ...</option>
<option value="web">Web</option>
<option value="platform">Platform</option>
<option value="data">Data</option>
<option value="sales">Sales - not one of the rules</option>
</select>
</label>
<span class="error" x-text="errors.team" x-show="errors.team" x-cloak></span>
<label class="label">Seats <input class="input" name="seats" x-model="form.seats" @blur="leave('seats')" :class="errors.seats && 'is-invalid'"></label>
<span class="error" x-text="errors.seats" x-show="errors.seats" x-cloak></span>
<label class="small check-line"><input type="checkbox" x-model="browser"> Check in the browser first</label>
<button :disabled="busy">Book</button>
<p class="small" x-text="note" x-show="note" x-cloak></p>
</form>
<div>
<div class="panel-title">Booked in your session</div>
<template x-for="(one, at) in people" :key="at">
<div class="card booked"><strong x-text="one.name"></strong> <span class="badge" x-text="one.team"></span> <span class="muted small" x-text="one.seats + ' seats'"></span></div>
</template>
<p class="muted small" x-show="people.length === 0">Nobody yet.</p>
</div>
</div>
{validator}
<script src="{$padRoot}alpine/examples/form.js"></script><?php
// One list of rules for both sides. padValidate checks a post against it; for the browser
// padValidateClient exports the same list, every rule with the message padValidate would
// give, and {validator} in the template brings the checker that reads it. A good post is
// kept in the session; the answer is JSON either way - the messages, or the new list.
$rules = [ 'name' => 'required|max:40',
'email' => 'required|email',
'team' => 'required|in:web,platform,data',
'seats' => 'required|integer|min:1|max:8' ];
$people = padSession ( 'alpinePeople', [] );
$errors = [];
$saved = FALSE;
if ( padRequestIs ( 'POST' ) ) {
$errors = padValidate ( $rules );
if ( ! $errors ) {
array_unshift ( $people, [ 'name' => trim ( padRequest ( 'name' ) ), 'team' => padRequest ( 'team' ), 'seats' => (int) padRequest ( 'seats' ) ] );
$saved = padSessionPut ( 'alpinePeople', array_slice ( $people, 0, 8 ) );
$people = array_slice ( $people, 0, 8 );
}
}
$start = [ 'rules' => padValidateClient ( $rules ), 'people' => $people ];
$padExpose = [ 'saved', 'errors', 'people' ];
?>// The form: the rules are the page's PHP's, exported by padValidateClient into the argument
// - signup({^start}) - and padValidate, the checker {validator} brought, judges them in the
// browser with the server's own messages. Untick "check in the browser" and the post goes as
// it is: the server's answer holds the same messages.
document.addEventListener('alpine:init', () => {
Alpine.data('signup', (start) => ({
rules: start.rules,
people: start.people,
form: { name: '', email: '', team: '', seats: '' },
errors: {},
browser: true,
busy: false,
note: '',
leave(name) {
if (this.browser && this.form[name] !== '')
this.errors[name] = padValidate.field(this.rules[name], this.form[name], this.form);
},
async send() {
this.note = '';
if (this.browser) {
this.errors = padValidate.check(this.rules, this.form);
if (Object.keys(this.errors).length) {
this.note = 'Stopped in the browser - the same rules the server checks.';
return;
}
}
this.busy = true;
try {
const answer = await pad.post('examples/form', this.form);
this.errors = Array.isArray(answer.errors) ? {} : answer.errors;
this.people = answer.people;
if (answer.saved) {
this.form = { name: '', email: '', team: '', seats: '' };
this.note = 'Booked - PAD kept it in your session.';
} else {
this.note = 'The server sent it back - these are its messages.';
}
} catch (error) {
this.note = error.message;
} finally {
this.busy = false;
}
}
}));
});