DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Enhanced debugging for Stimulus

This article was originally published on Rails Designer


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:

Image description

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())
Enter fullscreen mode Exit fullscreen mode

Then you can enable debugging per controller like so:

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

Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Here, too, I'd like to highlight the readability: “debug callback for lifecycle”. Nice, right?

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
}

Enter fullscreen mode Exit fullscreen mode

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: []
  };
  // …
}
Enter fullscreen mode Exit fullscreen mode

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));

    // …
  };
}
Enter fullscreen mode Exit fullscreen mode

And that's all there is to it. I hope this short tour will help if you are interested in exploring this feature more. 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.

Top comments (1)

Collapse