What are Controllers in AdonisJS?

What are Controllers?

Controllers form the C in MVC. They contain the business logic of the app. Controllers are used to

  1. Perform CRUD operations
  2. Respond to web socket events

Types of Controllers

Controllers come in two flavours,

  1. HTTP
  2. WebSocket

HTTP Controllers

HTTP controllers are linked to routes. When users make HTTP requests through routes, these controllers respond to the data from the requests, process it and return a response to the user. The HomeController in our app has an index method which handles the rendering of the index (’/’) route.

Taking advantage of the Class Architecture

Since controllers are classes, we can add logic in methods and not worry about clogging our route handling methods. You can see an example of this in the index method using data from the categories method.

"use strict";
const Quiz = use("App/Models/Quiz");
const axios = require("axios");
const { nanoid } = require("nanoid");

class HomeController {
  categories() {
    return [
      {
        id: 17,
        name: "Science and Nature",
        link:
          "https://images.unsplash.com/photo-1562445927-bdd32a655213?ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8OHw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 15,
        name: "Gaming",
        link:
          "https://images.unsplash.com/photo-1493711662062-fa541adb3fc8?ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8N3w1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 11,
        name: "Film",
        link:
          "https://images.unsplash.com/photo-1485846234645-a62644f84728?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8Mnw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 12,
        name: "Music",
        link:
          "https://images.unsplash.com/photo-1487180144351-b8472da7d491?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8MXw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 25,
        name: "Art",
        link:
          "https://images.unsplash.com/photo-1499781350541-7783f6c6a0c8?ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8NXw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 30,
        name: "Gadgets",
        link:
          "https://images.unsplash.com/photo-1468495244123-6c6c332eeece?ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8Nnw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 24,
        name: "Politics",
        link:
          "https://images.unsplash.com/photo-1541872703-74c5e44368f9?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8NHw1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&auto=format&fit=crop&w=286&q=60",
      },
      {
        id: 18,
        name: "Computers",
        link:
          "https://images.unsplash.com/photo-1517430816045-df4b7de11d1d?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxjb2xsZWN0aW9uLXBhZ2V8M3w1MTA2MjMzN3x8ZW58MHx8fA%3D%3D&auto=format&fit=crop&w=286&q=60",
      },
    ];
  }
  shuffle(array) {
    var currentIndex = array.length,
      temporaryValue,
      randomIndex;

    // While there remain elements to shuffle...
    while (0 !== currentIndex) {
      // Pick a remaining element...
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex -= 1;

      // And swap it with the current element.
      temporaryValue = array[currentIndex];
      array[currentIndex] = array[randomIndex];
      array[randomIndex] = temporaryValue;
    }

    return array;
  }
  prepareOptions(options) {
    const optionLetters = ["A", "B", "C", "D"];
    return options.map((option, index) => ({
      choice: optionLetters[index],
      option,
    }));
  }
  async index({ view }) {
    const categories = this.categories();
    return view.render("home", { categories });
  }

  async initializeQuiz({ view, request, response }) {
    const {
      difficulty,
      "number-of-question": numberOfQuestions,
      id,
    } = request.all();

    const quizResponse = await axios.get(
      `https://opentdb.com/api.php?amount=${numberOfQuestions}&category=${id}&difficulty=${difficulty}&type=multiple&encode=url3986`
    );

    const user_id = request.cookie("user_id");

    const quizes = quizResponse.data.results.map((quiz, index) => {
      return {
        question: quiz.question,
        answers: this.prepareOptions(
          this.shuffle([...quiz.incorrect_answers, quiz.correct_answer])
        ),
      };
    });

    if (!request.cookie("user_id")) {
      response.cookie("user_id", nanoid());
    }

    const quiz = await Quiz.create({
      title: `${quizResponse.data.results[0].category} Quiz`,
      user_id,
      quiz_with_answers: JSON.stringify(quizResponse.data.results),
      quiz_without_answers: JSON.stringify(quizes),
    });

    response.cookie("quiz_id", quiz.id);
    return response.redirect("/quiz", { quizes, decode: decodeURIComponent });
  }
}

module.exports = HomeController;

WebSocket Controllers

WebSocket controllers respond to events emitted by WebSocket clients and also emit events to clients. They are not connected to routes and have a different structure from HTTP controllers.

// WebSocket controller example
class ChatController {
  constructor({ socket, request }) {
    this.socket = socket;
    this.request = request;
  }
  async onMessage({payload}){
    // do stuff
  }
}

How do controller methods work?

Controller methods receive several parameters

  1. request
  2. response
  3. auth
  4. view
  5. session

These methods get passed down from the routes they bind to. For example, the index method of HomeController.js receives the params.

Route.get("/", "HomeController.index").as("home");

Request and Response in Controllers

Let’s start fleshing out the QuizController by building up the initializeQuiz method. This method responds to

Route.post("/", "QuizController.initializeQuiz");

This route is triggered by the form with id initialize-quiz-form on the home.edge view. The data from the form is gotten from request.all(). Add this to the interactive code widget.

    const {
      difficulty,
      "number-of-question": numberOfQuestions,
      id,
    } = request.all();

We are able to obtain the user’s id from a cookie and also set it if it is not present.

    const user_id = request.cookie("user_id");
    if (!request.cookie("user_id")) {
      response.cookie("user_id", nanoid());
    }

At this stage, our app does not have auth. Cookies will serve as pseudo-auth until we add auth later.

Finally, we set the quiz_id to a cookie to keep track of the quiz the user selected use response.redirect to redirect users to the /quiz view. Add this block to the interactive widget.

    response.cookie("quiz_id", quiz.id);
    return response.redirect("/quiz");
import React from 'react';
require('./style.css');

import ReactDOM from 'react-dom';
import App from './app.js';

ReactDOM.render(
  <App />, 
  document.getElementById('root')
);

Returning Views from Controllers To return a view, we use the render method with the view name. In the index method below, we use view.render to render the quiz view and also pass the parameters.

// inside QuizController.index
return view.render("quiz", {
  quizes: JSON.parse(quizes),
  title
});

Fixing the encoded text

Quiz questions and answers have weird symbols in them. They were encoded using encodeURIComponent. How can we decode the text to display normally? Good news, Adonis allows us to pass functions to our views. Add decode: decodeURIComponent to view.render in the interactive widget below. It should look like

// inside QuizController.index
return view.render("quiz", {
  quizes: JSON.parse(quizes),
  title,
+ decode: decodeURIComponent,
});

Now, in quiz.edge, add decode to

  1. <h1>{{title}}</h1>
  2. <h3>quiz.question</h3>
  3. <p class="ms-2">answer.option</p>
  1. <h1>{{decode(title)}}</h1>
  2. <h3>{{decode(quiz.question)}}</h3>
  3. <p class="ms-2">{{decode(answer.option)}}</p>

Hint: Use the search tool.

import React from 'react';
require('./style.css');

import ReactDOM from 'react-dom';
import App from './app.js';

ReactDOM.render(
  <App />, 
  document.getElementById('root')
);

In the next lesson, we will learn more about views.