ESM Control Registration
For larger applications, it is recommended to define controls in separate files using ES modules and register them from a central location. This approach provides better code organization, tree-shaking support, and easier maintenance.
Project Structure
Controls are organized in a dedicated directory with each control in its own file:
resources/js/
├── controls.js # Central registration file
└── controls/
├── popup-control.js
├── phone-input-control.js
├── address-input-control.js
└── ...Defining a Control Class
Each control is defined in its own file and exported as the default export. The class extends ControlBase imported from larajax.
// controls/popup-control.js
import { ControlBase } from "larajax";
export default class PopupControl extends ControlBase
{
init() {
this.$content = this.element.querySelector('.popup-content');
}
connect() {
this.listen('click', '.popup-trigger', this.onTriggerClick);
}
onTriggerClick() {
this.$content.classList.toggle('is-visible');
}
}Central Registration
Import all controls and register them in a single entry point file. This provides a clear overview of all available controls and their identifiers.
// controls.js
import { registerControl } from "larajax"
import PopupControl from './controls/popup-control';
import PhoneInputControl from './controls/phone-input-control';
import AddressInputControl from './controls/address-input-control';
// General
registerControl('popup', PopupControl);
// Form Components
registerControl('phone-input', PhoneInputControl);
registerControl('address-input', AddressInputControl);TIP
Group related controls with comments to improve readability and make it easier to find specific registrations.
Lazy Registration
Controls that are large or appear on few pages can be imported on demand. Pass a function that returns the import instead of the class. The control file, and everything it imports, loads when the first element with that control appears on the page.
// controls.js
registerControl('code-editor', () => import('./controls/code-editor-control'));Elements added while the control loads are connected once it is ready, and until then fetchControl() returns null for them. If the import fails, the error is reported and the elements stay unconnected. When bundling, enable code splitting so the import becomes a separate file (esbuild needs format: 'esm' and splitting: true).
Inside an element with the data-lazy-controls attribute, controls connect only when that element scrolls into view, so the import waits until then.
<div data-lazy-controls>
<div data-control="code-editor"></div>
</div>Benefits
This approach offers several advantages over inline registration:
- Separation of concerns - Each control lives in its own file with focused responsibility
- Tree-shaking - Bundlers can eliminate unused controls from production builds
- Testability - Controls can be imported and tested in isolation
- IDE support - Better autocomplete, navigation, and refactoring support
- Reusability - Controls can be shared across projects via npm packages
Naming Conventions
Follow these conventions for consistency:
| Item | Convention | Example |
|---|---|---|
| File name | kebab-case with -control suffix | phone-input-control.js |
| Class name | PascalCase with Control suffix | PhoneInputControl |
| Control identifier | kebab-case matching the component | phone-input |
Including in Your Build
Ensure the central registration file is included in your JavaScript bundle entry point:
// app.js
import './controls';This will execute the registration code and make all controls available to the page observer.