Updating PHP variables without reloading?

I have a bunch of radio buttons on a page and, whenever their values change, I need to update a PHP variable consisting of a collection of Kirby Pages, based on the ID and value of the radio buttons. This variable then should be used to alter the contents of a list on the same page without manually reloading the page.

How do I achieve this? Are there Kirby-specific techniques, or should I look and ask elsewhere to learn this? I read a lot about AJAX, POST methods, Kirby routes, and the like, but it’s still quite new for me, and I’m not really getting anywhere.

Here is a bit of the relevant code I’m working with:

<?php $cards = page('cards')->children() ?>
<?php $deck = new Pages() ?> <!-- this is the variable that should be filled/altered accordingly -->

<?php foreach($cards as $card): ?>
  <?php for($i = 0; $i <= 2; $i++): ?>
    <input class="mdc-radio__native-control ds-card__amount"
           type="radio" value="<?= $i ?>"
           id="<?= $card->uid() ?>-<?= $i ?>"
           name="<?= $card->uid() ?>__amount"
           <?php e($i === 0, 'checked') ?>>
  <?php endfor ?>
<?php endforeach ?>

<ul>
  <?php foreach($deck as $slot): ?>
    <li><?= $slot->title() ?></li>
  <?php endforeach ?>
</ul>

And the JS:

const cardAmounts = document.getElementsByClassName('ds-card__amount');
for (var i = 0; i < cardAmounts.length; i++) {
  cardAmounts[i].addEventListener('change', cardAmountChange);
}
function cardAmountChange(event) {
  const copies = {
    card: event.target.id.slice(0, -2),
    amount: event.target.value
  }
  fetch('ajax', { // thought of creating a route to /ajax where I could handle the alteration to the PHP variable $deck
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(copies)
  })
  .then(response => response.text()) // not sure which response type I need here
  .then(data => {
    console.log('Success:', data);
  })
  .catch((error) => {
    console.error('Error:', error);
  });
}

Of course there is nothing yet that deals with the posted JSON, but that’s where the problems start as I don’t seem to be able to even receive that data. Any help or links to where I should learn the necessary things would be appreciated!

It would be helpful to know what you are trying to do here in more detail.

And yes, you would need either a route (or maybe a content representation) to which to send your AJAX request.

The purpose of radio buttons is to select one option, not multiple, so I don’t quite see why we end up with a Pages collection? Otherwise, you would have to use checkboxes instead of radio buttons.

Ah, I can see why that sounds confusing, sorry.
It’s going to be a deckbuilding website for a card game. Every card in the game has the option of being included 0, 1, or 2 times in a deck. So on the page, where all cards are being shown, there is a set of three radio buttons (with the same name attribute) per card with the values of 0, 1, 2.
I want to update the list of all cards in a deck whenever I choose the amount of times a specific card should be included in that deck, and this list should be shown on the same page and be updated at all times.

Ah, ok, I somehow missed that because there were no labels nor titles.

Oops, I missed that (or somehow didn’t deem it necessary) when trimming the snippet off of the Material Components for the web code that I’m working with.

This is the complete snippet for a card that gets called in a foreach loop for every card in the game, but most of it is specific to the Material Components:

<div class="mdc-layout-grid__cell">
  <div class="mdc-card ds-card">
    <div class="mdc-card__primary-action" tabindex="0">
      <div class="mdc-card__media"
           style="background-image: url(
             <?= $card->image('card.jpg')->url() ?>
           )">
      </div>
    </div>
    <div class="mdc-card__actions">
      <?php for($i = 0; $i <= 2; $i++): ?>
        <div class="mdc-form-field">
          <div class="mdc-touch-target-wrapper">
            <div class="mdc-radio mdc-radio--touch">
              <input class="mdc-radio__native-control ds-card__amount"
                     type="radio" value="<?= $i ?>"
                     id="<?= $card->uid() ?>-<?= $i ?>"
                     name="<?= $card->uid() ?>__amount"
                     <?php e($i === 0, 'checked') ?>>
              <div class="mdc-radio__background">
                <div class="mdc-radio__outer-circle"></div>
                <div class="mdc-radio__inner-circle"></div>
              </div>
              <div class="mdc-radio__ripple"></div>
            </div>
          </div>
          <label for="<?= $card->uid() ?>-<?= $i ?>"><?= $i ?></label>
        </div>
      <?php endfor ?>
    </div>
  </div>
