Stimulus: Lots of small controllers vs few larger ones?

Hey All,

I’m relatively new to Stimulus and loving it so far! However I was wondering…

Most of my stimulus is centered around dynamic form changes, think showing certain divs when checkboxes are checked and the like. I first started with a larger form controller, but soon found that I couldn’t pair an action with a certain target repeatedly within the form controller (think repeating checkboxes with each tied to their own div that shows/hides on checked/uncheck) without resorting to action parameters to specify targets. So I’m putting most field interactions into their own controllers, but this results in a LOT of controllers with one action for one dynamic interaction. Is this a good design pattern or am I missing something? It also means the potential of MANY controllers initializing on a single page…I assume the connect / initialize of these controllers are fairly lightweight?

1 Like

Could you explain yourself a bit more, sorry.

I believe it boils down to preference and the actions. Are the actions related in any way?. Maybe you can create a generic controller that acts as the housing of all the related actions?.

1 Like

To elaborate. Here is a controller that i grouped together, it basically disables a submit button in a form based on the fact of the input. See, the controller can either have target elements(see filledInputs method), or the input can directly call the sync method to check the status.

import { Controller } from "stimulus";

export default class extends Controller {
    static targets = ["submit", 'input'];
    static values = {
        enabled: Boolean,
    }

    connect() {
        if(this.enabledValue) {
           this.activate();
        } else {
           this.deactivate();
        }
    }

    activate() {
        if(this.active) return;

        this.submitTarget.disabled = false;
    }

    deactivate() {
        if(this.inactive) return;
        this.submitTarget.disabled = true;
    }

    // call this on an input where you would like it to disable the submit button
    // when it becomes empty
    sync(e) {
        if(e.target.value.trim().length === 0) {
            this.deactivate();
        } else {
            this.activate();
        }
    }

    syncInputTargets() {
        if (this.filledInputs) {
            this.activate()
        } else {
            this.deactivate();
        }
    }

    get active() {
        return this.submitTarget.disabled === false;
    }

    get inactive() {
        return this.submitTarget.disabled === true;
    }

    get filledInputs() {
        return this.inputTargets.every((input) => input.value.trim().length > 0)
    }
}
1 Like

Hey sorry I was trying to keep the question short but of course I can provide more details.

Let’s say I have a form (this is somewhat pseudocode):

<input type="checkbox" name="exp_date_toggle" id="exp_date_toggle1" value="true”> <select id=“select_1”>options….</select> </div <div id=“group_wrapper2> <input type="checkbox" name="exp_date_toggle" id="exp_date_toggle2" value="true”> <select id=“select_2”>options….</select> </div <div id=“group_wrapper3> <input type="checkbox" name="exp_date_toggle" id="exp_date_toggle3" value="true”> <select id=“select_3”>options….</select> </div> </blockquote> </form> <p>Here I want the functionality that on each checkbox being selected, the associated div appears, and deselected, it disappears. If I make a single controller on the form, I don’t see a way I can do this without using action parameters so that the checkbox change event explicitly states which associated select to toggle. If I make a “checkbox_controller”, then I’d attach one to each div wrapper and use targets is the “normal” way. But I’ve got a lot of these groupings and the checkbox_controller will be super small…with a toggleHide action. I’m simply wondering if this is a common thing to do as if I stick with this pattern I’ll have LOTS of little controllers.</p> <p>Let me know if you need more info!</p>

In your case. IMO it’s actually a good approach what you suggested(creating a checkbox_controller). And yes, it’s very common, i’m doing the same tbh. Here is a select_controller.

import { Controller } from "stimulus";

export default class extends Controller {
    static targets = ['select'];

    selectOption({target}) {
        Array.from(this.selectTarget.options).forEach((option) => {
            if(option.value === target.dataset.optionValue) {
                option.selected = true;
            }
        })
    }
}

I think it’s okay to have controllers be more files then be large controllers that try to do alot of stuff at once.

2 Likes

My guiding principle (partially inspired by this article) is to use a controller per type of interaction unless it’s impractical. That’s not a very strict methodology, but basically it means that more and smaller controller files is better because it makes it easier to reuse controllers and actions - unless the specific action’s structure makes it easier to group actions by class method or by controller.

For instance, I have an instant search filter that also highlights/marks the text you’ve searched for in the search result. For this, I found it more practical to put the highlight code in the same class method as the rest of the search/filter code instead of forcing it to be two data actions.

However, if it makes sense to split it up because of reuse or readability, I always prefer to split it up. I have a form element that uses data actions (five or six) from two different controllers. This is better (in my case) because some of the actions also apply to other templates. So it’s both easier to read and maintain and more DRY.

1 Like

Thank you both for your response! I was leaning this direction and it looks like I’m in good company :slight_smile:

1 Like