PAD + Alpine

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;
          }
        }
      }));
    
    });