</div>

Your route would send back json, but you cannot use a pages collection for the deck, because it looks as if you need multiple instances of the same page, which is not possible in a pages collection.

I don’t think it has to be a Pages collection.
But I’m already stuck on getting to the $_POST data. I assume I would have the following route:

[
  'pattern' => 'ajax',
  'action' => function() {
    return json_encode($_POST); // for testing
  },
  'method' => 'POST'
]

return json_encode($_POST) outputs []; $_POST seems to be an empty array. Calling console.log(slot) in JS works as expected and logs the correct data, which should be the same as $_POST to my understanding.

You have to make your route listen to a POST request: https://getkirby.com/docs/guide/routing#methods

By default, a route only listens to GET requests.

Isn’t that what 'method' => 'POST' does? I have that already included above.

I’m so blind…, sorry, of course.

No problem, I very much suspect myself of blindness here on why it doesn’t work. It seems like the code should be pretty straightforward.

Returning an array from the route works without issues:

[
        'pattern' => 'ajax',
        'action' => function() {
          return ['key' => 'value'];
        },
        'method' => 'POST'
      ]

All the more an indication that nothing gets POSTed here in the first place. I don’t understand why, though, since the console.log has the correct output.

function cardAmountChange(event) {
  const slot = {
    card: event.target.id.slice(0, -2),
    amount: event.target.value
  }
  fetch('ajax', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(slot)
  })
  .then(response => response.json())
  .then(data => {
    console.log('Success:', data);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

  console.log(slot);
}

This works

   [
        'pattern' => 'ajax',
        'action' => function() {
          $card = get('card');
          $amount = get('amount');

          return ['card' => $card, 'amount' => $amount];
        },
        'method' => 'POST'
      ]

Awesome, so it does, thanks! I’ll play around with it some more and see if I have further questions.

I do have further questions. I now have the following route:

[
  'pattern' => 'deck',
  'method' => 'POST',
  'action' => function() {
    $data = get();
    $deck = new Pages();
    foreach($data as $card => $amount) {
      $deck->add('cards/' . $card);
    }

    return $deck;
  }
]

That gives me an InvalidArgumentException error with the message Unexpected input. Why is that? Can I not do these things in a route? I made sure that $data is correct and that 'cards/' . $card exists.

You can’t return a collection from the route. You have to return $deck->toArray().

Of course, thanks! For some reason I thought the problem was with adding the page, not with the return. I don’t even want to return the whole collection, that was just placeholder …

I am trying to do pretty much the same thing (update PHP variables without reloading) and I’ve tried my best to follow this thread but it’s so specific to the OPs example that I can’t make much sense of it.

Simply put, I would like to update the data passed to my form from my controller when one of the input fields is changed. I’ve cheaply mocked this up with some JS that submits the form when it detects an input change, however I’d like to update the form without a page reload.

I’ve dabbled with AJAX before (Load More with AJAX) however this almost seems simpler if I can do it via a route and a JS file.

My controller is getting the values from URL parameters:

  //get Used Filters
  $query        = get('q');
  $topPlace     = get('topPlace');
  $type         = get('type');
  $ucategories  = get('categories');
  $utags        = get('tags');

  //Apply Used Filters
  $aplaces = $page->children()->listed()->flip()
    ->when($query, function ($query) {
        return $this->search($query, 'title|category|tags|locations');
    })
    ->when($topPlace, function ($topPlace) {
      return $this->filterBy('topPlace', true);
    })
    ->when($type, function ($type) {
        if($type != 'all'){
          $return = $this->filterBy('type', $type);
        } else {
          $return = $this;
        }
        return $return;
    })
    ->when($ucategories, function ($ucategories) {
      return $this->filterBy('category', 'in', $ucategories, ',');
    })
    ->when($utags, function ($utags) {
        return $this->filterBy('tags', 'in', $utags, ',');
    });

My template is disabling certain fields based on what has been selected, which is why it would be nice for this to happen on input change. What’s the best way to approach this?

I guess another question is how do I achieve this when using URL parameters for the search?