Controllers

Introduction

In EJS, a controller is a class responsible for providing action methods to execute specific functionality within a module.

Acting as an interface to a module, controllers provide predefined functionality which may be called from the application.

Controller classes

A controller is a class that resides in a module's Controller namespace and has its name suffixed with Controller. Each controller class may optionally include a constructor which will be passed the active module instance and the application's file loader.

In many cases, a module only needs a single controller. However, for larger modules, it is a good idea to group related functionality into multiple controllers.

Enforcing suffixes on class and method names helps to protect against routing to non-controller classes or methods, inadvertently or otherwise.

MyFirstController.js
class MyFirstController 
{
    constructor(module, loader) {}
}

Action methods

An action method is a method on a controller class with its name suffixed with Action. The application will call these methods to interact with modules from the outside. This ensures that a module can properly dictate how it should be used while preventing coupling to the application itself.

Action methods may optionally return a value. If the returned value is a component, it will be inserted into the active layout.

Asynchronous action methods

An asynchronous action method is a method that may not return immediately as it needs to wait for its processing to complete (e.g. waiting for a component to load). Instead of returning a value directly, asynchronous action methods return a Promise that will resolve to the return value.

Example: Loading resources just-in-time

MyFirstController.js
class MyFirstController
{
    constructor(module, loader) {
        this.module = module;
        this.loader = loader;
    }
    
    async indexAction() {
        // Get the path to this module's Component namespace. 
        let pathToComponent = this.module.map().componentPath();
        return this.loader.load(pathToComponents + '/IndexPage.vue');
    }
}

Synchronous action methods

A synchronous action method is a method that does not contain any asynchronous processing, and optionally returns a value immediately.

Synchronous action methods are most useful for preloading related resources.

Example: Preloading resources

In this example, MyComponent and its dependencies will be loaded recursively when MyFirstController is loaded. When mySyncMethodAction is called, it can return MyComponent immediately as it is already loaded.

Preloading resources should be used sparingly and primarily for optimization when necessary. Excessive preloading can cause performance issues resulting from network or memory limits.

const MyComponent = use('Module/MyModule/Components/MyComponent.vue');

class MyFirstController
{
    mySyncMethodAction() {
        console.log('Running my action method!');
        return MyComponent;
    }
}

Using an abstract controller

When designing your controllers, it's likely you'll want to share some common functionality across many or all of them.

EJS provides a simple abstract controller with some convenience methods:

  • ElentraJS/Controller/ControllerAbstract#respond(componentName) Accepts the filename of a component, relative to the module's Component namespace. Returns a Promise that will resolve to the component when it is loaded.

If you extend the EJS ControllerAbstract class instead of using it directly, you can limit coupling to a single point in your module or application.

Example: Use an abstract controller via composition

MyFirstController.js
const ControllerAbstract = use('ElentraJS/Controller/ControllerAbstract');

class MyFirstController {
    constructor(module, loader) {
        this.controller = new ControllerAbstract(module, loader);
    }
    
    async indexAction() {
        return this.controller.respond('IndexPage.vue');
    }
}

Example: Use an abstract controller via inheritance

Always prefer composition over inheritance as it removes the vertical coupling across classes, enhances flexibility, and simplifies maintenance.

MyFirstController.js
const ControllerAbstract = use('ElentraJS/Controller/ControllerAbstract');

class MyFirstController extends ControllerAbstract {
    // Tip: You may omit this constructor if the super call is its only statement.
    constructor(module, loader) {
        super(module, loader);
    }
    
    async indexAction() {
        return this.respond('IndexPage.vue');
    }
}

Last updated