Batch Updates

Editing one row at a time is straightforward. Updating multiple rows is a bit more complicated.

Easy Method

Sends one AJAX request and updates all rows at once.

Create a button

$this->buttonsRight->add([
  'name' => 'markClaimed',
  'icon' => 'check',
  'attrs' => ['show-when-selected', 'show-when-filtered'],
  'tooltip' => 'Mark selected/filtered items as claimed',
]);

Add JavaScript to react on button clicks:

// claimed button
RockGrid.on("button:markClaimed", (button) => {
  UIkit.modal.dialog(`
    <div class='uk-modal-body'>
      <h3 class='uk-text-bold'>Mark items claimed</h3>
    </div>
  `);
});

// during development it can be helpful to trigger the modal on every pageload
setTimeout(() => {
  document.querySelector(".rockgridbutton[data-name=markClaimed]").click();
}, 10);

Next we build the UI for entering the invoice number, for example:

RockGrid - Batch Updates
RockGrid.on("button:markClaimed", (button) => {
  const filteredCnt = table.getData("active").length;
  const selectedCnt = table.getSelectedRows().length;
  UIkit.modal.dialog(`
    <div class="uk-modal-body">
      <h3 class='uk-text-bold'>Mark items claimed</h3>
      <p>
        <label class='uk-margin-small-right'>
          <input class="uk-radio" type="radio" name="type" value="filtered" checked>
          All filtered rows (${filteredCnt})
        </label>
        <label>
          <input class="uk-radio" type="radio" name="type" value="selected" ${
            selectedCnt === 0 ? "disabled" : ""
          }>
          Selected rows (${selectedCnt})
        </label>
      </p>
      <p>
        <label><input class="uk-input" type="text" placeholder="Claimed with (eg INV123) ..." id="claimedwith"></label>
      </p>
    </div>
    <div class="uk-modal-footer uk-text-right">
      <button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
      <button disabled class="uk-button uk-button-primary" type="button" id="confirm-claim">
        <i class='fa fa-check' style='margin-right:5px'></i>
        <i class='fa fa-spinner fa-spin' style='margin-right:5px' hidden></i>
        Mark as Claimed
      </button>
    </div>
  `);
});

Now let's improve the UI of our form and disable the submit button as long as we don't have input:

// disable #confirm-claim button as long as no input
document.addEventListener("input", function (event) {
  const el = event.target;
  if (!el) return;
  if (el.id !== "claimedwith") return;
  document.getElementById("confirm-claim").disabled = el.value === "";
});

Now we need to add the code to handle the confirm button click. For testing we start simple:

// handle click on confirm button
document.addEventListener("click", function (event) {
  if (!event.target) return;
  if (event.target.id !== "confirm-claim") return;
  const input = document.getElementById("claimedwith").value;
  alert(input);

  // show loading spinner and disable button
  // get ids of selected/filtered rows
  // send ajax request to mark selected rows as claimed
  // reload grid when done
  // close modal
});

And here is the full implementation:

document.addEventListener("click", function (event) {
  if (!event.target) return;
  if (event.target.id !== "confirm-claim") return;
  const dialog = event.target.closest(".uk-modal-dialog");
  const input = document.getElementById("claimedwith").value;

  // show loading spinner and disable button
  dialog.querySelector("#confirm-claim").disabled = true;
  dialog.querySelector(".fa-check").hidden = true;
  dialog.querySelector(".fa-spinner").hidden = false;

  // get ids of selected/filtered rows
  let rowType = "filtered";
  const radio = dialog.querySelector('input[name="type"]:checked');
  if (radio) rowType = radio.value;
  let ids = [];
  if (rowType === "selected") {
    ids = table.getSelectedRows().map((row) => row.getData().id);
  } else {
    ids = table.getData("active").map((row) => row.id);
  }

  // send ajax request to mark selected rows as claimed
  const body = new URLSearchParams({
    ids: ids.join(","),
    claimedwith: input,
  });
  fetch("/path-to-your-endpoint", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: body.toString(),
  })
    .then((response) => response.json())
    .then((data) => {
      if (!data.success) throw new Error(data.message);

      // close modal after 5s
      setTimeout(() => {
        UIkit.modal(event.target.closest(".uk-modal")).hide();
      }, 5000);

      // reload grid when done
      grid.reload();

      // close modal
      UIkit.modal(event.target.closest(".uk-modal")).hide();
    })
    .catch((error) => {
      UIkit.notification({
        message: "An error occurred while processing your request",
        status: "danger",
        timeout: 5000,
      });
      // close modal after 5s
      setTimeout(() => {
        UIkit.modal(event.target.closest(".uk-modal")).hide();
      }, 5000);
    });
});

The button triggers an AJAX request to /path-to-your-endpoint which we have to implement on the backend. For that we first need to make our grid autoload:

class Efforts extends Grid
{
  const autoload = true;

  public function init(): void
  {
    wire()->addHookAfter('/path-to-your-endpoint', $this, 'markAsClaimed');
  }

  // ...
}

Then we can add the markAsClaimed method:

protected function markAsClaimed(HookEvent $event)
{
  // CAUTION: NEVER TRUST ANY USER INPUT!!
  $ids = wire()->input->post('ids', 'array');
  $with = wire()->input->post('claimedwith', 'string');
  foreach ($ids as $id) {
    $p = wire()->pages->get($id);
    // add checks as required, eg for proper page template, editable etc.
    if ($p->template != 'effort') continue;
    if (!$p->editable()) continue;
    $p->setAndSave('claimedwith', $with);
  }
  return ['success' => true];
}

Advanced Method

Uses SSE to stream updates to the browser. TBD.