Enhanced debugging for Stimulus

Published at Leave your comment

Stimulus has a basic debug mode that shows if Stimulus is running and lists which controller are initialized and connected. But when you work with Stimulus controllers often, there’s a need to see a bit more detail, without manually expecting the DOM versus your controller’s logic.

So I explored a new idea and add it as a new FX to Stimulus FX: enableDebug. This is what it gives you:

It is easy to set up:

// app/javascript/controllers/application.js

+import { enableDebug } from "stimulus-fx"

-const application = Application.start()

+const application = enableDebug(Application.start())

Then you can enable debugging per controller like so:

export default class extends Controller {
+  static debug = true
+
// …
}

I added this mostly as an experimental feature to see if it was possible and with releasing it, hopefully inspire someone from you, to expand this even more. I have some ideas myself, but don’t want to influence you.

How does it work

The code for this is surprisingly simple. It creates an enableDebug function that takes application as an argument (as seen from the usage: enableDebug(Application.start())).

import { debuggable } from "./debuggable";
import { initialize } from "./enableDebug/initialize";
import { values } from "./enableDebug/values";
import { targets } from "./enableDebug/targets";

export function enableDebug(application) {
  const debugFeatures = [
    initialize,
    targets,
    values
  ];

  return debuggable(application, { with: debugFeatures });
}

See how readable it is? “Return debuggable application with (debug) features”.

Then in debuggable it loops over the given features: features.forEach(feature => feature(identifier, callbacks)); invoking the feature with the identifier and the callbacks (the lifecycle methods). The initialize functions is simple:

export function initialize(identifier, callbacks) {
  const debugCallback = ({ for: lifecycle }) => ({
    log(context) {
      console.group(`#${lifecycle}`);

      console.log("details:", {
        application: context.application,
        identifier,
        controller: context,
        element: context.element
      });

      console.groupEnd();
    }
  });

  ["initialize", "connect", "disconnect"].forEach(lifecycle => {
    callbacks[lifecycle].push(function() {
      debugCallback({ for: lifecycle }).log(this);
    });
  });
}

Here, too, I’d like to highlight the readability: “debug callback for lifecycle”. Nice, right? This syntax exemplifies the approach I advocate throughout the book JavaScript for Rails Developers.

The values and targets logic has a bit more to it, let’s look at src/enableDebug/values.js:

export function values(identifier, callbacks) {
  callbacks.connect.push(function() {
    logValues({ on: this.element, for: identifier });
  });
}

function logValues({ on: element, for: identifier }) {
  const values = allValues(element, identifier);

  if (Object.keys(values).length === 0) return;

  console.group("Values");
  console.table(values);
  console.groupEnd();
}

function allValues(element, identifier) {
  const prefix = `${identifier}-`;
  const dataPrefix = "data-";
  const valueSuffix = "-value";

  // logic to get values from the element/identifier
}

It pushes the result from logValues, which is just the grouped console.table result onto the connect array that is defined in src/debuggable.js (same is happening for the targets):

export function debuggable(application, { with: features }) {
  // …
  const callbacks = {
    initialize: [],
    connect: [],
    disconnect: []
  };
  // …
}

Still in src/debuggable.js these functions are then invoked within the controller’s connect (and other lifecycle methods):

export function debuggable(application, { with: features }) {
  // …

  controller.prototype.connect = function() {
    callbacks.connect.forEach(hook => hook.call(this));

    // …
  };
}

And that’s all there is to it. I hope this short tour will help if you are interested in extending this feature yourself. Maybe with enough weight and interest behind it, it can even be ported into Stimulus it self.

Let me know if you have any questions.

💬 Over to you…

What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!

Comments are powered by Chirp Form

More articles like this on modern Rails, UI & frontend engineering?
JavaScript for Rails Developers
Make JS your 2nd favorite language