One place for hosting & domains

      Application

      How To Build and Deploy a Node.js Application To DigitalOcean Kubernetes Using Semaphore Continuous Integration and Delivery


      The author selected the Open Internet / Free Speech fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Kubernetes allows users to create resilient and scalable services with a single command. Like anything that sounds too good to be true, it has a catch: you must first prepare a suitable Docker image and thoroughly test it.

      Continuous Integration (CI) is the practice of testing the application on each update. Doing this manually is tedious and error-prone, but a CI platform runs the tests for you, catches errors early, and locates the point at which the errors were introduced. Release and deployment procedures are often complicated, time-consuming, and require a reliable build environment. With Continuous Delivery (CD) you can build and deploy your application on each update without human intervention.

      To automate the whole process, you’ll use Semaphore, a Continuous Integration and Delivery (CI/CD) platform.

      In this tutorial, you’ll build an address book API service with Node.js. The API exposes a simple RESTful API interface to create, delete, and find people in the database. You’ll use Git to push the code to GitHub. Then you’ll use Semaphore to test the application, build a Docker image, and deploy it to a DigitalOcean Kubernetes cluster. For the database, you’ll create a PostgreSQL cluster using DigitalOcean Managed Databases.

      Prerequisites

      Before reading on, ensure you have the following:

      • A DigitalOcean account and a Personal Access Token. Follow Create a Personal Access Token to set one up for your account.
      • A Docker Hub account.
      • A GitHub account.
      • A Semaphore account; you can sign up with your GitHub account.
      • A new GitHub repository called addressbook for the project. When creating the repository, select the Initialize this repository with a README checkbox and select Node in the Add .gitignore menu. Follow GitHub’s Create a Repo help page for more details.
      • Git installed on your local machine and set up to work with your GitHub account. If you are unfamiliar or need a refresher, consider reading the How to use Git reference guide.
      • curl installed on your local machine.
      • Node.js installed on your local machine. In this tutorial, you’ll use Node.js version 10.16.0.

      Step 1 — Creating the Database and the Kubernetes Cluster

      Start by provisioning the services that will power the application: the DigitalOcean Database Cluster and the DigitalOcean Kubernetes Cluster.

      Log in to your DigitalOcean account and create a project. A project lets you organize all the resources that make up the application. Call the project addressbook.

      Next, create a PostgreSQL cluster. The PostgreSQL database service will hold the application’s data. You can pick the latest version available. It should take a few minutes before the service is ready.

      Once the PostgreSQL service is ready, create a database and a user. Set the database name to addessbook_db and set the username to addressbook_user. Take note of the password that’s generated for your new user. Databases are PostgreSQL’s way of organizing data. Usually, each application has its own database, although there are no hard rules about this. The application will use the username and password to get access to the database so it can save and retrieve its data.

      Finally, create a Kubernetes Cluster. Choose the same region in which the database is running. Name the cluser addressbook-server and set the number of nodes to 3.

      While the nodes are provisioning, you can start building your application.

      Step 2 — Writing the Application

      Let’s build the address book application you’re going to deploy. To start, clone the GitHub repository you created in the prerequisites so you have a local copy of the .gitignore file GitHub created for you, and you’ll be able to commit your application code quickly without having to manually create a repository. Open your browser and go to your new GitHub repository. Click on the Clone or download button and copy the provided URL. Use Git to clone the empty repository to your machine:

      • git clone https://github.com/your_github_username/addressbook

      Enter the project directory:

      With the repository cloned, you can start writing the app. You’ll build two components: a module that interacts with the database, and a module that provides the HTTP service. The database module will know how to save and retrieve persons from the address book database, and the HTTP module will receive requests and respond accordingly.

      While not strictly mandatory, it’s good practice to test your code while you write it, so you’ll also create a testing module. This is the planned layout for the application:

      • database.js: database module. It handles database operations.
      • app.js: the end user module and the main application. It provides an HTTP service for the users to connect to.
      • database.test.js: tests for the database module.

      In addition, you’ll want a package.json file for your project, which describes the project and its required dependencies. You can either create it manually with your editor, or interactively using npm. Run the npm init command to create the file interactively:

      The command will ask for some information to get started. Fill in the values as shown in the example. If you don’t see an answer listed, leave the answer blank, which uses the default value in parentheses:

      npm output

      package name: (addressbook) addressbook version: (1.0.0) 1.0.0 description: Addressbook API and database entry point: (index.js) app.js test command: git repository: URL for your GitHub repository keywords: author: Sammy the Shark <[email protected]>" license: (ISC) About to write to package.json: { "name": "addressbook", "version": "1.0.0", "description": "Addressbook API and database", "main": "app.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "author": "", "license": "ISC" } Is this OK? (yes) yes

      Now you can start writing the code. The database is at the core of the service you’re developing. It’s essential to have a well-designed database model before writing any other components. Consequently, it makes sense to start with the database code.

      You don’t have to code all the bits of the application; Node.js has a large library of reusable modules. For instance, you don’t have to write any SQL queries if you have the Sequelize ORM module in the project. This module provides an interface that handles databases as JavaScript objects and methods. It can also create tables in your database. Sequelize needs the pg module to work with PostgreSQL.

      Install modules using the npm install command with the --save option, which tells npm to save the module in package.json. Execute this command to install both sequelize and pg:

      • npm install --save sequelize pg

      Create a new JavaScript file to hold the database code:

      Import the sequelize module by adding this line to the file:

      database.js

      const Sequelize = require('sequelize');
      
      . . .
      

      Then, below that line, initialize a sequelize object with the database connection parameters, which you’ll retrieve from the system environment. This keeps the credentials out of your code so you don’t accidentally share your credentials when you push your code to GitHub. You can use process.env to access environment variables, and JavaScripts’s || operator to set defaults for undefined variables:

      database.js

      . . .
      
      const sequelize = new Sequelize(process.env.DB_SCHEMA || 'postgres',
                                      process.env.DB_USER || 'postgres',
                                      process.env.DB_PASSWORD || '',
                                      {
                                          host: process.env.DB_HOST || 'localhost',
                                          port: process.env.DB_PORT || 5432,
                                          dialect: 'postgres',
                                          dialectOptions: {
                                              ssl: process.env.DB_SSL == "true"
                                          }
                                      });
      
      . . .
      

      Now define the Person model. To keep the example from getting too complex, you’ll only create two fields: firstName and lastName, both storing string values. Add the following code to define the model:

      database.js

      . . .
      
      const Person = sequelize.define('Person', {
          firstName: {
              type: Sequelize.STRING,
              allowNull: false
          },
          lastName: {
              type: Sequelize.STRING,
              allowNull: true
          },
      });
      
      . . .
      

      This defines the two fields, making firstName mandatory with allowNull: false. Sequelize’s model definition documentation shows the available data types and options.

      Finally, export the sequelize object and the Person model so other modules can use them:

      database.js

      . . .
      
      module.exports = {
          sequelize: sequelize,
          Person: Person
      };
      

      It’s handy to have a table-creation script in a separate file that you can call at any time during development. These types of files are called migrations. Create a new file to hold this code:

      Add these lines to the file to import the database model you defined, and call the sync() function to initialize the database, which creates the table for your model:

      migrate.js

      var db = require('./database.js');
      db.sequelize.sync();
      

      The application is looking for database connection information in system environment variables. Create a file called .env to hold those values, which you will load into the environment during development:

      Add the following variable declarations to the file. Ensure that you set DB_HOST, DB_PORT, and DB_PASSWORD to those associated with your DigitalOcean PostgreSQL cluster:

      .env

      export DB_SCHEMA=addressbook_db
      export DB_USER=addressbook_user
      export DB_PASSWORD=your_db_user_password
      export DB_HOST=your_db_cluster_host
      export DB_PORT=your_db_cluster_port
      export DB_SSL=true
      export PORT=3000
      

      Save the file.

      Warning: never check environment files into source control. They usually have sensitive information.

      Since you defined a default .gitignore file when you created the repository, this file is already ignored.

      You are ready to initialize the database. Import the environment file and run migrate.js:

      • source ./.env
      • node migrate.js

      This creates the database table:

      Output

      Executing (default): CREATE TABLE IF NOT EXISTS "People" ("id" SERIAL , "firstName" VARCHAR(255) NOT NULL, "lastName" VARCHAR(255), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id")); Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'People' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;

      The output shows two commands. The first one creates the People table as per your definition. The second command checks that the table was indeed created by looking it up in the PostgreSQL catalog.

      It’s good practice to create tests for your code. With tests, you can validate the code’s behavior. You can write a check for each function, method, or any other part of your system and verify that it works the way you’d expect, without having to test things manually.

      The jest testing framework is a great fit for writing tests against Node.js applications. Jest scans the files in the project for test files and executes them one a time. Install Jest with the --save-dev option, which tells npm that the module is not required to run the program, but it is a dependency for developing the application:

      • npm install --save-dev jest

      You’ll write tests to verify that you can insert, read, and delete records from your database. These tests will verify that your database connection and permissions are configured properly, and will also provide some tests you can use in your CI/CD pipeline later.

      Create the database.test.js file:

      Add the following content. Start by importing the database code:

      database.test.js

      const db = require('./database');
      
      . . .
      

      To ensure the database is ready to use, call sync() inside the beforeAll function:

      database.test.js

      . . .
      
      beforeAll(async () => {
          await db.sequelize.sync();
      });
      
      . . .
      

      The first test creates a person record in the database. The sequelize library executes all queries asynchronously, which means it doesn’t wait for the results of the query. To make the test wait for results so you can verify them, you must use the async and await keywords. This test calls the create() method to insert a new row in the database. Use expect to compare the person.id column with 1. The test will fail if you get a different value:

      database.test.js

      . . .
      
      test('create person', async () => {
          expect.assertions(1);
          const person = await db.Person.create({
              id: 1,
              firstName: 'Sammy',
              lastName: 'Davis Jr.',
              email: '[email protected]'
          });
          expect(person.id).toEqual(1);
      });
      
      . . .
      

      In the next test, use the findByPk() method to retrieve the row with id=1. Then, validate the firstName and lastName values. Once again, use async and await:

      database.test.js

      . . .
      
      test('get person', async () => {
          expect.assertions(2);
          const person = await db.Person.findByPk(1);
          expect(person.firstName).toEqual('Sammy');
          expect(person.lastName).toEqual('Davis Jr.');
      });
      
      . . .
      

      Finally, test removing a person from the database. The destroy() method deletes the person with id=1. To ensure that it worked, try retrieving the person a second time and checking that the returned value is null:

      database.test.js

      . . .
      
      test('delete person', async () => {
          expect.assertions(1);
          await db.Person.destroy({
              where: {
                  id: 1
              }
          });
          const person = await db.Person.findByPk(1);
          expect(person).toBeNull();
      });
      
      . . .
      

      Finally, add this code to close the connection to the database with close() once all tests have finished:

      app.js

      . . .
      
      afterAll(async () => {
          await db.sequelize.close();
      });
      

      Save the file.

      The jest command runs the test suite for your program, but you can also store commands in package.json. Open this file in your editor:

      Locate the scripts keyword and replace the existing test line (which was just a placeholder). The test command is jest:

      . . .
      
        "scripts": {
          "test": "jest"
        },
      
      . . .
      

      Now you can call npm run test to invoke the test suite. This may be a longer command, but if you need to modify the jest command later, external services won’t have to change; they can continue calling npm run test.

      Run the tests:

      Then, check the results:

      Output

      console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): CREATE TABLE IF NOT EXISTS "People" ("id" SERIAL , "firstName" VARCHAR(255) NOT NULL, "lastName" VARCHAR(255), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id")); console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'People' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname; console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): INSERT INTO "People" ("id","firstName","lastName","createdAt","updatedAt") VALUES ($1,$2,$3,$4,$5) RETURNING *; console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1; console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): DELETE FROM "People" WHERE "id" = 1 console.log node_modules/sequelize/lib/sequelize.js:1176 Executing (default): SELECT "id", "firstName", "lastName", "createdAt", "updatedAt" FROM "People" AS "Person" WHERE "Person"."id" = 1; PASS ./database.test.js ✓ create person (344ms) ✓ get person (173ms) ✓ delete person (323ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 5.315s Ran all test suites.

      With the database code tested, you can build the API service to manage the people in the address book.

      To serve HTTP requests, you’ll use the Express web framework. Install Express and save it as a dependency using npm install:

      • npm install --save express

      You’ll also need the body-parser module, which you’ll use to access the HTTP request body. Install this as a dependency as well:

      • npm install --save body-parser

      Create the main application file app.js:

      Import the express, body-parser, and database modules. Then create an instance of the express module called app to control and configure the service. You use app.use() to add features such as middleware. Use this to add the body-parser module so the application can read url-encoded strings:

      app.js

      var express = require('express');
      var bodyParser = require('body-parser');
      var db = require('./database');
      var app = express();
      app.use(bodyParser.urlencoded({ extended: true }));
      
      . . .
      

      Next, add routes to the application. Routes are similar to buttons in an app or website; they trigger some action in your application. Routes link unique URLs to actions in the application. Each route will serve a specific path and support a different operation.

      The first route you’ll define handles GET requests for the /person/$ID path, which will display the database record for the person with the specified ID. Express automatically sets the value of the requested $ID in the req.params.id variable.

      The application must reply with the person data encoded as a JSON string. As you did in the database tests, use the findByPk() method to retrieve the person by id and reply to the request with HTTP status 200 (OK) and send the person record as JSON. Add the following code:

      app.js

      . . .
      
      app.get("/person/:id", function(req, res) {
          db.Person.findByPk(req.params.id)
              .then( person => {
                  res.status(200).send(JSON.stringify(person));
              })
              .catch( err => {
                  res.status(500).send(JSON.stringify(err));
              });
      });
      
      . . .
      

      Errors cause the code in catch() to be executed. For instance, if the database is down, the connection will fail, and this will execute instead. In case of trouble, set the HTTP status to 500 (Internal Server Error) and send the error message back to the user:

      Add another route to create a person in the database. This route will handle PUT requests and access the person’s data from the req.body. Use the create() method to insert a row in the database:

      app.js

      . . .
      
      app.put("/person", function(req, res) {
          db.Person.create({
              firstName: req.body.firstName,
              lastName: req.body.lastName,
              id: req.body.id
          })
              .then( person => {
                  res.status(200).send(JSON.stringify(person));
              })
              .catch( err => {
                  res.status(500).send(JSON.stringify(err));
              });
      });
      
      . . .
      

      Add another route to handle DELETE requests, which will remove records from the address book. First, use the ID to locate the record and then use the destroy method to remove it:

      app.js

      . . .
      
      app.delete("/person/:id", function(req, res) {
          db.Person.destroy({
              where: {
                  id: req.params.id
              }
          })
              .then( () => {
                  res.status(200).send();
              })
              .catch( err => {
                  res.status(500).send(JSON.stringify(err));
              });
      });
      
      . . .
      

      And for convenience, add a route that retrieves all people in the database using the /all path:

      app.js

      . . .
      
      app.get("/all", function(req, res) {
          db.Person.findAll()
              .then( persons => {
                  res.status(200).send(JSON.stringify(persons));
              })
              .catch( err => {
                  res.status(500).send(JSON.stringify(err));
              });
      });
      
      . . .
      

      One last route left. If the request did not match any of the previous routes, send status code 404 (Not Found):

      app.js

      . . .
      
      app.use(function(req, res) {
          res.status(404).send("404 - Not Found");
      });
      
      . . .
      

      Finally, add the listen() method, which starts up the service. If the environment variable PORT is defined, then the service listens in that port; otherwise, it defaults to port 3000:

      app.js

      . . .
      
      var server = app.listen(process.env.PORT || 3000, function() {
          console.log("app is running on port", server.address().port);
      });
      

      As you’ve learned, the package.json file lets you define various commands to run tests, start your apps, and other tasks, which often lets you run common commands with much less typing. Add a new command on package.json to start the application. Edit the file:

      Add the start command, so it looks like this:

      package.json

      . . .
      
        "scripts": {
          "test": "jest",
          "start": "node app.js"
        },
      
      . . .
      

      Don’t forget to add a comma to the previous line, as the scripts section needs its entries separated by commas.

      Save the file and start the application for the first time. First, load the environment file with source; this imports the variables into the session and makes them available to the application. Then, start the application with npm run start:

      • source ./.env
      • npm run start

      The app starts on port 3000:

      Output

      app is running on port 3000

      Open a browser and navigate to http://localhost:3000/all. You’ll see a page showing [].

      Switch back to your terminal and press CTRL-C to stop the application.

      Now is an excellent time to add code quality tests. Code quality tools, also known as linters, scan the project for issues in the code. Bad coding practices like leaving unused variables, not ending statements with a semicolon, or missing curly braces can cause bugs that are difficult to find.

      Install jshint tool, a JavaScript linter, as a development dependency:

      • npm install --save-dev jshint

      Over the years, JavaScript has received of updates, features, and syntax changes. The language has been standardized by ECMA International under the name of “ECMAScript”. About once a year, ECMA releases a new version of ECMAScript with new features.

      By default, jshint assumes that your code is compatible with ES6 (ECMAScript Version 6), and will throw an error if it finds any keywords not supported in that version. You’ll want to find the version that is compatible with your code. If you look at the feature table for all the recent versions, you’ll find that the async/await keywords were not introduced until ES8. You used both keywords in the database test code, so that sets the minimum compatible version to ES8.

      To tell jshint the version you’re using, create a file called .jshintrc:

      In the file, specify esversion. The jshintrc file uses JSON, so create a new JSON object in the file:

      .jshintrc

      { "esversion": 8 }
      

      Save the file and exit the editor.

      Add a command to run jshint. Edit package.json:

      Add a lint command to your project in the scripts section of package.json. The command calls the lint tool against all the JavaScript files you created so far:

      package.json

      . . .
      
        "scripts": {
          "test": "jest",
          "start": "node app.js",
          "lint": "jshint app.js database*.js migrate.js"
        },
      
      . . .
      

      Now you can run the linter to find any issues:

      There should not be any error messages:

      Output

      > jshint app.js database*.js migrate.js

      If there are any errors, jshint will show the line that has the problem.

      You’ve completed the project and ensured it works. Add the files to the repository, commit, and push the changes:

      • git add *.js
      • git add package*.json
      • git add .jshintrc
      • git commit -m 'initial commit'
      • git push origin master

      Now you can configure Semaphore to test, build, and deploy the application, starting by configuring Semaphore with your DigitalOcean Personal Access Token and database credentials.

      Step 3 — Creating Secrets in Semaphore

      There is some information that doesn’t belong in a GitHub repository. Passwords and API Tokens are good examples of this. You’ve stored this sensitive data in a separate file and loaded it into your environment, When using Semaphore, you can use Secrets to store sensitive data.

      There are three kinds of secrets in the project:

      • Docker Hub: the username and password of your Docker Hub account.
      • DigitalOcean Personal Access Token: to deploy the application to your Kubernetes cluster.
      • Environment Variables: for database username and password connection parameters.

      To create the first secret, open your browser and log in to the Semaphore website. On the left navigation menu, click Secrets under the CONFIGURATION heading. Click the Create New Secret button.

      For Name of the Secret, enter dockerhub. Then under Environment Variables, create two environment variables:

      • DOCKER_USERNAME: your DockerHub username.
      • DOCKER_PASSWORD: your DockerHub password.

      Docker Hub Secret

      Click Save Changes.

      Create a second secret for your DigitalOcean Personal Access Token. Once again, click on Secrets on the left navigation menu, then on Create New Secret. Call this secret do-access-token and create an environment value called DO_ACCESS_TOKEN with the value set to your Personal Access Token:

      DigitalOcean Token Secret

      Save the secret.

      For the next secret, instead of setting environment variables directly, you’ll upload the .env file from the project’s root.

      Create a new secret called env-production. Under the Files section, press the Upload file link to locate and upload your .env file, and tell Semaphore to place it at /home/semaphore/env-production.

      Environment Secret

      Note: Because the file is hidden, you may have trouble finding it on your computer. There is usually a menu item or a key combination to view hidden files, such as CTRL+H. If all else fails, you can try copying the file with a non-hidden name:

      Then upload the file and rename it back:

      The environment variables are all configured. Now you can begin the Continuous Integration setup.

      Step 4 — Adding your Project to Semaphore

      In this step you will add your project to Semaphore and start the Continuous Integration (CI) pipeline.

      First, link your GitHub repository with Semaphore:

      1. Log in to your Semaphore account.
      2. Click the + icon next to PROJECTS.
      3. Click the Add Repository button next to your repository.

      Add Repository to Semaphore

      Now that Semaphore is connected, it will pick up any changes in the repository automatically.

      You are now ready to create the Continuous Integration pipeline for the application. A pipeline defines the path your code must travel to get built, tested, and deployed. The pipeline is automatically run each time there is a change in the GitHub repository.

      First, you should ensure that Semaphore uses the same version of Node you’ve been using during development. You can check which version is running on your machine:

      Output

      v10.16.0

      You can tell Semaphore which version of Node.js to use by creating a file called .nvmrc in your repository. Internally, Semaphore uses node version manager to switch between Node.js versions. Create the .nvmrc file and set the version to 10.16.0:

      Semaphore pipelines go in the .semaphore directory. Create the directory:

      Create a new pipeline file. The initial pipeline is always called semaphore.yml. In this file, you’ll define all the steps required to build and test the application.

      • nano .semaphore/semaphore.yml

      Note: You are creating a file in the YAML format. You must preserve the leading spaces as shown in the tutorial.

      The first line must set the Semaphore file version; the current stable is v1.0. Also, the pipeline needs a name. Add these lines to your file:

      .semaphore/semaphore.yml

      version: v1.0
      name: Addressbook
      
      . . .
      

      Semaphore automatically provisions virtual machines to run the tasks. There are various machines to choose from. For the integration jobs, use the e1-standard-2 (2 CPUs 4 GB RAM) along with an Ubuntu 18.04 OS. Add these lines to the file:

      .semaphore/semaphore.yml

      . . .
      
      agent:
        machine:
          type: e1-standard-2
          os_image: ubuntu1804
      
      . . .
      

      Semaphore uses blocks to organize the tasks. Each block can have one or more jobs. All jobs in a block run in parallel, each one in an isolated machine. Semaphore waits for all jobs in a block to pass before starting the next one.

      Start by defining the first block, which installs all the JavaScript dependencies to test and run the application:

      .semaphore/semaphore.yml

      . . .
      
      blocks:
        - name: Install dependencies
          task:
      
      . . .
      

      You can define environment variables that are common for all jobs, like setting NODE_ENV to test, so Node.js knows this is a test environment. Add this code after task:

      .semaphore/semaphore.yml

      . . .
          task:
            env_vars:
              - name: NODE_ENV
                value: test
      
      . . .
      

      Commands in the prologue section are executed before each job in the block. It’s a convenient place to define setup tasks. You can use checkout to clone the GitHub repository. Then, nvm use activates the appropriate Node.js version you specified in .nvmrc. Add the prologue section:

      .semaphore/semaphore.yml

          task:
      . . .
      
            prologue:
              commands:
                - checkout
                - nvm use
      
      . . .
      

      Next add this code to install the project’s dependencies. To speed up jobs, Semaphore provides the cache tool. You can run cache store to save node_modules directory in Semaphore’s cache. cache automatically figures out which files and directories should be stored. The second time the job is executed, cache restore restores the directory.

      .semaphore/semaphore.yml

      . . .
      
            jobs:
              - name: npm install and cache
                commands:
                  - cache restore
                  - npm install
                  - cache store 
      
      . . .
      

      Add another block which will run two jobs. One to run the lint test, and another to run the application’s test suite.

      .semaphore/semaphore.yml

      . . .
      
        - name: Tests
          task:
            env_vars:
              - name: NODE_ENV
                value: test
            prologue:
              commands:
                - checkout
                - nvm use
                - cache restore 
      
      . . .
      

      The prologue repeats the same commands as in the previous block and restores node_module from the cache. Since this block will run tests, you set the NODE_ENV environment variable to test.

      Now add the jobs. The first job performs the code quality check with jshint:

      .semaphore/semaphore.yml

      . . .
      
            jobs:
              - name: Static test
                commands:
                  - npm run lint
      
      . . .
      

      The next job executes the unit tests. You’ll need a database to run them, as you don’t want to use your production database. Semaphore’s sem-service can start a local PostgreSQL database in the test environment that is completely isolated. The database is destroyed when the job ends. Start this service and run the tests:

      .semaphore/semaphore.yml

      . . .
      
              - name: Unit test
                commands:
                  - sem-service start postgres
                  - npm run test
      

      Save the .semaphore/semaphore.yml file.

      Now add and commit the changes to the GitHub repository:

      • git add .nvmrc
      • git add .semaphore/semaphore.yml
      • git commit -m "continuous integration pipeline"
      • git push origin master

      As soon as the code is pushed to GitHub, Semaphore starts the CI pipeline:

      Running Workflow

      You can click on the pipeline to show the blocks and jobs, and their output.

      Integration Pipeline

      Next you will create a new pipeline that builds a Docker image for the application.

      Step 5 — Building Docker Images for the Application

      A Docker image is the basic unit of a Kubernetes deployment. The image should have all the binaries, libraries, and code required to run the application. A Docker container is not a lightweight virtual machine, but it behaves like one. The Docker Hub registry contains hundreds of ready-to-use images, but we’re going to build our own.

      In this step, you’ll add a new pipeline to build a custom Docker image for your app and push it to Docker Hub.

      To build a custom image, create a Dockerfile:

      The Dockerfile is a recipe to create the image. You can use the official Node.js distribution as a starting point instead of starting from scratch. Add this to your Dockerfile:

      Dockerfile

      FROM node:10.16.0-alpine
      
      . . .
      

      Then add a command which copies package.json and package-lock.json, and then install the node modules inside the image:

      Dockerfile

      . . .
      
      COPY package*.json ./
      RUN npm install
      
      . . .
      

      Installing the dependencies first will speed up subsequent builds, as Docker will cache this step.

      Now add this command which copies all the application files in the project root into the image:

      Dockerfile

      . . .
      
      COPY *.js ./
      
      . . .
      

      Finally, EXPOSE specifies that the container listens for connections on port 3000, where the application is listening, and CMD sets the command that should run when the container starts. Add these lines to your file:

      Dockerfile

      . . .
      
      EXPOSE 3000
      CMD [ "npm", "run", "start" ]
      

      Save the file.

      With the Dockerfile complete, you can create a new pipeline so Semaphore can build the image for you when you push your code to GitHub. Create a new file called docker-build.yml:

      • nano .semaphore/docker-build.yml

      Start the pipeline with the same boilerplate as the the CI pipline, but with the name Docker build:

      .semaphore/docker-build.yml

      version: v1.0
      name: Docker build
      agent:
        machine:
          type: e1-standard-2
          os_image: ubuntu1804
      
      . . .
      

      This pipeline will have only one block and one job. In Step 3, you created a secret named dockerhub with your Docker Hub username and password. Here, you’ll import these values using the secrets keyword. Add this code:

      .semaphore/docker-build.yml

      . . .
      
      blocks:
        - name: Build
          task:
            secrets:
              - name: dockerhub
      
      . . .
      

      Docker images are stored in repositories. We’ll use the official Docker Hub which allows for an unlimited number of public images. Add these lines to check out the code from GitHub and use the docker login command to authenticate with Docker Hub.

      .semaphore/docker-build.yml

          task:
      . . .
      
            prologue:
              commands:
                - checkout
                - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
      
      . . .
      

      Each Docker image is fully identified by the combination of name and tag. The name usually corresponds to the product or software, and the tag corresponds to the particular version of the software. For example, node.10.16.0. When no tag is supplied, Docker defaults to the special latest tag. Hence, it’s considered good practice to use the latest tag to refer to the most current image.

      Add the following code to build the image and push it to Docker Hub:

      .semaphore/docker-build.yml

      . . .
      
            jobs:
            - name: Docker build
              commands:
                - docker pull "${DOCKER_USERNAME}/addressbook:latest" || true
                - docker build --cache-from "${DOCKER_USERNAME}/addressbook:latest" -t "${DOCKER_USERNAME}/addressbook:$SEMAPHORE_WORKFLOW_ID" .
                - docker push "${DOCKER_USERNAME}/addressbook:$SEMAPHORE_WORKFLOW_ID"
      

      When Docker builds the image, it reuses parts of existing images to speed up the process. The first command tries to pull the latest image from Docker Hub so it may be reused. Semaphore stops the pipeline if any of the commands return a status code different than zero. For example, if the repository doesn’t have any latest image, as it won’t on the first try, the pipeline will stop. You can force Semaphore to ignore failed commands by appending || true to the command.

      The second command builds the image. To reference this particular image later, you can tag it with a unique string. Semaphore provides several environment variables for jobs. One of them, $SEMAPHORE_WORKFLOW_ID is unique and shared among all the pipelines in the workflow. It’s handy for referencing this image later in the deployment.

      The third command pushes the image to Docker Hub.

      The build pipeline is ready, but Semaphore will not start it unless you connect it to the main CI pipeline. You can chain multiple pipelines to create complex, multi-branch workflows using promotions.

      Edit the main pipeline file .semaphore/semaphore.yml:

      • nano .semaphore/semaphore.yml

      Add the following lines at the end of the file:

      .semaphore/semaphore.yml

      . . .
      
      promotions:
        - name: Dockerize
          pipeline_file: docker-build.yml
          auto_promote_on:
            - result: passed
      

      auto_promote_on defines the condition to start the docker build pipeline. In this case, it runs when all jobs defined in the semaphore.yml file have passed.

      To test the new pipeline, you need to add, commit, and push all the modified files to GitHub:

      • git add Dockerfile
      • git add .semaphore/docker-build.yml
      • git add .semaphore/semaphore.yml
      • git commit -m "docker build pipeline"
      • git push origin master

      After the CI pipeline is complete, the Docker build pipeline starts.

      Build Pipeline

      When it finishes, you’ll see your new image in your Docker Hub repository.

      You’ve got your build process testing and creating the image. Now you’ll create the final pipeline to deploy the application to your Kubernetes cluster.

      Step 6 — Setting up Continuous Deployment to Kubernetes

      The building block of a Kubernetes deployment is the pod. A pod is a group of containers that are managed as a single unit. The containers inside a pod start and stop in unison and always run on the same machine, sharing its resources. Each pod has an IP address. In this case, the pods will only have one container.

      Pods are ephemeral; they are created and destroyed frequently. You can’t tell which IP address is going to be assigned to each pod until it’s started. To solve this, you’ll use services, which have fixed public IP addresses so incoming connections can be load-balanced and forwarded to the pods.

      You could manage pods directly, but it’s better to let Kubernetes handle that by using a deployment. In this section, you will create a declarative manifest that describes the final desired state for your cluster. The manifest has two resources:

      • Deployment: starts the pods in the cluster nodes as required and keeps track of their status. Since in this tutorial we’re using a 3-node cluster, we’ll deploy 3 pods.
      • Service: acts as an entry point for our users. Listens to traffic on port 80 (HTTP) and forwards the connection to the pods.

      Create a manifest file called deployment.yml:

      Start the manifest with the Deployment resource. Add the following contents to the new file to define the deployment:

      deployment.yml

      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: addressbook
      spec:
        replicas: 3
        selector:
          matchLabels:
            app: addressbook
        template:
          metadata:
            labels:
              app: addressbook
          spec:
            containers:
              - name: addressbook
                image: ${DOCKER_USERNAME}/addressbook:${SEMAPHORE_WORKFLOW_ID}
                env:
                  - name: NODE_ENV
                    value: "production"
                  - name: PORT
                    value: "$PORT"
                  - name: DB_SCHEMA
                    value: "$DB_SCHEMA"
                  - name: DB_USER
                    value: "$DB_USER"
                  - name: DB_PASSWORD
                    value: "$DB_PASSWORD"
                  - name: DB_HOST
                    value: "$DB_HOST"
                  - name: DB_PORT
                    value: "$DB_PORT"
                  - name: DB_SSL
                    value: "$DB_SSL"
      
      
      . . .
      

      For each resource in the manifest, you need to set an apiVersion. For deployments, use apiVersion: apps/v1, a stable version. Then, tell Kubernetes that this resource is a Deployment with kind: Deployment. Each definition should have a name defined in metadata.name.

      In the spec section you tell Kubernetes what the desired final state is. This definition requests that Kubernetes should create 3 pods with replicas: 3.

      Labels are key-value pairs used to organize and cross-reference Kubernetes resources. You define labels with metadata.labels, and you can look for matching labels with selector.matchLabels. This is how you connect elements togther.

      The key spec.template defines a model that Kubernetes will use to create each pod. Inside spec.template.metadata.labels you set one label for the pods: app: addressbook.

      With spec.selector.matchLabels you make the deployment manage any pods with the label app: addressbook. In this case you are making this deployment responsible for all the pods.

      Finally, you define the image that runs in the pods. In spec.template.spec.containers you set the image name. Kubernetes will pull the image from the registry as needed. In this case, it will pull from Docker Hub). You can also set environment variables for the containers, which is fortunate because you need to supply several values for the database connection.

      To keep the deployment manifest flexible, you’ll be relying on variables. The YAML format, however, doesn’t allow variables, so the file isn’t valid yet. You’ll solve that problem when you define the deployment pipeline for Semaphore.

      That’s it for the deployment. But this only defines the pods. You still need a service that will allow traffic to flow to your pods. You can add another Kubernetes resource in the same file as long as you use three hyphens (---) as a separator.

      Add the following code to define a load balancer service that connects to pods with the addressbook label:

      deployment.yml

      . . .
      
      ---
      
      apiVersion: v1
      kind: Service
      metadata:
        name: addressbook-lb
      spec:
        selector:
          app: addressbook
        type: LoadBalancer
        ports:
          - port: 80
            targetPort: 3000
      

      The load balancer will receive connections on port 80 and forward them to the pods’ port 3000 where the application is listening.

      Save the file.

      Now, create a deployment pipeline for Semaphore that will deploy the app using the manifest. Create a new file in the .semaphore directory:

      • nano .semaphore/deploy-k8s.yml

      Begin the pipeline as usual, specifying the version, name, and image:

      .semaphore/deploy-k8s.yml

      version: v1.0
      name: Deploy to Kubernetes
      agent:
        machine:
          type: e1-standard-2
          os_image: ubuntu1804
      
      . . .
      

      This pipeline will have two blocks. The first block deploys the application to the Kubernetes cluster.

      Define the block and import all the secrets:

      .semaphore/deploy-k8s.yml

      . . .
      
      blocks:
        - name: Deploy to Kubernetes
          task:
            secrets:
              - name: dockerhub
              - name: do-access-token
              - name: env-production
      
      . . .
      

      Store your DigitalOcean Kubernetes cluster name in an environment variable so you can reference it later:

      .semaphore/deploy-k8s.yml

      . . .
      
            env_vars:
              - name: CLUSTER_NAME
                value: addressbook-server
      
      . . .
      

      DigitalOcean Kubernetes clusters are managed with a combination of two programs: kubectl and doctl. The former is already included in Semaphore’s image, but the latter isn’t, so you need to install it. You can use the prologue section to do it.

      Add this prologue section:

      .semaphore/deploy-k8s.yml

      . . .
      
            prologue:
              commands:
                - wget https://github.com/digitalocean/doctl/releases/download/v1.20.0/doctl-1.20.0-linux-amd64.tar.gz
                - tar xf doctl-1.20.0-linux-amd64.tar.gz 
                - sudo cp doctl /usr/local/bin
                - doctl auth init --access-token $DO_ACCESS_TOKEN
                - doctl kubernetes cluster kubeconfig save "${CLUSTER_NAME}"
                - checkout
      
      . . .
      

      The first command downloads the doctl official release with wget. The second command decompresses it with tar and copies it into the local path. Once doctl is installed, it can be used to authenticate with the DigitalOcean API and request the Kubernetes config file for our cluster. After checking out our code, we are done with the prologue:

      Next comes the final piece of our pipeline: deploying to the cluster.

      Remember that there were some environment variables in deployment.yml, and YAML does not allow that. As a result, deployment.yml in its current state, won’t work. To get around that, source the environment file to load the variables, then use the envsubst command to expand the variables in-place with the actual values. The result, a file called deploy.yml, is entirely valid YAML with the values inserted. With the file in place, you can start the deployment with kubectl apply:

      .semaphore/deploy-k8s.yml

      . . .
      
            jobs:
            - name: Deploy
              commands:
                - source $HOME/env-production
                - envsubst < deployment.yml | tee deploy.yml
                - kubectl apply -f deploy.yml
      
      . . .
      

      The second block adds the latest tag to the image on Docker Hub to denote that this is the most current version deployed. Repeat the Docker login steps, then pull, retag, and push to Docker Hub:

      .semaphore/deploy-k8s.yml

      . . .
      
        - name: Tag latest release
          task:
            secrets:
              - name: dockerhub
            prologue:
              commands:
                - checkout
                - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
                - checkout
            jobs:
            - name: docker tag latest
              commands:
                - docker pull "${DOCKER_USERNAME}/addressbook:$SEMAPHORE_WORKFLOW_ID" 
                - docker tag "${DOCKER_USERNAME}/addressbook:$SEMAPHORE_WORKFLOW_ID" "${DOCKER_USERNAME}/addressbook:latest"
                - docker push "${DOCKER_USERNAME}/addressbook:latest"
      

      Save the file.

      This pipeline performs the deployment, but it can only start if the Docker image was successfully generated and pushed to Docker Hub. As a result, you must connect the build and deployment pipelines with a promotion. Edit the Docker build pipeline to add it:

      • nano .semaphore/docker-build.yml

      Add the promotion to the end of the file:

      .semaphore/docker-build.yml

      . . .
      
      promotions:
        - name: Deploy to Kubernetes
          pipeline_file: deploy-k8s.yml
          auto_promote_on:
            - result: passed
      

      You are done setting up the CI/CD workflow.

      All that remains is pushing the modified files and letting Semaphore do the work. Add, commit, and push your repository’s changes:

      • git add .semaphore/deploy-k8s.yml
      • git add .semaphore/docker-build.yml
      • git add deployment.yml
      • git commit -m "kubernetes deploy pipeline"
      • git push origin master

      It’ll take a few minutes for the deployment to complete.

      Deploy Pipeline

      Let’s test the application next.

      Step 7 — Testing the Application

      At this point, the application is up and running. In this step, you’ll use curl to test the API endpoint.

      You’ll need to know the public IP that DigitalOcean has given to your cluster. Follow these steps to find it:

      1. Log in to your DigitalOcean account.
      2. Select the addressbook project
      3. Go to Networking.
      4. Click on Load Balancers.
      5. The IP Address is shown. Copy the IP address.

      Load Balancer IP

      Let’s check the /all route using curl:

      • curl -w "n" YOUR_CLUSTER_IP/all

      You can use the -w "n" option to ensure curl prints all lines:

      Since there are no records in the database yet, you get an empty JSON array as the result:

      Output

      []

      Create a new person record by making a PUT request to the /person endpoint:

      • curl -w "n" -X PUT
      • -d "firstName=Sammy&lastName=the Shark" YOUR_CLUSTER_IP/person

      The API returns the JSON object for the person:

      Output

      { "id": 1, "firstName": "Sammy", "lastName": "the Shark", "updatedAt": "2019-07-04T23:51:00.548Z", "createdAt": "2019-07-04T23:51:00.548Z" }

      Create a second person:

      • curl -w "n" -X PUT
      • -d "firstName=Tommy&lastName=the Octopus" YOUR_CLUSTER_IP/person

      The output indicates that a second person was created:

      Output

      { "id": 2, "firstName": "Tommy", "lastName": "the Octopus", "updatedAt": "2019-07-04T23:52:08.724Z", "createdAt": "2019-07-04T23:52:08.724Z" }

      Now make a GET request to get the person with the id of 2:

      • curl -w "n" YOUR_CLUSTER_IP/person/2

      The server replies with the data you requested:

      Output

      { "id": 2, "firstName": "Tommy", "lastName": "the Octopus", "createdAt": "2019-07-04T23:52:08.724Z", "updatedAt": "2019-07-04T23:52:08.724Z" }

      To delete the person, send a DELETE request:

      • curl -w "n" -X DELETE YOUR_CLUSTER_IP/person/2

      No output is returned by this command.

      You should only have one person in your database, the one with the id of 1. Try getting /all again:

      • curl -w "n" YOUR_CLUSTER_IP/all

      The server replies with an array of persons containing only one record:

      Output

      [ { "id": 1, "firstName": "Sammy", "lastName": "the Shark", "createdAt": "2019-07-04T23:51:00.548Z", "updatedAt": "2019-07-04T23:51:00.548Z" } ]

      At this point, there’s only one person left in the database.

      This completes the tests for all the endpoints in our application and marks the end of the tutorial.

      Conclusion

      In this tutorial, you wrote a complete Node.js application from scratch which used DigitalOcean’s managed PostgreSQL database service. You then used Semaphore’s CI/CD pipelines to fully automate a workflow that tested and built a container image, uploaded it to Docker Hub, and deployed it to DigitalOcean Kubernetes.

      To learn more about Kubernetes, you can read An Introduction to Kubernetes and the rest of DigitalOcean’s Kubernetes tutorials.

      Now that your application is deployed, you may consider adding a domain name, securing your database cluster, or setting up alerts for your database.



      Source link

      How To Add Stimulus to a Ruby on Rails Application


      Introduction

      If you are working with a Ruby on Rails project, your requirements may include some interactivity with the HTML generated by your view templates. If so, you have a few choices for how to implement this interactivity.

      For example, you could implement a JavaScript framework like React or Ember. If your requirements include handling state on the client side, or if you are concerned about performance issues associated with frequent queries to the server, then choosing one of these frameworks may make sense. Many Single Page Applications (SPAs) take this approach.

      However, there are several considerations to keep in mind when implementing a framework that manages state and frequent updates on the client side:

      1. It’s possible for loading and conversion requirements — things like parsing JavaScript, and fetching and converting JSON to HTML — to limit performance.
      2. Commitment to a framework may involve writing more code than your particular use case requires, particularly if you are looking for small-scale JavaScript enhancements.
      3. State managed on both the client and server side can lead to a duplication of efforts, and increases the surface area for errors.

      As an alternative, the team at Basecamp (the same team that wrote Rails) has created Stimulus.js, which they describe as “a modest JavaScript framework for the HTML you already have.” Stimulus is meant to enhance a modern Rails application by working with server-side generated HTML. State lives in the Document Object Model (DOM), and the framework offers standard ways of interacting with elements and events in the DOM. It works side by side with Turbolinks (included in Rails 5+ by default) to improve performance and load times with code that is limited and scoped to a clearly defined purpose.

      In this tutorial, you will install and use Stimulus to build on an existing Rails application that offers readers information about sharks. The application already has a model for handling shark data, but you will add a nested resource for posts about individual sharks, allowing users to build out a body of thoughts and opinions about sharks. This piece runs roughly parallel to How To Create Nested Resources for a Ruby on Rails Application, except that we will be using JavaScript to manipulate the position and appearance of posts on the page. We will also take a slightly different approach to building out the post model itself.

      Prerequisites

      To follow this tutorial, you will need:

      • A local machine or development server running Ubuntu 18.04. Your development machine should have a non-root user with administrative privileges and a firewall configured with ufw. For instructions on how to set this up, see our Initial Server Setup with Ubuntu 18.04 tutorial.
      • Node.js and npm installed on your local machine or development server. This tutorial uses Node.js version 10.16.3 and npm version 6.9.0. For guidance on installing Node.js and npm on Ubuntu 18.04, follow the instructions in the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 18.04.
      • Ruby, rbenv, and Rails installed on your local machine or development server, following Steps 1-4 in How To Install Ruby on Rails with rbenv on Ubuntu 18.04. This tutorial uses Ruby 2.5.1, rbenv 1.1.2, and Rails 5.2.3.
      • SQLite installed, and a basic shark information application created, following the directions in How To Build a Ruby on Rails Application.

      Step 1 — Creating a Nested Model

      Our first step will be to create a nested Post model, which we will associate with our existing Shark model. We will do this by creating Active Record associations between our models: posts will belong to particular sharks, and each shark can have multiple posts.

      To get started, navigate to the sharkapp directory that you created for your Rails project in the prerequisites:

      To create our Post model, we’ll use the rails generate command with the model generator. Type the following command to create the model:

      • rails generate model Post body:text shark:references

      With body:text, we’re telling Rails to include a body field in the posts database table — the table that maps to the Post model. We’re also including the :references keyword, which sets up an association between the Shark and Post models. Specifically, this will ensure that a foreign key representing each shark entry in the sharks database is added to the posts database.

      Once you have run the command, you will see output confirming the resources that Rails has generated for the application. Before moving on, you can check your database migration file to look at the relationship that now exists between your models and database tables. Use the following command to look at the contents of the file, making sure to substitute the timestamp on your own migration file for what’s shown here:

      • cat db/migrate/20190805132506_create_posts.rb

      You will see the following output:

      Output

      class CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| t.text :body t.references :shark, foreign_key: true t.timestamps end end end

      As you can see, the table includes a column for a shark foreign key. This key will take the form of model_name_id — in our case, shark_id.

      Rails has established the relationship between the models elsewhere as well. Take a look at the newly generated Post model with the following command:

      Output

      class Post < ApplicationRecord belongs_to :shark end

      The belongs_to association sets up a relationship between models in which a single instance of the declaring model belongs to a single instance of the named model. In the case of our application, this means that a single post belongs to a single shark.

      Though Rails has already set the belongs_to association in our Post model, we will need to specify a has_many association in our Shark model as well in order for that relationship to function properly.

      To add the has_many association to the Shark model, open app/models/shark.rb using nano or your favorite editor:

      Add the following line to the file to establish the relationship between sharks and posts:

      ~/sharkapp/app/models/shark.rb

      class Shark < ApplicationRecord
        has_many :posts
        validates :name, presence: true, uniqueness: true
        validates :facts, presence: true
      end
      

      One thing that is worth thinking about here is what happens to posts once a particular shark is deleted. We likely do not want the posts associated with a deleted shark persisting in the database. To ensure that any posts associated with a given shark are eliminated when that shark is deleted, we can include the dependent option with the association.

      Add the following code to the file to ensure that the destroy action on a given shark deletes any associated posts:

      ~/sharkapp/app/models/shark.rb

      class Shark < ApplicationRecord
        has_many :posts , dependent: :destroy
        validates :name, presence: true, uniqueness: true
        validates :facts, presence: true
      end
      

      Once you have finished making these changes, save and close the file. If you are working with nano, do this by pressing CTRL+X, Y, then ENTER.

      You now have a model generated for your posts, but you will also need a controller to coordinate between the data in your database and the HTML that’s generated and presented to users.

      Step 2 — Creating a Controller for a Nested Resource

      Creating a posts controller will involve setting a nested resource route in the application’s main routing file and creating the controller file itself to specify the methods we want associated with particular actions.

      To begin, open your config/routes.rb file to establish the relationship between your resourceful routes:

      Currently, the file looks like this:

      ~/sharkapp/config/routes.rb

      Rails.application.routes.draw do
        resources :sharks
      
        root 'sharks#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      We want to create a dependent relationship relationship between shark and post resources. To do this, update your route declaration to make :sharks the parent of :posts. Update the code in the file to look like the following:

      ~/sharkapp/config/routes.rb

      Rails.application.routes.draw do
        resources :sharks do
          resources :posts
        end
        root 'sharks#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      Save and close the file when you are finished editing.

      Next, create a new file called app/controllers/posts_controller.rb for the controller:

      • nano app/controllers/posts_controller.rb

      In this file, we’ll define the methods that we will use to create and destroy individual posts. However, because this is a nested model, we’ll also want to create a local instance variable, @shark, that we can use to associate particular posts with specific sharks.

      First, we can create the PostsController class itself, along with two private methods: get_shark, which will allow us to reference a particular shark, and post_params, which gives us access to user-submitted information by way of the params method.

      Add the following code to the file:

      ~/sharkapp/app/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :get_shark 
      
        private
      
        def get_shark
          @shark = Shark.find(params[:shark_id])
        end
      
        def post_params
          params.require(:post).permit(:body, :shark_id)
        end
      end
      

      You now have methods to get the particular shark instances with which your posts will be associated, using the :shark_id key, and the data that users are inputting to create posts. Both of these objects will now be available for the methods you will define to handle creating and destroying posts.

      Next, above the private methods, add the following code to the file to define your create and destroy methods:

      ~/sharkapp/app/controllers/posts_controller.rb

      . . .
        def create
          @post = @shark.posts.create(post_params)
        end
      
        def destroy
          @post = @shark.posts.find(params[:id])
          @post.destroy   
        end
      . . .
      

      These methods associate @post instances with particular @shark instances, and use the collection methods that became available to us when we created the has_many association between sharks and posts. Methods such as find and create allow us to target the collection of posts associated with a particular shark.

      The finished file will look like this:

      ~/sharkapp/app/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :get_shark 
      
        def create
          @post = @shark.posts.create(post_params)
        end
      
        def destroy
          @post = @shark.posts.find(params[:id])
          @post.destroy   
        end
      
        private
      
        def get_shark
          @shark = Shark.find(params[:shark_id])
        end
      
        def post_params
          params.require(:post).permit(:body, :shark_id)
        end
      end
      

      Save and close the file when you are finished editing.

      With your controller and model in place, you can begin thinking about your view templates and how you will organize your application’s generated HTML.

      Step 3 — Reorganizing Views with Partials

      You have created a Post model and controller, so the last thing to think about from a Rails perspective will be the views that present and allow users to input information about sharks. Views are also the place where you will have a chance to build out interactivity with Stimulus.

      In this step, you will map out your views and partials, which will be the starting point for your work with Stimulus.

      The view that will act as the base for posts and all partials associated with posts is the sharks/show view.

      Open the file:

      • nano app/views/sharks/show.html.erb

      Currently, the file looks like this:

      ~/sharkapp/app/views/sharks/show.html.erb

      <p id="notice"><%= notice %></p>
      
      <p>
        <strong>Name:</strong>
        <%= @shark.name %>
      </p>
      
      <p>
        <strong>Facts:</strong>
        <%= @shark.facts %>
      </p>
      
      <%= link_to 'Edit', edit_shark_path(@shark) %> |
      <%= link_to 'Back', sharks_path %>
      

      When we created our Post model, we opted not to generate views for our posts, since we will handle them through our sharks/show view. So in this view, the first thing we will address is how we will accept user input for new posts, and how we will present posts back to the user.

      Note: For an alternative to this approach, please see How To Create Nested Resources for a Ruby on Rails Application, which sets up post views using the full range of Create, Read, Update, Delete (CRUD) methods defined in the posts controller. For a discussion of these methods and how they work, please see Step 3 of How To Build a Ruby on Rails Application.

      Instead of building all of our functionality into this view, we will use partials — reusable templates that serve a particular function. We will create one partial for new posts, and another to control how posts are displayed back to the user. Throughout, we’ll be thinking about how and where we can use Stimulus to manipulate the appearance of posts on the page, since our goal is to control the presentation of posts with JavaScript.

      First, below shark facts, add an <h2> header for posts and a line to render a partial called sharks/posts:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . . 
      <p>
        <strong>Facts:</strong>
        <%= @shark.facts %>
      </p>
      
      <h2>Posts</h2>
      <%= render 'sharks/posts' %>
      . . . 
      

      This will render the partial with the form builder for new post objects.

      Next, below the Edit and Back links, we will add a section to control the presentation of older posts on the page. Add the following lines to the file to render a partial called sharks/all:

      ~/sharkapp/app/views/sharks/show.html.erb

      <%= link_to 'Edit', edit_shark_path(@shark) %> |
      <%= link_to 'Back', sharks_path %>
      
      <div>
        <%= render 'sharks/all' %>
      </div>
      

      The <div> element will be useful when we start integrating Stimulus into this file.

      Once you are finished making these edits, save and close the file. With the changes you’ve made on the Rails side, you can now move on to installing and integrating Stimulus into your application.

      Step 4 — Installing Stimulus

      The first step in using Stimulus will be to install and configure our application to work with it. This will include making sure we have the correct dependencies, including the Yarn package manager and Webpacker, the gem that will allow us to work with the JavaScript pre-processor and bundler webpack. With these dependencies in place, we will be able to install Stimulus and use JavaScript to manipulate events and elements in the DOM.

      Let’s begin by installing Yarn. First, update your package list:

      Next, add the GPG key for the Debian Yarn repository:

      • curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -

      Add the repository to your APT sources:

      • echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

      Update the package database with the newly added Yarn packages:

      And finally, install Yarn:

      With yarn installed, you can move on to adding the webpacker gem to your project.

      Open your project’s Gemfile, which lists the gem dependencies for your project:

      Inside the file, you will see Turbolinks enabled by default:

      ~/sharkapp/Gemfile

      . . . 
      # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
      gem 'turbolinks', '~> 5'
      . . . 
      

      Turbolinks is designed to improve performance by optimizing page loads: instead of having link clicks navigate to a new page, Turbolinks intercepts these click events and makes the page request using Asynchronous JavaScript and HTML (AJAX). It then replaces the body of the current page and merges the contents of the <head> sections, while the JavaScript window and document objects and the <html> element persist between renders. This addresses one of the main causes of slow page load times: the reloading of CSS and JavaScript resources.

      We get Turbolinks by default in our Gemfile, but we will need to add the webpacker gem so that we can install and use Stimulus. Below the turbolinks gem, add webpacker:

      ~/sharkapp/Gemfile

      . . . 
      # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
      gem 'turbolinks', '~> 5'
      gem 'webpacker', '~> 4.x'
      . . . 
      

      Save and close the file when you are finished.

      Next, add the gem to your project’s bundle with the bundle command:

      This will generate a new Gemfile.lock file — the definitive record of gems and versions for your project.

      Next, install the gem in the context of your bundle with the following bundle exec command:

      • bundle exec rails webpacker:install

      Once the installation is complete, we will need to make one small adjustment to our application’s content security file. This is due to the fact that we are working with Rails 5.2+, which is a Content Security Policy (CSP) restricted environment, meaning that the only scripts allowed in the application must be from trusted sources.

      Open config/initializers/content_security_policy.rb, which is the default file Rails gives us for defining application-wide security policies:

      • nano config/initializers/content_security_policy.rb

      Add the following lines to the bottom of the file to allow webpack-dev-server — the server that serves our application’s webpack bundle — as an allowed origin:

      ~/sharkapp/config/initializers/content_security_policy.rb

      . . . 
      Rails.application.config.content_security_policy do |policy|
        policy.connect_src :self, :https, 'http://localhost:3035', 'ws://localhost:3035' if Rails.env.development?
      end
      

      This will ensure that the webpacker-dev-server is recognized as a trusted asset source.

      Save and close the file when you are finished making this change.

      By installing webpacker, you created two new directories in your project’s app directory, the directory where your main application code is located. The new parent directory, app/javascript, will be where your project’s JavaScript code will live, and it will have the following structure:

      Output

      ├── javascript │ ├── controllers │ │ ├── hello_controller.js │ │ └── index.js │ └── packs │ └── application.js

      The app/javascript directory will contain two child directories: app/javascript/packs, which will have your webpack entry points, and app/javascript/controllers, where you will define your Stimulus controllers. The bundle exec command that we just used will create the app/javascript/packs directory, but we will need to install Stimulus for the app/javascript/controllers directory to be autogenerated.

      With webpacker installed, we can now install Stimulus with the following command:

      • bundle exec rails webpacker:install:stimulus

      You will see output like the following, indicating that the installation was successful:

      Output

      . . . success Saved lockfile. success Saved 5 new dependencies. info Direct dependencies └─ [email protected] info All dependencies ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] ├─ @stimulus/[email protected] └─ [email protected] Done in 8.30s. Webpacker now supports Stimulus.js 🎉

      We now have Stimulus installed, and the main directories we need to work with it in place. Before moving on to writing any code, we’ll need to make a few application-level adjustments to complete the installation process.

      First, we’ll need to make an adjustment to app/views/layouts/application.html.erb to ensure that our JavaScript code is available and that the code defined in our main webpacker entry point, app/javascript/packs/application.js, runs each time a page is loaded.

      Open that file:

      • nano app/views/layouts/application.html.erb

      Change the following javascript_include_tag tag to javascript_pack_tag to load app/javascript/packs/application.js:

      ~/sharkapp/app/views/layouts/application.html.erb

      . . .
          <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
          <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
      . . . 
      

      Save and close the file when you have made this change.

      Next, open app/javascript/packs/application.js:

      • nano app/javascript/packs/application.js

      Initially, the file will look like this:

      ~/sharkapp/app/javascript/packs/application.js

      . . . 
      console.log('Hello World from Webpacker')
      
      import "controllers"
      

      Delete the boilerplate code that’s there, and add the following code to load your Stimulus controller files and boot the application instance:

      ~/sharkapp/app/javascript/packs/application.js

      . . . 
      import { Application } from "stimulus"
      import { definitionsFromContext } from "stimulus/webpack-helpers"
      
      const application = Application.start()
      const context = require.context("../controllers", true, /.js$/)
      application.load(definitionsFromContext(context))
      

      This code uses webpack helper methods to require the controllers in the app/javascript/controllers directory and load this context for use in your application.

      Save and close the file when you are finished editing.

      You now have Stimulus installed and ready to use in your application. Next, we’ll build out the partials that we referenced in our sharks show view — sharks/posts and sharks/all — using Stimulus controllers, targets, and actions.

      Step 5 — Using Stimulus in Rails Partials

      Our sharks/posts partial will use the form_with form helper to create a new post object. It will also make use of Stimulus’s three core concepts: controllers, targets, and actions. These concepts work as follows:

      • Controllers are JavaScript classes that are defined in JavaScript modules and exported as the module’s default object. Through controllers, you have access to particular HTML elements and the Stimulus Application instance defined in app/javascript/packs/application.js.
      • Targets allow you to reference particular HTML elements by name, and are associated with particular controllers.
      • Actions control how DOM events are handled by controllers, and are also associated with particular controllers. They create a connection between the HTML element associated with the controller, the methods defined in the controller, and a DOM event listener.

      In our partial, we’re first going to build a form as we normally would using Rails. We will then add a Stimulus controller, action, and targets to the form in order to use JavaScript to control how new posts get added to the page.

      First, create a new file for the partial:

      • nano app/views/sharks/_posts.html.erb

      Inside the file, add the following code to create a new post object using the form_with helper:

      ~/sharkapp/app/views/sharks/_posts.html.erb

              <%= form_with model: [@shark, @shark.posts.build] do |form| %>
                      <%= form.text_area :body, placeholder: "Your post here" %>
                      <br>
                      <%= form.submit %>
              <% end %>
      

      So far, this form behaves like a typical Rails form, using the form_with helper to build a post object with the fields defined for the Post model. Thus, the form has a field for the post :body, to which we’ve added a placeholder with a prompt for filling in a post.

      Additionally, the form is scoped to take advantage of the collection methods that come with the associations between the Shark and Post models. In this case, the new post object that’s created from user-submitted data will belong to the collection of posts associated with the shark we’re currently viewing.

      Our goal now is to add some Stimulus controllers, events, and actions to control how the post data gets displayed on the page. The user will ultimately submit post data and see it posted to the page thanks to a Stimulus action.

      First, we’ll add a controller to the form called posts in a <div> element:

      ~/sharkapp/app/views/sharks/_posts.html.erb

      <div data-controller="posts">
              <%= form_with model: [@shark, @shark.posts.build] do |form| %>
                       <%= form.text_area :body, placeholder: "Your post here" %>
                       <br>
                       <%= form.submit %>
              <% end %>
      </div>
      

      Make sure you add the closing <div> tag to scope the controller properly.

      Next, we’ll attach an action to the form that will be triggered by the form submit event. This action will control how user input is displayed on the page. It will reference an addPost method that we will define in the posts Stimulus controller:

      ~/sharkapp/app/views/sharks/_posts.html.erb

      <div data-controller="posts">
              <%= form_with model: [@shark, @shark.posts.build], data: { action: "posts#addBody" } do |form| %>
              . . . 
                       <%= form.submit %>
              <% end %>
      </div>
      

      We use the :data option with form_with to submit the Stimulus action as an additional HTML data attribute. The action itself has a value called an action descriptor made up of the following:

      • The DOM event to listen for. Here, we are using the default event associated with form elements, submit, so we do not need to specify the event in the descriptor itself. For more information about common element/event pairs, see the Stimulus documentation.
      • The controller identifier, in our case posts.
      • The method that the event should invoke. In our case, this is the addBody method that we will define in the controller.

      Next, we’ll attach a data target to the user input defined in the :body <textarea> element, since we will use this inputted value in the addBody method.

      Add the following :data option to the :body <textarea> element:

      ~/sharkapp/app/views/sharks/_posts.html.erb

      <div data-controller="posts">
              <%= form_with model: [@shark, @shark.posts.build], data: { action: "posts#addBody" } do |form| %>
                      <%= form.text_area :body, placeholder: "Your post here", data: { target: "posts.body" } %>
      . . .
      

      Much like action descriptors, Stimulus targets have target descriptors, which include the controller identifier and the target name. In this case, posts is our controller, and body is the target itself.

      As a last step, we’ll add a data target for the inputted body values so that users will be able to see their posts as soon as they are submitted.

      Add the following <ul> element with an add target below the form and above the closing <div>:

      ~/sharkapp/app/views/sharks/_posts.html.erb

      . . .
              <% end %>
        <ul data-target="posts.add">
        </ul>
      
      </div>
      

      As with the body target, our target descriptor includes both the name of the controller and the target — in this case, add.

      The finished partial will look like this:

      ~/sharkapp/app/views/sharks/_posts.html.erb

      <div data-controller="posts">
              <%= form_with model: [@shark, @shark.posts.build], data: { action: "posts#addBody"} do |form| %>
                      <%= form.text_area :body, placeholder: "Your post here", data: { target: "posts.body" } %>
                      <br>
                      <%= form.submit %>
              <% end %>
        <ul data-target="posts.add">
        </ul>
      
      </div>
      

      Once you have made these changes, you can save and close the file.

      You have now created one of the two partials you added to the sharks/show view template. Next, you’ll create the second, sharks/all, which will show all of the older posts from the database.

      Create a new file named _all.html.erb in the app/views/sharks/ directory:

      • nano app/views/sharks/_all.html.erb

      Add the following code to the file to iterate through the collection of posts associated with the selected shark:

      ~/sharkapp/app/views/sharks/_all.html.erb

      <% for post in @shark.posts  %>
          <ul>
      
              <li class="post">
                  <%= post.body %>
              </li>
      
          </ul>
          <% end %>
      

      This code uses a for loop to iterate through each post instance in the collection of post objects associated with a particular shark.

      We can now add some Stimulus actions to this partial to control the appearance of posts on the page. Specifically, we will add actions that will control upvotes and whether or not posts are visible on the page

      Before we do that, however, we will need to add a gem to our project so that we can work with Font Awesome icons, which we’ll use to register upvotes. Open a second terminal window, and navigate to your sharkapp project directory.

      Open your Gemfile:

      Below your webpacker gem, add the following line to include the font-awesome-rails gem in the project:

      ~/sharkapp/Gemfile

      . . . 
      gem 'webpacker', '~> 4.x'
      gem 'font-awesome-rails', '~>4.x'
      . . . 
      

      Save and close the file.

      Next, install the gem:

      Finally, open your application’s main stylesheet, app/assets/stylesheets/application.css:

      • nano app/assets/stylesheets/application.css

      Add the following line to include Font Awesome’s styles in your project:

      ~/sharkapp/app/assets/stylesheets/application.css

      . . . 
      *
       *= require_tree .
       *= require_self
       *= require font-awesome
       */
      

      Save and close the file. You can now close your second terminal window.

      Back in your app/views/sharks/_all.html.erb partial, you can now add two button_tags with associated Stimulus actions, which will be triggered on click events. One button will give users the option to upvote a post and the other will give them the option to remove it from the page view.

      Add the following code to app/views/sharks/_all.html.erb:

      ~/sharkapp/app/views/sharks/_all.html.erb

      <% for post in @shark.posts  %>
          <ul>
      
              <li class="post">
                  <%= post.body %>
                  <%= button_tag "Remove Post", data: { controller: "posts", action: "posts#remove" } %>
                  <%= button_tag "Upvote Post", data: { controller: "posts", action: "posts#upvote" } %>
              </li>
      
          </ul>
          <% end %>
      

      Button tags also take a :data option, so we’ve added our posts Stimulus controller and two actions: remove and upvote. Once again, in the action descriptors, we only need to define our controller and method, since the default event associated with button elements is click. Clicking on each of these buttons will trigger the respective remove and upvote methods defined in our controller.

      Save and close the file when you have finished editing.

      The final change we will make before moving on to defining our controller is to set a data target and action to control how and when the sharks/all partial will be displayed.

      Open the show template again, where the initial call to render sharks/all is currently defined:

      • nano app/views/sharks/show.html.erb

      At the bottom of the file, we have a <div> element that currently looks like this:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . . 
      <div>
        <%= render 'sharks/all' %>
      </div>
      

      First, add a controller to this <div> element to scope actions and targets:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . . 
      <div data-controller="posts">
        <%= render 'sharks/all' %>
      </div>
      

      Next, add a button to control the appearance of the partial on the page. This button will trigger a showAll method in our posts controller.

      Add the button below the <div> element and above the render statement:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . . 
      <div data-controller="posts">
      
      <button data-action="posts#showAll">Show Older Posts</button>
      
        <%= render 'sharks/all' %>
      

      Again, we only need to identify our posts controller and showAll method here — the action will be triggered by a click event.

      Next, we will add a data target. The goal of setting this target is to control the appearance of the partial on the page. Ultimately, we want users to see older posts only if they have opted into doing so by clicking on the Show Older Posts button.

      We will therefore attach a data target called show to the sharks/all partial, and set its default style to visibility:hidden. This will hide the partial unless users opt in to seeing it by clicking on the button.

      Add the following <div> element with the show target and style definition below the button and above the partial render statement:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . . 
      <div data-controller="posts">
      
      <button data-action="posts#showAll">Show Older Posts</button>
      
      <div data-target="posts.show" style="visibility:hidden">
        <%= render 'sharks/all' %>
      </div>
      

      Be sure to add the closing </div> tag.

      The finished show template will look like this:

      ~/sharkapp/app/views/sharks/show.html.erb

      <p id="notice"><%= notice %></p>
      
      <p>
        <strong>Name:</strong>
        <%= @shark.name %>
      </p>
      
      <p>
        <strong>Facts:</strong>
        <%= @shark.facts %>
      </p>
      
      <h2>Posts</h2>
      
      <%= render 'sharks/posts' %>
      
      <%= link_to 'Edit', edit_shark_path(@shark) %> |
      <%= link_to 'Back', sharks_path %>
      
      <div data-controller="posts">
      
      <button data-action="posts#showAll">Show Older Posts</button>
      
      <div data-target="posts.show" style="visibility:hidden">
        <%= render 'sharks/all' %>
      </div>
      </div>
      

      Save and close the file when you are finished editing.

      With this template and its associated partials finished, you can move on to creating the controller with the methods you’ve referenced in these files.

      Step 6 — Creating the Stimulus Controller

      Installing Stimulus created the app/javascript/controllers directory, which is where webpack is loading our application context from, so we will create our posts controller in this directory. This controller will include each of the methods we referenced in the previous step:

      • addBody(), to add new posts.
      • showAll(), to show older posts.
      • remove(), to remove posts from the current view.
      • upvote(), to attach an upvote icon to posts.

      Create a file called posts_controller.js in the app/javascript/controllers directory:

      • nano app/javascript/controllers/posts_controller.js

      First, at the top of the file, extend Stimulus’s built-in Controller class:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      import { Controller } from "stimulus"
      
      export default class extends Controller {
      }
      

      Next, add the following target definitions to the file:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      . . .
      export default class extends Controller {
          static targets = ["body", "add", "show"]
      }
      

      Defining targets in this way will allow us to access them in our methods with the this.target-nameTarget property, which gives us the first matching target element. So, for example, to match the body data target defined in our targets array, we would use this.bodyTarget. This property allows us to manipulate things like input values or css styles.

      Next, we can define the addBody method, which will control the appearance of new posts on the page. Add the following code below the target definitions to define this method:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      . . .
      export default class extends Controller {
          static targets = [ "body", "add", "show"]
      
          addBody() {
              let content = this.bodyTarget.value;
              this.addTarget.insertAdjacentHTML('beforebegin', "<li>" + content + "</li>");
          }
      }
      

      This method defines a content variable with the let keyword and sets it equal to the post input string that users entered into the posts form. It does this by virtue of the body data target that we attached to the <textarea> element in our form. Using this.bodyTarget to match this element, we can then use the value property that is associated with that element to set the value of content as the post input users have entered.

      Next, the method adds this post input to the add target we added to the <ul> element below the form builder in the sharks/posts partial. It does this using the Element.insertAdjacentHTML() method, which will insert the content of the new post, set in the content variable, before the add target element. We’ve also enclosed the new post in an <li> element, so that new posts appear as bulleted list items.

      Next, below the addBody method, we can add the showAll method, which will control the appearance of older posts on the page:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      . . . 
      export default class extends Controller {
      . . .
          addBody() {
              let content = this.bodyTarget.value;
              this.addTarget.insertAdjacentHTML('beforebegin', "<li>" + content + "</li>");
          }
      
          showAll() {
              this.showTarget.style.visibility = "visible";
          }
      
      }
      

      Here, we again use the this.target-nameTarget property to match our show target, which is attached to the <div> element with the sharks/all partial. We gave it a default style, "visibility:hidden", so in this method, we simply change the style to "visible". This will show the partial to users who have opted into seeing older posts.

      Below showAll, we’ll next add an upvote method, to allow users to “upvote” posts on the page by attaching the free Font Awesome check-circle icon to a particular post.

      Add the following code to define this method:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      . . . 
      export default class extends Controller {
      . . . 
      
          showAll() {
              this.showTarget.style.visibility = "visible";
          }
      
          upvote() {
              let post = event.target.closest(".post");
              post.insertAdjacentHTML('beforeend', '<i class="fa fa-check-circle"></i>');
          }
      
      }
      

      Here, we’re creating a post variable that will target the closest <li> element with the class post — the class we attached to each <li> element in our loop iteration in sharks/all. This will target the closest post and add the check-circle icon just inside <li> element, after its last child.

      Next, we’ll use a similar method to hide posts on the page. Add the following code below the upvote method to define a remove method:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      . . . 
      export default class extends Controller {
      . . . 
      
          upvote() {
              let post = event.target.closest(".post");
              post.insertAdjacentHTML('beforeend', '<i class="fa fa-check-circle"></i>');
          }
      
          remove() {
              let post = event.target.closest(".post");
              post.style.visibility = "hidden";
          }
      }
      

      Once again, our post variable will target the closest <li> element with the class post. It will then set the visibility property to "hidden" to hide the post on the page.

      The finished controller file will now look like this:

      ~/sharkapp/app/javascript/controllers/posts_controller.js

      import { Controller } from "stimulus"
      
      export default class extends Controller {
      
          static targets = ["body", "add", "show"]
      
          addBody() {
              let content = this.bodyTarget.value;
              this.addTarget.insertAdjacentHTML('beforebegin', "<li>" + content + "</li>");
          }
      
          showAll() {
              this.showTarget.style.visibility = "visible";
          }
      
          upvote() {
              let post = event.target.closest(".post");
              post.insertAdjacentHTML('beforeend', '<i class="fa fa-check-circle"></i>');
          }
      
          remove() {
              let post = event.target.closest(".post");
              post.style.visibility = "hidden";
          }
      } 
      

      Save and close the file when you are finished editing.

      With your Stimulus controller in place, you can move on to making some final changes to the index view and testing your application.

      Step 7 — Modifying the Index View and Testing the Application

      With one final change to the sharks index view you will be ready to test out your application. The index view is the root of the application, which you set in Step 4 of How To Build a Ruby on Rails Application.

      Open the file:

      • nano app/views/sharks/index.html.erb

      In place of the link_to helpers that were autogenerated for us to display and destroy sharks, we’ll use button_to helpers. This will help us work with generated HTML code instead of the default Rails JavaScript assets, which we specified we would no longer use in Step 1, when we changed javascript_include_tag to javascript_pack_tag in app/views/layouts/application.html.erb.

      Replace the existing link_to helpers in the file with the following button_to helpers:

      ~/sharkapp/app/views/sharks/index.html.erb

      . . . 
        <tbody>
          <% @sharks.each do |shark| %>
            <tr>
              <td><%= shark.name %></td>
              <td><%= shark.facts %></td>
              <td><%= button_to 'Show', shark_path(:id => shark.id), :method => :get %></td>
              <td><%= button_to 'Edit', edit_shark_path(:id => shark.id), :method => :get %></td>
              <td><%= button_to 'Destroy', shark_path(:id => shark.id), :method => :delete %></td>
            </tr>
          <% end %>
        </tbody>
      . . . 
      

      These helpers accomplish much the same things as their link_to counterparts, but the Destroy helper now relies on generated HTML rather than Rails’s default JavaScript.

      Save and close the file when you are finished editing.

      You are now ready to test your application.

      First, run your database migrations:

      Next, start your server. If you are working locally, you can do this with the following command:

      If you are working on a development server, you can start the application with:

      • rails s --binding=your_server_ip

      Navigate to the application landing page in your browser. If you are working locally, this will be localhost:3000, or http://your_server_ip:3000 if you are working on a server.

      You will see the following landing page:

      Application Landing Page

      Clicking on Show will take you to the show view for this shark. Here you will see a form to fill out a post:

      Shark Show Page

      In the post form, type “These sharks are scary!”:

      Filled in Post

      Click on Create Post. You will now see the new post on the page:

      New Post Added to Page

      You can add another new post, if you would like. This time, type “These sharks are often misrepresented in films” and click Create Post:

      Second Post Added to Page

      In order to test the functionality of the Show Older Posts feature, we will need to leave this page, since our Great White does not currently have any posts that are older than the ones we’ve just added.

      Click Back to get to the main page, and then Show to return to the Great White landing page:

      Shark Show Page

      Clicking on Show Older Posts will now show you the posts you created:

      Show Older Posts

      You can now upvote a post by clicking on the Upvote Post button:

      Upvote a Post

      Similarly, clicking Remove Post will hide the post:

      Remove a Post

      You have now confirmed that you have a working Rails application that uses Stimulus to control how nested post resources are displayed on individual shark pages. You can use this as the jumping off point for future development and experimentation with Stimulus.

      Conclusion

      Stimulus represents a possible alternative to working with rails-ujs, JQuery, and frameworks like React and Vue.

      As discussed in the introduction, Stimulus makes the most sense when you need to work directly with HTML generated by the server. It is lightweight, and aims to make code – particularly HTML – self-explanatory to the highest degree possible. If you don’t need to manage state on the client side, then Stimulus may be a good choice.

      If you are interested in how to create nested resources without a Stimulus integration, you can consult How To Create Nested Resources for a Ruby on Rails Application.

      For more information on how you would integrate React with a Rails application, see How To Set Up a Ruby on Rails Project with a React Frontend.



      Source link

      How To Create Nested Resources for a Ruby on Rails Application


      Introduction

      Ruby on Rails is a web application framework written in Ruby that offers developers an opinionated approach to application development. Working with Rails gives developers:

      • Conventions for handling things like routing, stateful data, and asset management.
      • A firm grounding in the model-view-controller (MCV) architectural pattern, which separates an application’s logic, located in models, from the presentation and routing of application information.

      As you add complexity to your Rails applications, you will likely work with multiple models, which represent your application’s business logic and interface with your database. Adding related models means establishing meaningful relationships between them, which then affect how information gets relayed through your application’s controllers, and how it is captured and presented back to users through views.

      In this tutorial, you will build on an existing Rails application that offers users facts about sharks. This application already has a model for handling shark data, but you will add a nested resource for posts about individual sharks. This will allow users to build out a wider body of thoughts and opinions about individual sharks.

      Prerequisites

      To follow this tutorial, you will need:

      • A local machine or development server running Ubuntu 18.04. Your development machine should have a non-root user with administrative privileges and a firewall configured with ufw. For instructions on how to set this up, see our Initial Server Setup with Ubuntu 18.04 tutorial.
      • Node.js and npm installed on your local machine or development server. This tutorial uses Node.js version 10.16.3 and npm version 6.9.0. For guidance on installing Node.js and npm on Ubuntu 18.04, follow the instructions in the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 18.04.
      • Ruby, rbenv, and Rails installed on your local machine or development server, following Steps 1-4 in How To Install Ruby on Rails with rbenv on Ubuntu 18.04. This tutorial uses Ruby 2.5.1, rbenv 1.1.2, and Rails 5.2.3.
      • SQLite installed, and a basic shark information application created, following the directions in How To Build a Ruby on Rails Application.

      Step 1 — Scaffolding the Nested Model

      Our application will take advantage of Active Record associations to build out a relationship between Shark and Post models: posts will belong to particular sharks, and each shark can have multiple posts. Our Shark and Post models will therefore be related through belongs_to and has_many associations.

      The first step to building out the application in this way will be to create a Post model and related resources. To do this, we can use the rails generate scaffold command, which will give us a model, a database migration to alter the database schema, a controller, a full set of views to manage standard Create, Read, Update, and Delete (CRUD) operations, and templates for partials, helpers, and tests. We will need to modify these resources, but using the scaffold command will save us some time and energy since it generates a structure we can use as a starting point.

      First, make sure that you are in the sharkapp directory for the Rails project that you created in the prerequisites:

      Create your Post resources with the following command:

      • rails generate scaffold Post body:text shark:references

      With body:text, we’re telling Rails to include a body field in the posts database table — the table that maps to the Post model. We’re also including the :references keyword, which sets up an association between the Shark and Post models. Specifically, this will ensure that a foreign key representing each shark entry in the sharks database is added to the posts database.

      Once you have run the command, you will see output confirming the resources that Rails has generated for the application. Before moving on, you can check your database migration file to look at the relationship that now exists between your models and database tables. Use the following command to look at the contents of the file, making sure to substitute the timestamp on your own migration file for what’s shown here:

      • cat db/migrate/20190805132506_create_posts.rb

      You will see the following output:

      Output

      class CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| t.text :body t.references :shark, foreign_key: true t.timestamps end end end

      As you can see, the table includes a column for a shark foreign key. This key will take the form of model_name_id — in our case, shark_id.

      Rails has established the relationship between the models elsewhere as well. Take a look at the newly generated Post model with the following command:

      Output

      class Post < ApplicationRecord belongs_to :shark end

      The belongs_to association sets up a relationship between models in which a single instance of the declaring model belongs to a single instance of the named model. In the case of our application, this means that a single post belongs to a single shark.

      In addition to setting this relationship, the rails generate scaffold command also created routes and views for posts, as it did for our shark resources in Step 3 of How To Build a Ruby on Rails Application.

      This is a useful start, but we will need to configure some additional routing and solidify the Active Record association for the Shark model in order for the relationship between our models and routes to work as desired.

      Step 2 — Specifying Nested Routes and Associations for the Parent Model

      Rails has already set the belongs_to association in our Post model, thanks to the :references keyword in the rails generate scaffold command, but in order for that relationship to function properly we will need to specify a has_many association in our Shark model as well. We will also need to make changes to the default routing that Rails gave us in order to make post resources the children of shark resources.

      To add the has_many association to the Shark model, open app/models/shark.rb using nano or your favorite editor:

      Add the following line to the file to establish the relationship between sharks and posts:

      ~/sharkapp/app/models/shark.rb

      class Shark < ApplicationRecord
        has_many :posts
        validates :name, presence: true, uniqueness: true
        validates :facts, presence: true
      end
      

      One thing that is worth thinking about here is what happens to posts once a particular shark is deleted. We likely do not want the posts associated with a deleted shark persisting in the database. To ensure that any posts associated with a given shark are eliminated when that shark is deleted, we can include the dependent option with the association.

      Add the following code to the file to ensure that the destroy action on a given shark deletes any associated posts:

      ~/sharkapp/app/models/post.rb

      class Shark < ApplicationRecord
        has_many :posts , dependent: :destroy
        validates :name, presence: true, uniqueness: true
        validates :facts, presence: true
      end
      

      Once you have finished making these changes, save and close the file. If you are using nano, you can do this by pressing CTRL+X, Y, then ENTER.

      Next, open your config/routes.rb file to modify the relationship between your resourceful routes:

      Currently, the file looks like this:

      ~/sharkapp/config/routes.rb

      Rails.application.routes.draw do
        resources :posts 
        resources :sharks
      
        root 'sharks#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      The current code establishes an independent relationship between our routes, when what we would like to express is a dependent relationship between sharks and their associated posts.

      Let’s update our route declaration to make :sharks the parent of :posts. Update the code in the file to look like the following:

      ~/sharkapp/config/routes.rb

      Rails.application.routes.draw do
        resources :sharks do
          resources :posts
        end
        root 'sharks#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      Save and close the file when you are finished editing.

      With these changes in place, you can move on to updating your posts controller.

      Step 3 — Updating the Posts Controller

      The association between our models gives us methods that we can use to create new post instances associated with particular sharks. To use these methods, we will need to add them our posts controller.

      Open the posts controller file:

      • nano app/controllers/posts_controller.rb

      Currently, the file looks like this:

      ~/sharkapp/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :set_post, only: [:show, :edit, :update, :destroy]
      
        # GET /posts
        # GET /posts.json
        def index
          @posts = Post.all
        end
      
        # GET /posts/1
        # GET /posts/1.json
        def show
        end
      
        # GET /posts/new
        def new
          @post = Post.new
        end
      
        # GET /posts/1/edit
        def edit
        end
      
        # POST /posts
        # POST /posts.json
        def create
          @post = Post.new(post_params)
      
          respond_to do |format|
            if @post.save
              format.html { redirect_to @post, notice: 'Post was successfully created.' }
              format.json { render :show, status: :created, location: @post }
            else
              format.html { render :new }
              format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      
        # PATCH/PUT /posts/1
        # PATCH/PUT /posts/1.json
        def update
          respond_to do |format|
            if @post.update(post_params)
              format.html { redirect_to @post, notice: 'Post was successfully updated.' }
              format.json { render :show, status: :ok, location: @post }
            else
              format.html { render :edit }
              format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      
        # DELETE /posts/1
        # DELETE /posts/1.json
        def destroy
          @post.destroy
          respond_to do |format|
            format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
            format.json { head :no_content }
          end
        end
      
        private
          # Use callbacks to share common setup or constraints between actions.
          def set_post
            @post = Post.find(params[:id])
          end
      
          # Never trust parameters from the scary internet, only allow the white list through.
          def post_params
            params.require(:post).permit(:body, :shark_id)
          end
      end
      

      Like our sharks controller, this controller’s methods work with instances of the associated Post class. For example, the new method creates a new instance of the Post class, the index method grabs all instances of the class, and the set_post method uses find and params to select a particular post by id. If, however, we want our post instances to be associated with particular shark instances, then we will need to modify this code, since the Post class is currently operating as an independent entity.

      Our modifications will make use of two things:

      • The methods that became available to us when we added the belongs_to and has_many associations to our models. Specifically, we now have access to the build method thanks to the has_many association we defined in our Shark model. This method will allow us to create a collection of post objects associated with a particular shark object, using the shark_id foreign key that exists in our posts database.
      • The routes and routing helpers that became available when we created a nested posts route. For a full list of example routes that become available when you create nested relationships between resources, see the Rails documentation. For now, it will be enough for us to know that for each specific shark — say sharks/1 — there will be an associated route for posts related to that shark: sharks/1/posts. There will also be routing helpers like shark_posts_path(@shark) and edit_sharks_posts_path(@shark) that refer to these nested routes.

      In the file, we’ll begin by writing a method, get_shark, that will run before each action in the controller. This method will create a local @shark instance variable by finding a shark instance by shark_id. With this variable available to us in the file, it will be possible to relate posts to a specific shark in the other methods.

      Above the other private methods at the bottom of the file, add the following method:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
      private
        def get_shark
          @shark = Shark.find(params[:shark_id])
        end
        # Use callbacks to share common setup or constraints between actions.
      . . . 
      

      Next, add the corresponding filter to the top of the file, before the existing filter:

      ~/sharkapp/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :get_shark
      

      This will ensure that get_shark runs before each action defined in the file.

      Next, you can use this @shark instance to rewrite the index method. Instead of grabbing all instances of the Post class, we want this method to return all post instances associated with a particular shark instance.

      Modify the index method to look like this:

      ~/sharkapp/controllers/posts_controller.rb

      . . .
        def index
          @posts = @shark.posts
        end
      . . .
      

      The new method will need a similar revision, since we want a new post instance to be associated with a particular shark. To achieve this, we can make use of the build method, along with our local @shark instance variable.

      Change the new method to look like this:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
        def new
          @post = @shark.posts.build
        end
      . . . 
      

      This method creates a post object that’s associated with the specific shark instance from the get_shark method.

      Next, we’ll address the method that’s most closely tied to new: create. The create method does two things: it builds a new post instance using the parameters that users have entered into the new form, and, if there are no errors, it saves that instance and uses a route helper to redirect users to where they can see the new post. In the case of errors, it renders the new template again.

      Update the create method to look like this:

      ~/sharkapp/controllers/posts_controller.rb

        def create
          @post = @shark.posts.build(post_params)
      
              respond_to do |format|
               if @post.save  
                  format.html { redirect_to shark_posts_path(@shark), notice: 'Post was successfully created.' }
                  format.json { render :show, status: :created, location: @post }
               else
                  format.html { render :new }
                  format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      

      Next, take a look at the update method. This method uses a @post instance variable, which is not explicitly set in the method itself. Where does this variable come from?

      Take a look at the filters at the top of the file. The second, auto-generated before_action filter provides an answer:

      ~/sharkapp/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :get_shark
        before_action :set_post, only: [:show, :edit, :update, :destroy]
        . . .
      

      The update method (like show, edit, and destroy) takes a @post variable from the set_post method. That method, listed under the get_shark method with our other private methods, currently looks like this:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
      private
      . . . 
        def set_post
          @post = Post.find(params[:id])
        end
      . . .
      

      In keeping with the methods we’ve used elsewhere in the file, we will need to modify this method so that @post refers to a particular instance in the collection of posts that’s associated with a particular shark. Keep the build method in mind here — thanks to the associations between our models, and the methods (like build) that are available to us by virtue of those associations, each of our post instances is part of a collection of objects that’s associated with a particular shark. So it makes sense that when querying for a particular post, we would query the collection of posts associated with a particular shark.

      Update set_post to look like this:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
      private
      . . . 
        def set_post
          @post = @shark.posts.find(params[:id])
        end
      . . .
      

      Instead of finding a particular instance of the entire Post class by id, we instead search for a matching id in the collection of posts associated with a particular shark.

      With that method updated, we can look at the update and destroy methods.

      The update method makes use of the @post instance variable from set_post, and uses it with the post_params that the user has entered in the edit form. In the case of success, we want Rails to send the user back to the index view of the posts associated with a particular shark. In the case of errors, Rails will render the edit template again.

      In this case, the only change we will need to make is to the redirect_to statement, to handle successful updates. Update it to redirect to shark_post_path(@shark), which will redirect to the index view of the selected shark’s posts:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
        def update
          respond_to do |format|
            if @post.update(post_params)
              format.html { redirect_to shark_post_path(@shark), notice: 'Post was successfully updated.' }
              format.json { render :show, status: :ok, location: @post }
            else
              format.html { render :edit }
              format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      . . .
      

      Next, we will make a similar change to the destroy method. Update the redirect_to method to redirect requests to shark_posts_path(@shark) in the case of success:

      ~/sharkapp/controllers/posts_controller.rb

      . . . 
        def destroy
          @post.destroy
           respond_to do |format|
            format.html { redirect_to shark_posts_path(@shark), notice: 'Post was successfully destroyed.' }
            format.json { head :no_content }
          end
        end
      . . .
      

      This is the last change we will make. You now have a posts controller file that looks like this:

      ~/sharkapp/controllers/posts_controller.rb

      class PostsController < ApplicationController
        before_action :get_shark
        before_action :set_post, only: [:show, :edit, :update, :destroy]
      
        # GET /posts
        # GET /posts.json
        def index
          @posts = @shark.posts
        end
      
        # GET /posts/1
        # GET /posts/1.json
        def show
        end
      
        # GET /posts/new
        def new
          @post = @shark.posts.build
        end
      
        # GET /posts/1/edit
        def edit
        end
      
        # POST /posts
        # POST /posts.json
        def create
          @post = @shark.posts.build(post_params)
      
              respond_to do |format|
               if @post.save  
                  format.html { redirect_to shark_posts_path(@shark), notice: 'Post was successfully created.' }
                  format.json { render :show, status: :created, location: @post }
               else
                  format.html { render :new }
                  format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      
        # PATCH/PUT /posts/1
        # PATCH/PUT /posts/1.json
        def update
          respond_to do |format|
            if @post.update(post_params)
              format.html { redirect_to shark_post_path(@shark), notice: 'Post was successfully updated.' }
              format.json { render :show, status: :ok, location: @post }
            else
              format.html { render :edit }
              format.json { render json: @post.errors, status: :unprocessable_entity }
            end
          end
        end
      
        # DELETE /posts/1
        # DELETE /posts/1.json
        def destroy
          @post.destroy
          respond_to do |format|
            format.html { redirect_to shark_posts_path(@shark), notice: 'Post was successfully destroyed.' }
            format.json { head :no_content }
          end
        end
      
        private
      
         def get_shark
           @shark = Shark.find(params[:shark_id])
         end
          # Use callbacks to share common setup or constraints between actions.
          def set_post
            @post = @shark.posts.find(params[:id])
          end
      
          # Never trust parameters from the scary internet, only allow the white list through.
          def post_params
            params.require(:post).permit(:body, :shark_id)
          end
      end
      

      The controller manages how information is passed from the view templates to the database and vice versa. Our controller now reflects the relationship between our Shark and Post models, in which posts are associated with particular sharks. We can move on to modifying the view templates themselves, which are where users will pass in and modify post information about particular sharks.

      Step 4 — Modifying Views

      Our view template revisions will involve changing the templates that relate to posts, and also modifying our sharks show view, since we want users to see the posts associated with particular sharks.

      Let’s start with the foundational template for our posts: the form partial that is reused across multiple post templates. Open that form now:

      • nano app/views/posts/_form.html.erb

      Rather than passing only the post model to the form_with form helper, we will pass both the shark and post models, with post set as a child resource.

      Change the first line of the file to look like this, reflecting the relationship between our shark and post resources:

      ~/sharkapp/views/posts/_form.html.erb

      <%= form_with(model: [@shark, post], local: true) do |form| %>
      . . . 
      

      Next, delete the section that lists the shark_id of the related shark, since this is not essential information in the view.

      The finished form, complete with our edits to the first line and without the deleted shark_id section, will look like this:

      ~/sharkapp/views/posts/_form.html.erb

      <%= form_with(model: [@shark, post], local: true) do |form| %>
        <% if post.errors.any? %>
          <div id="error_explanation">
            <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
      
            <ul>
            <% post.errors.full_messages.each do |message| %>
              <li><%= message %></li>
            <% end %>
            </ul>
          </div>
        <% end %>
      
        <div class="field">
          <%= form.label :body %>
          <%= form.text_area :body %>
        </div>
      
        <div class="actions">
          <%= form.submit %>
        </div>
      <% end %>
      

      Save and close the file when you are finished editing.

      Next, open the index view, which will show the posts associated with a particular shark:

      • nano app/views/posts/index.html.erb

      Thanks to the rails generate scaffold command, Rails has generated the better part of the template, complete with a table that shows the body field of each post and its associated shark.

      Much like the other code we have already modified, however, this template treats posts as independent entities, when we would like to make use of the associations between our models and the collections and helper methods that these associations give us.

      In the body of the table, make the following updates:

      First, update post.shark to post.shark.name, so that the table will include the name field of the associated shark, rather than identifying information about the shark object itself:

      ~/sharkapp/app/views/posts/index.html.erb

      . . . 
        <tbody>
          <% @posts.each do |post| %>
            <tr>
              <td><%= post.body %></td>
              <td><%= post.shark.name %></td>
      . . . 
      

      Next, change the Show redirect to direct users to the show view for the associated shark, since they will most likely want a way to navigate back to the original shark. We can make use of the @shark instance variable that we set in the controller here, since Rails makes instance variables created in the controller available to all views. We’ll also change the text for the link from Show to Show Shark, so that users will better understand its function.

      Update the this line to the following:

      ~/sharkapp/app/views/posts/index.html.erb

      . . . 
        <tbody>
          <% @posts.each do |post| %>
            <tr>
              <td><%= post.body %></td>
              <td><%= post.shark.name %></td>
              <td><%= link_to 'Show Shark', [@shark] %></td>
      

      In the next line, we want to ensure that users are routed the right nested path when they go to edit a post. This means that rather than being directed to posts/post_id/edit, users will be directed to sharks/shark_id/posts/post_id/edit. To do this, we’ll use the shark_post_path routing helper and our models, which Rails will treat as URLs. We’ll also update the link text to make its function clearer.

      Update the Edit line to look like the following:

      ~/sharkapp/app/views/posts/index.html.erb

      . . . 
        <tbody>
          <% @posts.each do |post| %>
            <tr>
              <td><%= post.body %></td>
              <td><%= post.shark.name %></td>
              <td><%= link_to 'Show Shark', [@shark] %></td>
              <td><%= link_to 'Edit Post', edit_shark_post_path(@shark, post) %></td>
      

      Next, let’s add a similar change to the Destroy link, updating its function in the string, and adding our shark and post resources:

      ~/sharkapp/app/views/posts/index.html.erb

      . . . 
        <tbody>
          <% @posts.each do |post| %>
            <tr>
              <td><%= post.body %></td>
              <td><%= post.shark.name %></td>
              <td><%= link_to 'Show Shark', [@shark] %></td>
              <td><%= link_to 'Edit Post', edit_shark_post_path(@shark, post) %></td>
              <td><%= link_to 'Destroy Post', [@shark, post], method: :delete, data: { confirm: 'Are you sure?' } %></td>
      

      Finally, at the bottom of the form, we will want to update the New Post path to take users to the appropriate nested path when they want to create a new post. Update the last line of the file to make use of the new_shark_post_path(@shark) routing helper:

      ~/sharkapp/app/views/posts/index.html.erb

      . . . 
      <%= link_to 'New Post', new_shark_post_path(@shark) %>
      

      The finished file will look like this:

      ~/sharkapp/app/views/posts/index.html.erb

      <p id="notice"><%= notice %></p>
      
      <h1>Posts</h1>
      
      <table>
        <thead>
          <tr>
            <th>Body</th>
            <th>Shark</th>
            <th colspan="3"></th>
          </tr>
        </thead>
      
        <tbody>
          <% @posts.each do |post| %>
            <tr>
              <td><%= post.body %></td>
              <td><%= post.shark.name %></td>
              <td><%= link_to 'Show Shark', [@shark] %></td>
              <td><%= link_to 'Edit Post', edit_shark_post_path(@shark, post) %></td>
              <td><%= link_to 'Destroy Post', [@shark, post], method: :delete, data: { confirm: 'Are you sure?' } %></td>
            </tr>
          <% end %>
        </tbody>
      </table>
      
      <br>
      
      <%= link_to 'New Post', new_shark_post_path(@shark) %>
      

      Save and close the file when you are finished editing.

      The other edits we will make to post views won’t be as numerous, since our other views use the form partial we have already edited. However, we will want to update the link_to references in the other post templates to reflect the changes we have made to our form partial.

      Open app/views/posts/new.html.erb:

      • nano app/views/posts/new.html.erb

      Update the link_to reference at the bottom of the file to make use of the shark_posts_path(@shark) helper:

      ~/sharkapp/app/views/posts/new.html.erb

      . . . 
      <%= link_to 'Back', shark_posts_path(@shark) %>
      

      Save and close the file when you are finished making this change.

      Next, open the edit template:

      • nano app/views/posts/edit.html.erb

      In addition to the Back path, we’ll update Show to reflect our nested resources. Change the last two lines of the file to look like this:

      ~/sharkapp/app/views/posts/edit.html.erb

      . . . 
      <%= link_to 'Show', [@shark, @post] %> |
      <%= link_to 'Back', shark_posts_path(@shark) %>
      

      Save and close the file.

      Next, open the show template:

      • nano app/views/posts/show.html.erb

      Make the following edits to the Edit and Back paths at the bottom of the file:

      ~/sharkapp/app/views/posts/edit.html.erb

      . . .
      <%= link_to 'Edit', edit_shark_post_path(@shark, @post) %> |
      <%= link_to 'Back', shark_posts_path(@shark) %>
      

      Save and close the file when you are finished.

      As a final step, we will want to update the show view for our sharks so that posts are visible for individual sharks. Open that file now:

      • nano app/views/sharks/show.html.erb

      Our edits here will include adding a Posts section to the form and an Add Post link at the bottom of the file.

      Below the Facts for a given shark, we will add a new section that iterates through each instance in the collection of posts associated with this shark, outputting the body of each post.

      Add the following code below the Facts section of the form, and above the redirects at the bottom of the file:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . .
      <p>
        <strong>Facts:</strong>
        <%= @shark.facts %>
      </p>
      
      <h2>Posts</h2>
      <% for post in @shark.posts %>
          <ul>
            <li><%= post.body %></li>
        </ul>
      <% end %>
      
      <%= link_to 'Edit', edit_shark_path(@shark) %> |
      . . . 
      

      Next, add a new redirect to allow users to add a new post for this particular shark:

      ~/sharkapp/app/views/sharks/show.html.erb

      . . .
      <%= link_to 'Edit', edit_shark_path(@shark) %> |
      <%= link_to 'Add Post', shark_posts_path(@shark) %> |
      <%= link_to 'Back', sharks_path %>
      

      Save and close the file when you are finished editing.

      You have now made changes to your application’s models, controllers, and views to ensure that posts are always associated with a particular shark. As a final step, we can add some validations to our Post model to guarantee consistency in the data that’s saved to the database.

      Step 5 — Adding Validations and Testing the Application

      In Step 5 of How To Build a Ruby on Rails Application, you added validations to your Shark model to ensure uniformity and consistency in the data that gets saved to the sharks database. We’ll now take a similar step to ensure guarantees for the posts database as well.

      Open the file where your Post model is defined:

      Here, we want to ensure that posts are not blank and that they don’t duplicate content other users may have posted. To achieve this, add the following line to the file:

      ~/sharkapp/app/models/post.rb

      class Post < ApplicationRecord
        belongs_to :shark
        validates :body, presence: true, uniqueness: true
      end
      

      Save and close the file when you are finished editing.

      With this last change in place, you are ready to run your migrations and test the application.

      First, run your migrations:

      Next, start your server. If you’re working locally, you can do so by running:

      If you are working on a development server, run the following command instead:

      • rails s --binding=your_server_ip

      Navigate to your application’s root at http://localhost:3000 or http://your_server_ip:3000.

      The prerequisite Rails project tutorial walked you through adding and editing a Great White shark entry. If you have not added any further sharks, the application landing page will look like this:

      Shark App Landing Page

      Click on Show next to the Great White’s name. This will take you to the show view for this shark. You will see the name of the shark and its facts, and a Posts header with no content. Let’s add a post to populate this part of the form.

      Click on Add Post below the Posts header. This will bring you to the post index view, where you will have the chance to select New Post:

      Post Index View

      Thanks to the authentication mechanisms you put in place in Step 6 of How To Build a Ruby on Rails Application, you may be asked to authenticate with the username and password you created in that Step, depending on whether or not you have created a new session.

      Click on New Post, which will bring you to your post new template:

      New Post

      In the Body field, type, “These sharks are scary!”

      New Shark Post

      Click on Create Post. You will be redirected to the index view for all posts that belong to this shark:

      Post Success

      With our post resources working, we can now test our data validations to ensure that only desired data gets saved to the database.

      From the index view, click on New Post. In the Body field of the new form, try entering “These sharks are scary!” again:

      Repeat Shark Post

      Click on Create Post. You will see the following error:

      Unique Post Error

      Click on Back to return to the main posts page.

      To test our other validation, click on New Post again. Leave the post blank and click Create Post. You will see the following error:

      Blank Post Error

      With your nested resources and validations working properly, you now have a working Rails application that you can use as a starting point for further development.

      Conclusion

      With your Rails application in place, you can now work on things like styling and developing other front-end components. If you would like to learn more about routing and nested resources, the Rails documentation is a great place to start.

      To learn more about integrating front-end frameworks with your application, take a look at How To Set Up a Ruby on Rails Project with a React Frontend.



      Source link