Slide-y Thin-King

Slide-y Thin-King

Functions, rings, modules, and a slide-y slider for learning the mathematics and beauty of hypertext, texts, language, and languages.

This is the live documentation of how we are creating the libre slidey-things library of slider components. We will be building a basic slider and iteratively adding features and polish to it. These are some basics to cover before we dive into keyframes, animations, pre-loading, caching, responsiveness, and browser-native optimizations.

Initial Conceptions

  • A slider may show at least one slide at once.

  • A slider can have a previous, central and next slide all visible at once.

  • Similarly, a slider can have many previous, one central and many next slides all visible at once (if they are very narrow in terms of width, assuming horizontal slides).

  • Sliding is defined as translating a slide from point A to point B.

  • Sliding and slides are not merely architectural/software constructs, they are also visual and human co-structures.

A Minimal Version

So far, we have established that a slide being shown may mean a translation to the center of view.

When implementing it in a browser natively without a build tool, we have to focus on HTML, CSS and JS first.

Here's how the HTML for such a slider may look like:

      <div class="slider">
        <div class="slide-l slidey">
          <img src="img/public/square/galaxy.png" alt="Andromeda">
        </div>
        <div class="slide-c slidey">
          <img src="img/public/square/brain.png" alt="Pre-frontal Cortex">
        </div>
        <div class="slide-r slidey">
          <img src="img/public/square/big.png" alt="Data Architecture">
        </div>
      </div>

We only have a div around each img element and a parent div classed as .slider. Presumably, we would need some CSS for

  • the left image (.slide-l)

  • the central image (.slide-c)

  • the right image (.slide-r)

And they all might share some common CSS config, which we could put under the .slidey class.

Various users of the sliders would want to use the slider differently and might have different tastes and design preferences.

We will keep our implementations open for extension and closed for modifications. Essentially, this necessitates immutable data structures and thinking of code / developmental work itself as a graphical data structure.

Let's grab a reference to the parent div first:

    const slides = document.querySelectorAll('.slider');

One way of thinking about these slides would be to think as if we are meant to unroll it as a film out of a film roll.

In JS, this could look like this if we think of pulling out each roll via cogs attached to each of the film's edges for pulling into view:

    function showSlide(index) {
        slides.forEach((slide) => {
            const slideWidth = slide.clientWidth;
            slide.style.transform = `translateX(-${index * slideWidth}px)`;
        });
    }

This is for showing a slide that is n index away from the original or zeroth position.

We extract the slideWidth of a given slide by iterating through each item inside the slides element we queried for earlier. Every image moves to the negative X direction i.e. the left of the screen.

If your nth image is moved to the left by n times the slideWidth, it would have hopped that width n number of times towards left.

Why does it make sense to initially do it this way?

If you are n steps further away, you need to move yourself n times to get to the original or zeroth position.

How big are each of those steps? Let's just say you move yourself just one unit to the left.

If we make this 'unit' a constant for each slide, it would not really cover the required distances.

If we are, for example, constraining all the images to the same height and allowing each image to be of any aspect ratio / width, we need to consider each image's journey / distances individually.

When thinking in natural numbers, we think in terms of unit increments and decrements also aside from n number of unit increments or decrements.

If we are able to have an action and an inverse operation for that action, e.g. both a forward and previous button / action associated with our slider then we would have something close to what is called a 'ring' in mathematics.

Let's try to have a simple pair of nextSlide and prevSlide functions that act as the inverses of each other. By this, we generally mean that doing one action and then the next will lead you back to your original position or state in general.

    let currentSlide = 0;

    function nextSlide() {
        currentSlide =(currentSlide + 1) % slides.length;
        showSlide(currentSlide);
    }

    function prevSlide() {
        currentSlide = (currentSlide - 1 + slides.length) % slides.length;
        showSlide(currentSlide);
    }

Apart from using let in JS, it is also a bit flaky because it has two places that try to modify the same value: the value of the currentSlide index. Such mutation oriented code needs to be dealt with with care because of the complexities that arise in reading and writing the same value from different optical vantage points. However, since we will try to keep it as simple as possible without it being useless, we will keep the index as a closure state within our function.

Holding the state of the current slide in memory requires us to think of how we want to place and manage this in memory. In JS, an idiomatic way to achieve this is by using closure inside a functional context. In mathematics, closure is a property that usually implies that operations do not change the type of outcome. Similarly, in programming, closure is a similar property that is bounded within a lexical or functional scope, like in the following example:

    function sliderContext(cssClass) {
      const slides = document.querySelectorAll(cssClass);
      let currentSlide = 0;

      function showSlide(index) {
        slides.forEach((slide) => {
          const slideWidth = slide.clientWidth;
          slide.style.transform = `translateX(-${index * slideWidth}px)`;
        });
      }

      function nextSlide() {
        currentSlide =(currentSlide + 1) % slides.length;
        showSlide(currentSlide);
      }

      function prevSlide() {
        currentSlide = (currentSlide - 1 + slides.length) % slides.length;
        showSlide(currentSlide);
      }

      return {
        nextSlide,
        prevSlide
      };
    }

Here we have created a function that takes an input and returns an object with a couple of functions as an output.

Depending on whether you have a functional or imperative upbringing, you might call such a thing a higher order function or a 'module pattern'.

In theindex.html file, with the above DOM elements for the sliders, one could add such functions as actions to dispatch when a user event like click happens.

Here's one simple way of initializing this:

        <script type="text/javascript" src="js/slidey/slider.js"></script>
        <script>
            const slideContextA = sliderContext(".slidey");
        </script>

And somewhere in the DOM, where you have your buttons, you could do this:

    <a class="prev" onclick="slideContextA.prevSlide()">&lt;</a>
    <a class="next" onclick="slideContextA.nextSlide()">&gt;</a>

We obtained these two functions to use in our HTML because the module or sliderContext function has returned that to us for use in the call site, which happens to be the index.html page itself in this case.

However, it is not always preferable to attach event listeners to the HTML itself like this, as this does not scale well with more complex use cases and large number of DOM nodes being involved.

HTML is meant to be a language for semantic markup, and usually is best used for that itself. We can attach event listeners to HTML DOM elements within the JS itself. The trade-off in that case is that the JS needs to know at least some identifiers to find the reference or know how the HTML is structurally as a DOM tree.

Either way, there is not going to be a true separation of concerns between semantic, visual, and causal markers in a language meant for a structural description of a document.

With user interface components like a slider, it is not always the case that a user interaction triggers an event. A slider could, for instance, automatically play in a loop forever or for a finite time span.

Here's how one could add options to auto the slides in a loop:

    const defaultOptions = {
      auto: false,
      infinite: false,
      time: 2000
    }

    function sliderContext(cssClass = ".slidey", options = defaultOptions) {
      // same code as before
      // ...
      // logic for looping:
      if (allOptions.auto) {
            const intervalRef = setInterval(nextSlide, allOptions.time);
            if (!allOptions.infinite) {
                setTimeout(clearInterval(intervalRef), allOptions.time * 2);
             }
      }

      return {
        nextSlide,
        prevSlide
      };

  }

Here we have decreed that the nextSlide function be called every 2000ms or any other finite duration. We have also said that if the slide is not supposed to be infinite, it should stop looping after a certain predefined amount of time.

A Minimal Requirement Analysis

We might need to have a lot more images in a slideshow. This also does not seem like it will be smooth without the appropriate CSS for the transitions and/or animations.

In a more complex and publicly usable slider, what else would be required of the slider HTML components and the JS/CSS backing them? Let's explore that in the next Slide-y Thinking session.