Using decorators with Stimulus controllers


#1

Hello

I wanted to try using decorators in a Stimulus controller and I am running into an issue.
I have to add and remove a “spinner” class before and after a fetch function.
Since I have this code at several places I wanted to try using a decorator to make it a little dryer (disclaimer I am just getting started with decorators).

here is my code

import { Controller } from "stimulus";

function toggleBusy(target, name, descriptor) {
  const value = descriptor.value;

  descriptor.value = function() {
    console.log("target :", target);
    target.toggleBusyClass = true;
    value.call(target);
  };

  return descriptor;
}

export default class extends Controller {
  async fetch() {
    this.toggleBusyClass = true;
    await this.fakefetch();
    this.toggleBusyClass = false;
  }

  @toggleBusy
  async fetchWithDecorator() {
    await this.fakefetch();
  }

  set toggleBusyClass(bool) {
    this.element.classList.toggle("spinner", bool);
  }

  fakefetch() {
    console.log("fetch start");
    return new Promise(resolve => {
      return setTimeout(() => {
        console.log("fetch done");
        resolve();
      }, 1000);
    });
  }
}

and the simple html

<div data-controller="decorator">
  <button data-action="click->decorator#fetch">fetch data </button>
  <button data-action="click->decorator#fetchWithDecorator">fetch data with decorator</button>
</div>

The simple fetchmethod works perfectly.

The decorated fecthWithDecoratormethod doesn’t work. I am getting this error:

TypeError: Cannot read property 'scope' of undefined
    at Controller.get [as scope] (controller.js:18)
    at Controller.get [as element] (controller.js:25)
    at Controller.set toggleBusyClass [as toggleBusyClass] (decorator_controller.js:28)
    at Controller.descriptor.value (decorator_controller.js:8)
    at Binding.invokeWithEvent (binding.js:52)
    at Binding.handleEvent (binding.js:29)
    at EventListener.handleEvent (event_listener.js:28)

It seems that I am losing the controller’s context.

For now, I have a working solution without a decorator but just curious if someone has an idea on how to make it work.

PS : Babel is up well configured for decorator

Thanks


#2

here is a repo to reproduce the environment


#3

I’m not super familiar with decorators, but it’s probably best to work with this instead of target:

function toggleBusy(target, name, descriptor) {
  const value = descriptor.value;

  descriptor.value = function() {
    this.toggleBusyClass = true;
    value.call(this);
  };

  return descriptor;
}

:point_up_2:that works. (it should probably toggle the busy class back to false at the end)


#4

Thanks, @javan
Got it to work now with both toggle on and off.
Will now need to dig a bit more into decorators to understand why target is supposed to be the context but is not really exactly the same context as this :sweat_smile: