Source Code for /public/appendix-samples/fruits-and-filters-js-enhanced.php

<?php
/**
 * Returns a sorted array of all of the fruit images in the
 * img/fruit directory.
 */
function get_fruits() {
  // Get a sorted list of fruits like we've done before:
  $jpgs = glob('../img/fruit/*.jpg');
  $webps = glob('../img/fruit/*.webp');
  $files = array_merge( $jpgs, $webps );
  sort( $files );
  // Return the list to whichever code called get_fruits()
  return $files;
}

/**
 * Returns an array of CSS filters that can be applied to an
 * image.
 */
function get_filters() {
  return [
    'filter: grayscale(100%)',
    'filter: blur(2px)',
    'filter: brightness(1.5)',
    'filter: contrast(1.5)',
    'filter: hue-rotate(90deg)',
    'filter: invert(1)',
    'filter: sepia(1)',
    'transform: rotateZ(90deg);',
    'transform: skewX(-15deg);',
  ];
}

/**
 * Returns true if the user provided a fruit_id and filter_id.
 * Returns false otherwise.
 */
function user_requested_a_fruit_and_filter() {
  // If they haven't provided a fruit_id or filter_id, false:
  if( ! isset( $_POST['fruit_id'] ) ) return false;
  if( ! isset( $_POST['filter_id'] ) ) return false;
  // Othewise, true!
  return true;
}

/**
 * Shows the requested fruit with the applied CSS filter.
 */
function show_a_filtered_fruit() {
  // Note that we don't ask the user to send us the filename
  // but the NUMBER of the fruit (and filter), in its list.
  // This is a strong validation step - the user can't inject
  // anything we didn't expect them to!
  $fruit_id = intval( $_POST['fruit_id'] );
  $filter_id = intval( $_POST['filter_id'] );
  $fruit = get_fruits()[ $fruit_id ];
  $filter = get_filters()[ $filter_id ];

  // Output the fruit image with the filter applied:
  ?>
  <img src="<?php echo $fruit; ?>"
     width="200"
    height="200"
     style="border: 1px solid black; <?php echo $filter; ?>;">
  <?php
}

/**
 * This new block of code handles the possibility that
 * JavaScript was used to request a filtered fruit, rather than
 * a plain HTML form submission. If so, we return JSON to the
 * JavaScript so it can update the page with the new filtered
 * fruit.
 */
if( isset( $_GET['js'] ) && ( $_GET['js'] === 'true' ) && user_requested_a_fruit_and_filter() ) {
  // If JavaScript was used to request a filtered fruit, we return JSON to the JavaScript so it can update the page with the new filtered fruit:
  echo json_encode([
    'fruit' => get_fruits()[ intval( $_POST['fruit_id'] ) ],
    'filter' => get_filters()[ intval( $_POST['filter_id'] ) ]
  ]);
  die(); // don't send the page's HTML; it'll break the JSON!
}
?>

<h1>Fruity Filters</h1>

<!--
  This block is always added ot the page, but it's only displayed if a fruit and filter was requested
  at page load (e.g. if JS was disabled). But because it's always added, it can be MADE visible by
  JavaScript in the event of a form submission using a fetch() request!
-->
<div id="results" style="display: <?php echo( user_requested_a_fruit_and_filter() ) ? 'block' : 'none'; ?>;">
  <h2>
    Here's your filtered fruit:
  </h2>
  <?php show_a_filtered_fruit(); ?>
</div>

<h2>
  <?php if( user_requested_a_fruit_and_filter() ) { ?>
    Filter another fruit!
  <?php } else { ?>
    Filter a fruit!
  <?php } ?>
</h2>
<form method="post" action="fruits-and-filters-js-enhanced.php">
  <p>
    <label for="fruit">Fruit:</label>
    <select id="fruit" name="fruit_id">
      <?php foreach( get_fruits() as $id => $fruit ) { ?>
        <option value="<?php echo $id; ?>">
          <?php echo basename( $fruit ); ?>
        </option>
      <?php } ?>
    </select>
  </p>

  <p>
    <label for="filter">Filter:</label>
    <select id="filter" name="filter_id">
      <?php foreach( get_filters() as $id => $filter ) { ?>
        <option value="<?php echo $id; ?>">
          <?php echo $filter; ?>
        </option>
      <?php } ?>
    </select>
  </p>

  <button type="submit">Submit</button>
</form>

<script>
  /* Here's our new JavaScript-enhancement to the form. */
  const results = document.getElementById('results');
  const resultsImage = results.querySelector('img');
  const form = document.querySelector('form');
  const submitButton = form.querySelector('button[type="submit"]');

  form.addEventListener('submit', function(event) {
    // When we submit the form, JS can prevent the regular HTML submission process:
    event.preventDefault();

    // Then it can begin its own. Let's disable the submit button to prevent multiple submissions and make it clear to the user that something is happening:
    submitButton.disabled = true;
    submitButton.innerHTML = 'Please wait...';

    // Now we'll submit the form using a fetch() request and expect some JSON back:
    const url = this.action + '?js=true'; // let's add a GET var - js=true - to say this is a JS request (so PHP knows to send back JSON)
    fetch(url, {
      method: 'POST',
      body: new FormData(this)
    })
    .then(response => response.json())
    .then(data => {
      // The data we get back should look something like this:
      // {
      //   "src": "../img/fruit/apple.jpg",
      //   "filter": "filter: grayscale(100%);"
      // }
      // We can update our image with the new src and filter:
      resultsImage.src = data.fruit;
      resultsImage.style.cssText = 'transition: box-shadow 0.2s; border: 1px solid black;' + data.filter;

      // Let's apply a quick effect to the image so that the user knows it's changed:
      resultsImage.style.boxShadow = '0 0 12px yellow';
      setTimeout(()=>resultsImage.style.boxShadow = 'none', 250);
      
      // And ensure the results block is visible:
      results.style.display = 'block';
    })
    .catch(error => {
      // If there's an error, let's show it to the user:
      alert('Something went wrong: ' + error);
    })
    .finally(() => {
      // Whether it worked or not, let's re-enable the submit button so they can have another go:
      submitButton.disabled = false;
      submitButton.innerHTML = 'Submit';
    });
  });
</script>

<p>
  <a href="/source-viewer.php?file=public/appendix-samples/fruits-and-filters-js-enhanced.php">View source code for this page</a>
</p>