PAD + Alpine

Components in a file

Tabs, an accordion and a dialog registered with Alpine.data in www/alpine/ - the template calls them by name and hands them PAD's data as the argument: x-data="accordion({^faq})".

{^field}{faq} Alpine.datax-showx-transition
Live
Accordion - accordion({^faq})
Alpine writes objects with braces, and PAD reads every brace of a template as the start of a tag. Let PAD write the JSON instead.
In www/alpine/examples/components.js - a file the web server sends as it is, so its braces never meet the template parser.
The answers are in the HTML PAD wrote; x-cloak and x-show only fold them away once Alpine runs.
Tabs - tabs({^tabs})

PAD renders the page, with every value escaped for where it stands.

Dialog - dialog()
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">
    
      <section>
        <div class="panel-title">Accordion - accordion({open}^faq{close})</div>
        <div class="accordion" x-data="accordion({^faq})">
          {faq}
          <div class="accordion-item" :class="isOpen({current@faq}) && 'is-open'">
            <button class="accordion-head" @click="toggle({current@faq})" :aria-expanded="isOpen({current@faq})">{$q}<span aria-hidden="true">+</span></button>
            <div class="accordion-body" x-show="isOpen({current@faq})" x-transition>{$a}</div>
          </div>
          {/faq}
        </div>
      </section>
    
      <section>
        <div class="panel-title">Tabs - tabs({open}^tabs{close})</div>
        <div class="tabs" x-data="tabs({^tabs})">
          <div class="pills" role="tablist">
            <template x-for="tab in list" :key="tab.key">
              <button class="pill" role="tab" :class="tab.key === active && 'is-on'" :aria-selected="tab.key === active" @click="active = tab.key" x-text="tab.label"></button>
            </template>
          </div>
          <p class="panel tab-text" x-text="current().text">{$tabs.0.text}</p>
        </div>
    
        <div class="panel-title" style="margin-top: 26px">Dialog - dialog()</div>
        <div x-data="dialog()">
          <button @click="show()">Open the dialog</button>
          <div class="backdrop" x-show="open" x-transition.opacity x-cloak @click.self="hide()" @keydown.escape.window="hide()">
            <div class="dialog" role="dialog" aria-modal="true" aria-label="A dialog" x-show="open" x-transition>
              <h3>Rendered by PAD, shown by Alpine</h3>
              <p class="muted">This box was in the page from the start; Alpine only decides when it shows.</p>
              <button class="btn-ghost" @click="hide()" x-ref="close">Close</button>
            </div>
          </div>
        </div>
      </section>
    
    </div>
    
    <script src="{$padRoot}alpine/examples/components.js"></script>
    <?php
    
      // The data of the components: plain arrays here, a database or a _data file anywhere else.
      // The template hands each to its component as the argument - x-data="accordion({^faq})" -
      // and renders the same rows as HTML inside it, for a visitor without JavaScript.
    
      $faq = [ [ 'q' => 'Why not write the state in the template?',
                 'a' => 'Alpine writes objects with braces, and PAD reads every brace of a template as the start of a tag. Let PAD write the JSON instead.' ],
               [ 'q' => 'Where do the components live?',
                 'a' => 'In www/alpine/examples/components.js - a file the web server sends as it is, so its braces never meet the template parser.' ],
               [ 'q' => 'Does it work without JavaScript?',
                 'a' => 'The answers are in the HTML PAD wrote; x-cloak and x-show only fold them away once Alpine runs.' ] ];
    
      $tabs = [ [ 'key' => 'server', 'label' => 'Server', 'text' => 'PAD renders the page, with every value escaped for where it stands.' ],
                [ 'key' => 'wire',   'label' => 'Wire',   'text' => 'The state crosses as JSON in an attribute - no second request.' ],
                [ 'key' => 'client', 'label' => 'Client', 'text' => 'Alpine reads it and keeps the component live in the browser.' ] ];
    
    ?>
    // Three components, registered before Alpine starts. Each is a function the template calls
    // in x-data, with PAD's data as the argument: accordion({^faq}), tabs({^tabs}), dialog().
    // This file is sent by the web server as it is - its braces never meet the PAD parser.
    
    document.addEventListener('alpine:init', () => {
    
      // Which question is open - the index PAD's {current@faq} wrote, starting at 1.
      Alpine.data('accordion', (items) => ({
        items,
        open: 1,
        isOpen(at) { return this.open === at; },
        toggle(at) { this.open = this.open === at ? 0 : at; }
      }));
    
      Alpine.data('tabs', (list) => ({
        list,
        active: list[0].key,
        current() { return this.list.find(tab => tab.key === this.active); }
      }));
    
      Alpine.data('dialog', () => ({
        open: false,
        show() {
          this.open = true;
          this.$nextTick(() => this.$refs.close.focus());
        },
        hide() { this.open = false; }
      }));
    
    });