docs/development/concepts/stimulus/README.md
In a decision to move OpenProject towards the Hotwire approach, we introduced Stimulus.js to replace a collection of dynamically loaded custom JavaScript files used to sprinkle some interactivity.
This guide will outline how to add controllers and the conventions around it. This is not a documentation of stimulus itself. Use their documentation instead.
All controllers live under frontend/src/stimulus/controllers/. The naming convention is <controller-name>.controller.ts, meaning to dasherize the name of the controller. This makes it easier to generate names and classes using common IDEs.
If you want to add a common pattern, manually register the controller under frontend/src/stimulus/setup.ts. Often you'll want to have a dynamically loaded controller instead though.
If you want to add a stimulus controller from plugin code, you can do so by manually adding it to the preregister:
import { OpenProjectStimulusApplication } from 'core-stimulus/openproject-stimulus-application';
import { MyTestControllerClass } from './test/foo/my-test.controller';
OpenProjectStimulusApplication.preregister(
'test',
MyTestControllerClass
);
To dynamically load a controller, it needs to live under frontend/src/stimulus/controllers/dynamic/<controller-name>.controller.ts.
The application controller (frontend/src/stimulus/controllers/op-application.controller.ts) will automatically load controllers dynamically if they are not registered in the setup.ts file.
<div data-controller="users"></div>
If you want to organize your dynamic controllers in a subfolder, use the double dash convention of stimulus. For example, adding a new admin controller settings, you'd do the following:
frontend/src/stimulus/controllers/dynamic/admin/settings.controller.ts<div data-controller="admin--settings"></div>
You need to take care to prefix all actions, values etc. with the exact same pattern, e.g., data-admin--settings-target="foobar".
If you want to add a dynamic stimulus controller import from plugin code, you can do so by manually adding it to the preregister:
import { OpenProjectStimulusApplication } from 'core-app/stimulus/app';
OpenProjectStimulusApplication.preregisterDynamic(
'test',
() => import('./test.controller')
);
This ensures that the controller is loaded only when it is needed, and not at application startup. The controller will then be enabled when the data-controller attribute is present in the DOM through the same mechanism as for the core dynamic controllers.
If you have a single controller used in a partial, we have added a helper to use in a partial in order to append a controller to the #content-wrapper tag. This is useful if your template doesn't have a single DOM root. For example, to load the dynamic project-storage-form controller and provide a custom value to it:
<% content_controller 'project-storage-form',
'project-storage-form-folder-mode-value': @project_storage.project_folder_mode %>