PAD + htmx 🛒 0

Inline editing

A row turns into a form and back. Each answer is one row of the same loop - the PHP narrows the rows to the one asked for, padValidate checks the post, the session keeps the edits.

padValidatepadSession&padFragment=row hx-gethx-posthx-includehx-target="closest tr"
Live
NameE-mailCity
Ada Fernández ada@example.com Amsterdam
Bram Visser bram@example.com Utrecht
Chen Wei chen@example.com Rotterdam
Dalia Haddad dalia@example.com Eindhoven
Emeka Obi emeka@example.com Groningen

Your edits stay in your session.

Requests htmx made
    Nothing yet - use the example above, and every request it makes shows here with the part PAD answered.

    How it is built

    PAD, on the server htmx, in the browser

    <table class="table">
      <thead><tr><th>Name</th><th>E-mail</th><th>City</th><th></th></tr></thead>
      <tbody hx-target="closest tr" hx-swap="outerHTML">
      {rows}
      {fragment 'row'}
        {if $editing}
        <tr class="is-editing">
          <td><input class="input{if $errName} is-invalid{/if}" name="name" value="{$name}" aria-label="Name">{if $errName}<span class="error">{$errName}</span>{/if}</td>
          <td><input class="input{if $errEmail} is-invalid{/if}" name="email" value="{$email}" aria-label="E-mail">{if $errEmail}<span class="error">{$errEmail}</span>{/if}</td>
          <td><input class="input{if $errCity} is-invalid{/if}" name="city" value="{$city}" aria-label="City">{if $errCity}<span class="error">{$errCity}</span>{/if}</td>
          <td class="actions">
            <button class="btn-sm" hx-post="?examples/edit&id={$id}&padFragment=row" hx-include="closest tr">Save</button>
            <button class="btn-sm btn-ghost" hx-get="?examples/edit&id={$id}&padFragment=row">Cancel</button>
          </td>
        </tr>
        {else}
        <tr>
          <td><strong>{$name}</strong></td>
          <td>{$email}</td>
          <td>{$city}</td>
          <td class="actions"><button class="btn-sm btn-ghost" hx-get="?examples/edit&id={$id}&mode=edit&padFragment=row">Edit</button></td>
        </tr>
        {/if}
      {/fragment}
      {/rows}
      </tbody>
    </table>
    
    <p class="small muted" style="margin-top: 14px">
      Your edits stay in your session.
      <button class="btn-sm btn-ghost" hx-post="?examples/edit&reset" hx-select="table" hx-target="previous table" hx-swap="outerHTML">Undo them all</button>
    </p>
    <?php
    
      // The contacts are the session's copy of _data/contacts.json. htmx asks for one row at a
      // time - &id= with &padFragment=row - and this file narrows the loop to that row, so the
      // first {fragment 'row'} to render is the answer: the row to read, the row as a form
      // (mode=edit), or after a post the row as stored - or the form again with padValidate's
      // messages in it. The token of the post comes in the X-CSRF-Token header (_inits.pad).
    
      $contacts = padSession ( 'htmxContacts', htmxData ( 'contacts' ) );
      $id       = (int) padRequest ( 'id', 0 );
      $mode     = padRequest ( 'mode', '' ) === 'edit' ? 'edit' : 'view';
      $errors   = [];
    
      if ( padRequestIs ( 'POST' ) and padRequestHas ( 'reset' ) ) {
        $contacts = htmxData ( 'contacts' );
        padSessionPut ( 'htmxContacts', $contacts );
        $id = 0;
      }
    
      elseif ( padRequestIs ( 'POST' ) and $id ) {
    
        $errors = padValidate ( [ 'name' => 'required|max:40', 'email' => 'required|email', 'city' => 'required|max:30' ] );
    
        foreach ( $contacts as $at => $contact )
          if ( $contact ['id'] == $id and ! $errors ) {
            $contacts [$at] = [ 'id' => $id, 'name' => trim ( padRequest ( 'name' ) ), 'email' => trim ( padRequest ( 'email' ) ), 'city' => trim ( padRequest ( 'city' ) ) ];
            padSessionPut ( 'htmxContacts', $contacts );
          }
    
        $mode = $errors ? 'edit' : 'view';
    
      }
    
      // The rows the template walks: all of them, or the one asked for - a form of it showing
      // the posted values and the messages when the post had errors.
    
      $rows = [];
    
      foreach ( $contacts as $contact ) {
    
        if ( $id and $contact ['id'] != $id )
          continue;
    
        if ( $errors )
          $contact = array_replace ( $contact, [ 'name'  => htmxText ( 'name' ),
                                                 'email' => htmxText ( 'email' ),
                                                 'city'  => htmxText ( 'city' ) ] );
    
        $rows [] = $contact + [ 'editing'  => ( $id and $mode == 'edit' ),
                                'errName'  => $errors ['name']  ?? '',
                                'errEmail' => $errors ['email'] ?? '',
                                'errCity'  => $errors ['city']  ?? '' ];
    
      }
    
      // An id the session does not hold - a stale or made-up request - has no row to answer: a
      // 404, which htmx swaps nothing for, where the empty loop rendered no {fragment 'row'} and
      // the answer was a 500, "there is no fragment named 'row' on this page".
    
      if ( $id and ! $rows )
        padAbort ( 404, 'There is no such contact' );
    
    ?>