How to Build a simple CRUD Web Application using Grails 4

How to Build a simple CRUD Web Application using Grails 4

In this Grails 4 Tutorial, you'll learn how easy to build a simple create-read-update-delete (CRUD) web application using Grails 4 framework. How to creating simple create-read-update-delete (CRUD) web application step by step using Grails 4 framework. Easy to Build CRUD Web Application

In this Grails 4 tutorial, we will show you how easy to build a simple create-read-update-delete (CRUD) web application using Grails 4 framework. A few years ago we have shown you a similar tutorial for Grails 3. There are no significant changes in the Grails framework or Groovy usage between Grails 3 and Grails 4. But there are a lot of performance improvements, features, compatibility with the new technology, etc.

Table of Contents:

  • Step #1. Preparation
  • Step #2. Create and Configure a Grails 4 Web Application
  • Step #3. Create a Grails 4 Domain Class
  • Step #4. Generate Grails 4 Controllers and Views
  • Step #5. Run and Test Grails 4 CRUD Web Application

As you see in the table of contents, there are just a few steps to build a Grails 4 CRUD web application easily. We will use HSQLDB or H2 in-memory databases as datastore. So, there is not much configuration or required tools or libraries. The following tools, frameworks, and libraries are required for this tutorial:

  1. JDK 8
  2. Grails 4
  3. Terminal (Mac OS/Linux) or Command Line (Windows)
  4. IDE or Text Editor (We are using Visual Studio Code)

Let's get started with the main steps!

Step #1. Preparation

The principal of building Grails 4 application is installing Grails 4 and Groovy SDK which required JDK 8 installed on your machine. First, download and install the Java Development Kit (JDK) from the official Oracle download page. Next, make sure the JDK 8 is added to your environment variables or path. If JDK 8 already installed and added to the path, check it by open the terminal or command line then type this command.

java -version
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

Next, install the Grails 4 and Groovy SDK by downloading it from the official Grails download page. Extract or install it to your machine then add to the environment variables path. On Mac OS, we are using SDKMan to install it by simply run this command in the terminal.

sdk install grails

To check the installed Grails SDK, type this command.

grails -version
| Grails Version: 4.0.1
| JVM Version: 1.8.0_92

Now, Grails 4 is ready to create a new application.

Step #2. Create and Configure a Grails 4 Web Application

We will use Grails 4 CLI to create a new Grails 4 web application and Grails interactive console to create or generate all required components. Type this Grails command to create a new Grails 4 web application.

grails create-app com.djamware.grails-crud

CLI has created a new Grails 4 web application with the name grails-crud and package name com.djamware. Next, go to the newly created Grails application folder.

cd ./grails-crud

Type this CLI to enter the Grails 4 interactive console.

grails

In the Grails interactive console type this command to see all available commands.

help

Here they are the list of available Grails commands.

Usage (optionals marked with *):'
grails [environment]* [target] [arguments]*'

| Examples:
$ grails dev run-app
$ grails create-app books

| Available Commands (type grails help 'command-name' for more info):
| Command Name                          Command Description
----------------------------------------------------------------------------------------------------
assemble                                Creates a JAR or WAR archive for production deployment
bug-report                              Creates a zip file that can be attached to issue reports for the current project
clean                                   Cleans a Grails application's compiled sources
compile                                 Compiles a Grails application
console                                 Runs the Grails interactive console
create-command                          Creates an Application Command
create-controller                       Creates a controller
create-domain-class                     Creates a Domain Class
create-functional-test                  Creates a Geb Functional Test
create-integration-test                 Creates an integration test
create-interceptor                      Creates an interceptor
create-scaffold-controller              Creates a scaffolded controller
create-script                           Creates a Grails script
create-service                          Creates a Service
create-taglib                           Creates a Tag Library
create-unit-test                        Creates a unit test
dependency-report                       Prints out the Grails application's dependencies
generate-all                            Generates a controller that performs CRUD operations and the associated views
generate-async-controller               Generates an asynchronous controller that performs CRUD operations
generate-controller                     Generates a controller that performs CRUD operations
generate-views                          Generates GSP views for the specified domain class
gradle                                  Allows running of Gradle tasks
help                                    Prints help information for a specific command
install-templates                       Installs scaffolding templates that use f:all to render properties
list-plugins                            Lists available plugins from the Plugin Repository
open                                    Opens a file in the project
plugin-info                             Prints information about the given plugin
run-app                                 Runs a Grails application
run-command                             Executes Grails commands
run-script                              Executes Groovy scripts in a Grails context
schema-export                           Creates a DDL file of the database schema
shell                                   Runs the Grails interactive shell
stats                                   Prints statistics about the project
stop-app                                Stops the running Grails application
test-app                                Runs the applications tests
url-mappings-report                     Prints out a report of the project's URL mappings

| Detailed usage with help [command]
Step #3. Create a Grails 4 Domain Class

We will create 2 domain classes that related to each other. In the Grails interactive console, we can create the Grails domain classes/entities/models by type these commands.

create-domain-class com.djamware.Team
create-domain-class com.djamware.Player

The relationship between those domain classes is Team has many Players and players belongs to Team. Next, open and edit src/grails-app/domain/com/djamware/Team.groovy then add these fields or variables inside class body.

String name
String city
String stadium
String logo
String manager
static hasMany = [players: Player]

It has 5 fields/variables with String types and a relationship with Player domain class. Next, add the constraint that use as a basic field validation.

static constraints = {
    name size: 5..40, blank: false, unique: true
    city size: 5..30, blank: false
    stadium size: 5..30, blank: false
    logo size: 5..255, blank: false
    manager size: 5..30, blank: false
}

Add this method to display the name of the team instead of the class name when this domain called.

String toString() {
    name
}

Next, open and edit src/grails-app/domain/com/djamware/Player.groovy then add these lines of fields or variables inside the class body.

static belongsTo = [team: Team]
String name
Integer age
String position

It has 3 fields/variables with type String and Integer and a relationship with Team domain class. Next, add the constraint that uses as a basic field validation.

static constraints = {
    name size: 5..40, blank: false
    age min: 16
    position size: 5..40, blank: false
}

Add this method to display the name of the player instead of the class name when this domain called.

String toString() {
    name
}
Step #4. Generate Grails 4 Controllers and Views

In Grails, you don't need to create one by one for each controller and view. Just using a single command in the Grails interactive console, will generate all required controllers and views including their unit testing. Type this command in the Grails interactive console to generate all controllers and views.

generate-all com.djamware.Team
generate-all com.djamware.Player

Now, you have each controller and views for the Team and Player domain. Because we are using String for the logo field image URL, we need to modify the Show team view to show the logo image. Open and edit src/grails-app/views/team/Show.gsp then modify the <f:display bean="team" /> to be like this.

<f:display bean="team" except="logo" />

Add a line after that to display the image from the URL of logo data.

<g:img uri="${team?.logo}"/>
Step #5. Run and Test Grails 4 CRUD Web Application

Still in the Grails 4 interactive console, we will run this Grails 4 application from there. We don't need to run the database, because it's already embedded in this Grails application. Type this command to run it.

run-app

Open your favorite browser then go to http://localhost:8080 and you will see this Grails 4 web application.




That it's, Grails 4 Tutorial: Easy to Build CRUD Web Application. You can get the full working source code in our GitHub.

Thanks!

Originally published at https://www.djamware.com

How to build a secure Grails 4 Application using Spring Security Core

How to build a secure Grails 4 Application using Spring Security Core

In this Grails 4 tutorial, we will show you how to build a secure Grails 4 application using Spring Security Core Plugin. We will add the login and register function to the Grails 4 application.

In this Grails 4 tutorial, we will show you how to build a secure Grails 4 application using Spring Security Core Plugin. We will add the login and register function to the Grails 4 application. The purpose of using the Spring Security plugin has simplified the integration of Spring Security Java (we have written this tutorial). The usage of this Grails 4 Spring Security plugin similar to Grails 2 or 3, but there's a lot of updates on the Spring Security code and its dependencies to match the compatibilities.

Table of Contents:

The flow of this tutorial is very simple. We have a secure Product list that only accessible to the authorized user with ROLE_USER and Product CRUD for the user with ROLE_ADMIN. Any access to this Product resource will be redirected to the Login Page as default (if no authorized user). In the login page, there will be a link to the registration page that will register a new user.

The following tools, frameworks, libraries, and dependencies are required for this tutorial:

  1. JDK 8
  2. Grails 4
  3. Grails Spring Security Core Plugin
  4. Terminal or Command Line
  5. Text Editor or IDE

Before starting the main steps, make sure you have downloaded and installed the latest Grails 4. In Mac, we are using the SDKMan. For that, type this command in the Terminal to install SDKMan.

curl -s https://get.sdkman.io | bash

Follow all instructions that showed up during installation. Next, open the new Terminal window or tab then type this command.

source "$HOME/.sdkman/bin/sdkman-init.sh"

Now, you can install Grails 4 using SDKMan.

sdk install grails 4.0.1

Set that new Grails 4 as default. To check the Grails version, type this command.

grails -version
| Grails Version: 4.0.1
| JVM Version: 1.8.0_92
Create Grails 4 Application

Same as previous Grails version, to create a new Grails application, simply type this command.

grails create-app com.djamware.gadgethouse

That command will create a new Grails 4 application with the name "gadgethouse" with the package name "com.djamware". Next, go to the newly created project folder then enter the Grails 4 interactive console.

cd ./gadgethouse
grails

In the Grails interactive console, type this command to run this Grails application for the first time.

grails> run-app

Here's the new Grails 4 look like.

Install Grails Spring Security Core Plugin

Now, we will install and configure Grails Spring Security Core Plugin. For the database, we keep H2 in-memory database (You can change to other relational database configuration). To install the Grails Spring Security Core Plugin, open and edit build.gradle then add this dependency in dependencies array.

compile "org.grails.plugins:spring-security-core:4.0.0.RC2"

Next, stop the running Grails application using this command in the Grails interactive console.

stop-app

Compile the Grails application, to install the Spring Security Core plugin.

compile
Create User, Role, and Product Domain Class

We will use the Grails s2-quickstart command to create User and Role domain class for authentication. Type this command in Grails interactive console.

s2-quickstart com.djamware User Role

That command will create User and Role domain class with the package name "com.djamware". Next, open and edit grails-app/domain/com/djamware/Role.groovy to add default field after the bracket closing when this domain calls.

String toString() {
  authority
}

Next, create a domain class using regular Grails command for Product.

create-domain-class com.djamware.Product

Next, we need to create an additional field in the user domain class. For that, open and edit grails-app/domain/com/djamware/User.groovy then add a fullName field after the password field.

String fullname

Also, add a constraint for that field.

static constraints = {
    password nullable: false, blank: false, password: true
    username nullable: false, blank: false, unique: true
    fullname nullable: false, blank: false
}

Next, open and edit grails-app/domain/com/djamware/Product.groovy then replace that domain class with these Groovy codes.

package com.djamware

class Product {

    String prodCode
    String prodName
    String prodModel
    String prodDesc
    String prodImageUrl
    String prodPrice

    static constraints = {
        prodCode nullable: false, blank: false
        prodName nullable: false, blank: false
        prodModel nullable: false, blank: false
        prodDesc nullable: false, blank: false
        prodImageUrl nullable: true
        prodPrice nullable: false, blank: false
    }

    String toString() {
        prodName
    }
}
Create CustomUserDetailsService

Because we have added a field in the previous User domain class, we need to create a custom UserDetails. Create a new Groovy file src/main/groovy/com/djamware/CustomUserDetails.groovy then add these lines of Groovy codes that add full name field to the Grails UserDetails.

package com.djamware

import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority

class CustomUserDetails extends GrailsUser {

   final String fullname

   CustomUserDetails(String username, String password, boolean enabled,
                 boolean accountNonExpired, boolean credentialsNonExpired,
                 boolean accountNonLocked,
                 Collection<GrantedAuthority> authorities,
                 long id, String fullname) {
      super(username, password, enabled, accountNonExpired,
            credentialsNonExpired, accountNonLocked, authorities, id)

      this.fullname = fullname
   }
}

Next, type this command in the Grails interactive console to create a new Grails service.

grails> create-service com.djamware.CustomUserDetails

Open that file then replace all Groovy codes with these codes.

package com.djamware

import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import grails.gorm.transactions.Transactional
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException

class CustomUserDetailsService implements GrailsUserDetailsService {

   /**
    * Some Spring Security classes (e.g. RoleHierarchyVoter) expect at least
    * one role, so we give a user with no granted roles this one which gets
    * past that restriction but doesn't grant anything.
    */
   static final List NO_ROLES = [new SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]

   UserDetails loadUserByUsername(String username, boolean loadRoles)
         throws UsernameNotFoundException {
      return loadUserByUsername(username)
   }

   @Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException, UsernameNotFoundException])
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

      User user = User.findByUsername(username)
      if (!user) throw new NoStackUsernameNotFoundException()

      def roles = user.authorities

      // or if you are using role groups:
      // def roles = user.authorities.collect { it.authorities }.flatten().unique()

      def authorities = roles.collect {
         new SimpleGrantedAuthority(it.authority)
      }

      return new CustomUserDetails(user.username, user.password, user.enabled,
            !user.accountExpired, !user.passwordExpired,
            !user.accountLocked, authorities ?: NO_ROLES, user.id,
            user.fullname)
   }
}

Next, register that new CustomUserDetailsService in the grails-app/conf/spring/resources.groovy.

import com.djamware.UserPasswordEncoderListener
import com.djamware.CustomUserDetailsService
// Place your Spring DSL code here
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener)
    userDetailsService(CustomUserDetailsService)
}
Override Login Auth View

We will customize the login page to make UI better and add a link to the Register page. For that, create a login folder under views then create an auth.gsp file in that folder. Open and edit grails-app/views/login/auth.gsp then add these lines of GSP HTML tags.

<html>
<head>
    <meta name="layout" content="${gspLayout ?: 'main'}"/>
    <title><g:message code='springSecurity.login.title'/></title>
</head>

<body>
    <div class="row">
      <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
        <div class="card card-signin my-5">
          <div class="card-body">
            <h5 class="card-title text-center">Please Login</h5>
            <g:if test='${flash.message}'>
                <div class="alert alert-danger" role="alert">${flash.message}</div>
            </g:if>
            <form class="form-signin" action="${postUrl ?: '/login/authenticate'}" method="POST" id="loginForm" autocomplete="off">
              <div class="form-group">
                  <label for="username">Username</label>
                <input type="text" class="form-control" name="${usernameParameter ?: 'username'}" id="username" autocapitalize="none"/>
              </div>

              <div class="form-group">
                  <label for="password">Password</label>
                <input type="password" class="form-control" name="${passwordParameter ?: 'password'}" id="password"/>
                <i id="passwordToggler" title="toggle password display" onclick="passwordDisplayToggle()">&#128065;</i>
              </div>

              <div class="form-group form-check">
                  <label class="form-check-label">
                      <input type="checkbox" class="form-check-input" name="${rememberMeParameter ?: 'remember-me'}" id="remember_me" <g:if test='${hasCookie}'>checked="checked"</g:if>/> Remember me
                </label>
              </div>
              <button id="submit" class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Sign in</button>
              <hr class="my-4">
              <p>Don't have an account? <g:link controller="register">Register</g:link></p>
            </form>
          </div>
        </div>
      </div>
    </div>
    <script type="text/javascript">
        document.addEventListener("DOMContentLoaded", function(event) {
            document.forms['loginForm'].elements['username'].focus();
        });
        function passwordDisplayToggle() {
            var toggleEl = document.getElementById("passwordToggler");
            var eyeIcon = '\u{1F441}';
            var xIcon = '\u{2715}';
            var passEl = document.getElementById("password");
            if (passEl.type === "password") {
                toggleEl.innerHTML = xIcon;
                passEl.type = "text";
            } else {
                toggleEl.innerHTML = eyeIcon;
                passEl.type = "password";
            }
        }
    </script>
</body>
</html>

Next, we will make this login page as default or homepage when the application opens in the browser. For that, open and edit grails-app/controllers/UrlMappings.groovy then replace this line.

"/"(view: "index")

With this line.

"/"(controller:'login', action:'auth')
Add User Info and Logout to the Navbar

Now, we have to implement POST logout to the Navbar. This logout button active when the user logged in successfully along with user info. For that, modify grails-app/views/layout/main.gsp then replace all GSP HTML tags with these.

<!doctype html>
<html lang="en" class="no-js">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>
        <g:layoutTitle default="Grails"/>
    </title>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <asset:link rel="icon" href="favicon.ico" type="image/x-ico"/>

    <asset:stylesheet src="application.css"/>

    <g:layoutHead/>
</head>

<body>

<nav class="navbar navbar-expand-lg navbar-dark navbar-static-top" role="navigation">
    <a class="navbar-brand" href="/#"><asset:image src="grails.svg" alt="Grails Logo"/></a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" aria-expanded="false" style="height: 0.8px;" id="navbarContent">
        <ul class="nav navbar-nav ml-auto">
            <g:pageProperty name="page.nav"/>
            <sec:ifLoggedIn>
              <li class="nav-item dropdown">
                  <a class="nav-link dropdown-toggle" href="#" id="navbardrop" data-toggle="dropdown">
                    <sec:loggedInUserInfo field='fullname'/>
                  </a>
                  <div class="dropdown-menu navbar-dark">
                    <g:form controller="logout">
                      <g:submitButton class="dropdown-item navbar-dark color-light" name="Submit" value="Logout" style="color:gray" />
                    </g:form>
                  </div>
              </li>
            </sec:ifLoggedIn>
        </ul>
    </div>

</nav>

<div class="container">
    <g:layoutBody/>
</div>

<div class="footer row" role="contentinfo">
    <div class="col">
        <a href="http://guides.grails.org" target="_blank">
            <asset:image src="advancedgrails.svg" alt="Grails Guides" class="float-left"/>
        </a>
        <strong class="centered"><a href="http://guides.grails.org" target="_blank">Grails Guides</a></strong>
        <p>Building your first Grails app? Looking to add security, or create a Single-Page-App? Check out the <a href="http://guides.grails.org" target="_blank">Grails Guides</a> for step-by-step tutorials.</p>

    </div>
    <div class="col">
        <a href="http://docs.grails.org" target="_blank">
            <asset:image src="documentation.svg" alt="Grails Documentation" class="float-left"/>
        </a>
        <strong class="centered"><a href="http://docs.grails.org" target="_blank">Documentation</a></strong>
        <p>Ready to dig in? You can find in-depth documentation for all the features of Grails in the <a href="http://docs.grails.org" target="_blank">User Guide</a>.</p>

    </div>

    <div class="col">
        <a href="https://grails-slack.cfapps.io" target="_blank">
            <asset:image src="slack.svg" alt="Grails Slack" class="float-left"/>
        </a>
        <strong class="centered"><a href="https://grails-slack.cfapps.io" target="_blank">Join the Community</a></strong>
        <p>Get feedback and share your experience with other Grails developers in the community <a href="https://grails-slack.cfapps.io" target="_blank">Slack channel</a>.</p>
    </div>
</div>

<div id="spinner" class="spinner" style="display:none;">
    <g:message code="spinner.alt" default="Loading&hellip;"/>
</div>

<asset:javascript src="application.js"/>

</body>
</html>

As you see, there are built in Grails Spring Security TagLib sec:ifLoggedIn and <sec:loggedInUserInfo field='fullname'/>. The <sec:loggedInUserInfo field='fullname'/> only working when you implementing CustomUserDetailsService.

Create Register Controller and View

Back to the Grails interactive console to create a controller for the Register page.

grails> create-controller com.djamware.Register

Open and edit that Groovy file then replace all Groovy codes with these codes that have 2 methods of Register landing page and register action.

package com.djamware

import grails.validation.ValidationException
import grails.gorm.transactions.Transactional
import grails.plugin.springsecurity.annotation.Secured
import com.djamware.User
import com.djamware.Role
import com.djamware.UserRole

@Transactional
@Secured('permitAll')
class RegisterController {

    static allowedMethods = [register: "POST"]

    def index() { }

    def register() {
        if(!params.password.equals(params.repassword)) {
            flash.message = "Password and Re-Password not match"
            redirect action: "index"
            return
        } else {
            try {
                def user = User.findByUsername(params.username)?: new User(username: params.username, password: params.password, fullname: params.fullname).save()
                def role = Role.get(params.role.id)
                if(user && role) {
                    UserRole.create user, role

                    UserRole.withSession {
                      it.flush()
                      it.clear()
                    }

                    flash.message = "You have registered successfully. Please login."
                    redirect controller: "login", action: "auth"
                } else {
                    flash.message = "Register failed"
                    render view: "index"
                    return
                }
            } catch (ValidationException e) {
                flash.message = "Register Failed"
                redirect action: "index"
                return
            }
        }
    }
}

Next, add the index.gsp inside grails-app/views/register/. Open and edit that file then add these lines of GSP HTML tags.

<html>
<head>
    <meta name="layout" content="${gspLayout ?: 'main'}"/>
    <title>Register</title>
</head>

<body>
    <div class="row">
    <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
      <div class="card card-signin my-5">
        <div class="card-body">
          <h5 class="card-title text-center">Register Here</h5>
                    <g:if test='${flash.message}'>
                        <div class="alert alert-danger" role="alert">${flash.message}</div>
                    </g:if>
              <form class="form-signin" action="register" method="POST" id="loginForm" autocomplete="off">
                      <div class="form-group">
                          <label for="role">Role</label>
              <g:select class="form-control" name="role.id"
                    from="${com.djamware.Role.list()}"
                    optionKey="id" />
                </div>

            <div class="form-group">
                    <label for="username">Username</label>
              <input type="text" placeholder="Your username" class="form-control" name="username" id="username" autocapitalize="none"/>
            </div>

            <div class="form-group">
                          <label for="password">Password</label>
              <input type="password" placeholder="Your password" class="form-control" name="password" id="password"/>
            </div>

            <div class="form-group">
                          <label for="password">Re-Enter Password</label>
              <input type="password" placeholder="Re-enter password" class="form-control" name="repassword" id="repassword"/>
            </div>

                      <div class="form-group">
                          <label for="username">Full Name</label>
              <input type="text" placeholder="Your full name" class="form-control" name="fullname" id="fullname" autocapitalize="none"/>
            </div>

            <button id="submit" class="btn btn-lg btn-primary btn-block text-uppercase" type="submit">Register</button>
            <hr class="my-4">
            <p>Already have an account? <g:link controller="login" action="auth">Login</g:link></p>
          </form>
        </div>
      </div>
    </div>
  </div>
    <script type="text/javascript">
        document.addEventListener("DOMContentLoaded", function(event) {
            document.forms['loginForm'].elements['username'].focus();
        });
    </script>
</body>
</html>
Create the Secure Product CRUD Scaffolding

Now, we will make Product CRUD scaffolding and make them secured and accessible to ROLE_USER and ROLE_ADMIN. To create CRUD scaffolding, simply run this command inside Grails interactive console.

grails>generate-all com.djamware.Product

That command will generate controller, service, and view for a Product domain class. Next, open and edit grails-app/controllers/ProductController.groovy then add the Secure annotation like these.

package com.djamware

import grails.validation.ValidationException
import static org.springframework.http.HttpStatus.*
import grails.plugin.springsecurity.annotation.Secured

class ProductController {

    ProductService productService

    static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]

    @Secured(['ROLE_ADMIN', 'ROLE_USER'])
    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        respond productService.list(params), model:[productCount: productService.count()]
    }

    @Secured(['ROLE_ADMIN', 'ROLE_USER'])
    def show(Long id) {
        respond productService.get(id)
    }

    @Secured('ROLE_ADMIN')
    def create() {
        respond new Product(params)
    }

    @Secured('ROLE_ADMIN')
    def save(Product product) {
        if (product == null) {
            notFound()
            return
        }

        try {
            productService.save(product)
        } catch (ValidationException e) {
            respond product.errors, view:'create'
            return
        }

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.created.message', args: [message(code: 'product.label', default: 'Product'), product.id])
                redirect product
            }
            '*' { respond product, [status: CREATED] }
        }
    }

    @Secured('ROLE_ADMIN')
    def edit(Long id) {
        respond productService.get(id)
    }

    @Secured('ROLE_ADMIN')
    def update(Product product) {
        if (product == null) {
            notFound()
            return
        }

        try {
            productService.save(product)
        } catch (ValidationException e) {
            respond product.errors, view:'edit'
            return
        }

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'product.label', default: 'Product'), product.id])
                redirect product
            }
            '*'{ respond product, [status: OK] }
        }
    }

    @Secured('ROLE_ADMIN')
    def delete(Long id) {
        if (id == null) {
            notFound()
            return
        }

        productService.delete(id)

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.deleted.message', args: [message(code: 'product.label', default: 'Product'), id])
                redirect action:"index", method:"GET"
            }
            '*'{ render status: NO_CONTENT }
        }
    }

    protected void notFound() {
        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.not.found.message', args: [message(code: 'product.label', default: 'Product'), params.id])
                redirect action: "index", method: "GET"
            }
            '*'{ render status: NOT_FOUND }
        }
    }
}

Next, we will make Product controller as a default landing page after succeful login. For that, open and edit grails-app/conf/application.groovy then add this configuration.

grails.plugin.springsecurity.successHandler.defaultTargetUrl = '/product'
Run and Test Grails 4 Spring Security Core

Now, we will run and test the Grails 4 Spring Security Core application. In the Grails interactive console run this command.

grails>run-app

Open the browser then go to http://localhost:8080 and you will see this page.




That it's, the Grails 4 Tutorial: Spring Security Core Login Example. You can find the full working source code in our GitHub.

Thanks!

Step by step guide to creating a Web App in Angular 8

Step by step guide to creating a Web App in Angular 8

In this Angular 8 tutorial, you'll learn step by step guide to create a Web App in Angular 8. We will show you a basic and quick way to create an Angular web app. Step by step Angular 8 tutorial on how to create an Angular web app quickly.

In this Angular 8 tutorial, we will show you a basic and quick way to create an Angular web app. This tutorial does not just show a simple Hello world or a default Angular generated the page, but more than that we will show using the interaction with the array of objects data in the separate file. Also, we will show you how to use a basic Angular Material, Routing, and Navigation with the parameters.

Table of Contents:

  • Step #1. Preparation
  • Step #2. Create an Angular 8 Web App
  • Step #3. Create an Array of Objects File
  • Step #4. Install Angular Material
  • Step #5. Add Angular Routing and Navigation
  • Step #6. Show List of Data
  • Step #7. Show Data Details
  • Step #8. Show Google Maps
  • Step #9. Run and Test the Angular 8 Web App

The flow or this Angular web app is very simple. Display a list of Premier League teams like the shopping list which is clickable to the team details page. In the team details page, there's a link to display the position of the Team stadium. The following tools, frameworks, and libraries or modules are required for this tutorial.

  1. Node.js
  2. Angular 8
  3. Terminal (Linux/Mac) or Node Command Line (Windows)
  4. Text Editor or IDE (We use VSCode)

For now, we will not show you how to set up or get Google Maps Javascript API key. You can find another tutorial in this site that covers about Google Maps and related.

Let's get started with the main steps.

Step #1. Preparation

The preparation step means we will prepare all the required tools, frameworks, and modules to start creating Angular Web App from scratch. Now, we are creating an Angular 8 app using Angular CLI which needs Node.js and NPM to install or update it. Make sure you have to download and install Node.js (NPM included) before installing or updating Angular CLI. Check the installed Node.js and NPM using this command.

node -v
v10.15.1
npm -v
6.11.3

To install or update an Angular CLI type this command.

sudo npm install -g @angular/cli
Step #2. Create an Angular 8 Web App

We will create an Angular 8 web app using Angular CLI. Type this command to create the Angular 8 web app.

ng new angular-tutorial

That command will create a new Angular 8 app with the name angular-tutorial and pass all questions as 'Y' then the Angular CLI will automatically install the required NPM modules. After finished, go to the newly created Angular 8 folder then run the Angular 8 app for the first time.

cd ./angular-tutorial
ng serve --open

Using that "--open" parameters will automatically open the Angular 8 in your default web browser. Here's the Angular 8 default page look like.

Step #3. Create an Array of Objects File

As we mention in the first paragraph, we will display the list of the Premier League team data. For that, create a new Typescript file in src/app/team.ts.

touch src/app/team.ts

Open and edit that file then add these lines of Typescript constant of that contain array of objects.

export const Team = [
  { id: 1, name: 'Arsenal',    image: 'assets/logos/arsenal.png', location: 'London (Holloway)', stadium:    'Emirates Stadium',
  capacity:    60704, manager: 'Unai Emery', captain: 'Pierre-Emerick Aubameyang', lat: 51.554929, lng: -0.108449 },
  { id: 2, name: 'Aston Villa',    image: 'assets/logos/aston-villa.png', location:    'Birmingham', stadium: 'Villa Park',
  capacity: 42785, manager: 'Dean Smith', captain: 'Jack Grealish', lat: 52.509131, lng: -1.884767 },
  { id: 3, name: 'Bournemouth',    image: 'assets/logos/bornemouth.png',    location:    'Bournemouth', stadium:    'Dean Court',
  capacity: 11364, manager: 'Eddie Howe', captain: 'Simon Francis', lat: 50.735278, lng: -1.838290 },
  { id: 4, name: 'Brighton & Hove Albion',    image: 'assets/logos/brighton.png', location: 'Brighton', stadium:    'Falmer Stadium',
  capacity: 30666, manager: 'Graham Potter', captain: 'Lewis Dunk', lat: 50.861606, lng: -0.083716 },
  { id: 5, name: 'Burnley',    image: 'assets/logos/burnley.png',    location:    'Burnley', stadium:    'Turf Moor',
  capacity: 21944, manager: 'Sean Dyche', captain: 'Ben Mee', lat: 53.789017, lng: -2.230187 },
  { id: 6, name: 'Chelsea',    image: 'assets/logos/chelsea.png',    location:    'London (Fulham)', stadium:    'Stamford Bridge',
  capacity: 41631, manager: 'Frank Lampard', captain: 'César Azpilicueta', lat: 51.481696, lng: -0.190957 },
  { id: 7, name: 'Crystal Palace',    image: 'assets/logos/crystal-palace.png', location: 'London (Selhurst)', stadium: 'Selhurst Park',
  capacity: 26047, manager: 'Roy Hodgson', captain: 'Luka Milivojević', lat: 51.398281, lng: -0.085489 },
  { id: 8, name: 'Everton',    image: 'assets/logos/everton.png',    location:    'Liverpool (Walton)', stadium: 'Goodison Park',
  capacity: 39221, manager: 'Marco Silva', captain: 'Séamus Coleman', lat: 53.438813, lng: -2.966331 },
  { id: 9, name: 'Leicester City',    image: 'assets/logos/leicester.png', location: 'Leicester', stadium: 'King Power Stadium',
  capacity:    32312, manager: 'Brendan Rodgers', captain: 'Wes Morgan', lat: 52.620399, lng: -1.142147 },
  { id: 10, name: 'Liverpool',    image: 'assets/logos/liverpool.png', location: 'Liverpool (Anfield)', stadium: 'Anfield',
  capacity: 54074, manager: 'Jürgen Klopp', captain: 'Jordan Henderson', lat: 53.430846, lng: -2.960844 },
  { id: 11, name:    'Manchester City',    image: 'assets/logos/manchester-city.png', location: 'Manchester',
  stadium: 'City of Manchester Stadium', capacity: 55017, manager: 'Pep Guardiola', captain: 'David Silva',
  lat: 53.483176, lng: -2.200427 },
  { id: 12, name:    'Manchester United',    image: 'assets/logos/manchester-united.png', location: 'Manchester', stadium: 'Old Trafford',
  capacity: 74879, manager: 'Ole Gunnar Solskjær', captain: 'Ashley Young', lat: 53.463077, lng: -2.291334 },
  { id: 13, name:    'Newcastle United',    image: 'assets/logos/newcastle-united.png', location: 'Newcastle', stadium: 'St James Park',
  capacity: 52354, manager: 'Steve Bruce', captain: 'Jamaal Lascelles', lat: 54.975581, lng: -1.621661 },
  { id: 14, name:    'Norwich City',    image: 'assets/logos/norwich-city.png', location: 'Norwich', stadium:    'Carrow Road',
  capacity: 27244, manager: 'Daniel Farke', captain: 'Grant Hanley', lat: 52.622219, lng: 1.309328 },
  { id: 15, name:    'Sheffield United',    image: 'assets/logos/sheffield-united.png', location: 'Sheffield', stadium: 'Bramall Lane',
  capacity: 32702, manager: 'Chris Wilder', captain: 'Billy Sharp', lat: 53.370374, lng:  -1.470880 },
  { id: 16, name:    'Southampton',    image: 'assets/logos/southampton.png', location: 'Southampton', stadium: 'St Marys Stadium',
  capacity: 32384, manager: 'Ralph Hasenhüttl', captain: 'Pierre-Emile Højbjerg', lat: 50.905860, lng: -1.390941 },
  { id: 17, name:    'Tottenham Hotspur',    image: 'assets/logos/tottenham-hotspur.png', location: 'London (Tottenham)',
  stadium:    'Tottenham Hotspur Stadium', capacity: 62214, manager: 'José Mourinho', captain: 'Hugo Lloris', lat: 51.604319, lng: -0.066381 },
  { id: 18, name:    'Watford',    image: 'assets/logos/watford.png', location: 'Watford', stadium: 'Vicarage Road',
  capacity: 20400, manager: 'Quique Sánchez Flores', captain: 'Troy Deeney', lat: 51.649959, lng: -0.401525 },
  { id: 19, name:    'West Ham United',    image: 'assets/logos/westham-united.png', location: 'London (Stratford)', stadium: 'London Stadium',
  capacity: 60000, manager: 'Manuel Pellegrini', captain: 'Mark Noble', lat: 51.538750, lng: -0.016625 },
  { id: 20, name:    'Wolverhampton Wanderers',    image: 'assets/logos/wolverhampton.png', location: 'Wolverhampton',
  stadium: 'Molineux Stadium', capacity: 32050, manager: 'Nuno Espírito Santo', captain: 'Conor Coady', lat: 52.590301, lng: -2.130418 }
];

That array of objects file will call by Angular component using import. On that array of objects, there are the fields of image. So, copy the image of the Premier League team logo to the src/assets/logos/ after creating a logos folder. Make sure the file name the same as the name in the image fields.

Step #4. Install Angular Material

For the user interface/user experience (UI/UX) we will use Angular 8 Material and CDK. There's a CLI for generating a Material component like Table as a component, but we will create or add the Table component from scratch to the existing component. Type this command to install Angular 8 Material (@angular/material).

ng add @angular/material

If there are questions like below, just use the default answer.

? Choose a prebuilt theme name, or "custom" for a custom theme: Purple/Green       [ Preview: h
ttps://material.angular.io?theme=purple-green ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

We will register all required Angular 8 Material components or modules to src/app/app.module.ts. Open and edit that file then add these imports of required Angular Material Components.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatGridListModule } from '@angular/material';

Register the above modules to @NgModule imports.

imports: [
  BrowserModule,
  AppRoutingModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatGridListModule
],

Now, the Angular Material is ready to use in the Angular Component.

Step #5. Add Angular Routing and Navigation

The Angular 8 routes already added when we create a new Angular 8 application in the previous step. Routes use to navigate between components. Before configuring the routes, type this command to create a new Angular 8 components.

ng g component list
ng g component details
ng g component maps

Open src/app/app.module.ts then you will see those components imported and declared in @NgModule declarations. Next, open and edit src/app/app-routing.module.ts then add these imports.

import { ListComponent } from './list/list.component';
import { DetailsComponent } from './details/details.component';
import { MapsComponent } from './maps/maps.component';

Add these arrays to the existing routes constant that contain route for above-added components.

const routes: Routes = [
  {
    path: 'list',
    component: ListComponent,
    data: { title: 'List of Teams' }
  },
  {
    path: 'details/:id',
    component: DetailsComponent,
    data: { title: 'Team Details' }
  },
  {
    path: 'maps/:lat/:lng',
    component: MapsComponent,
    data: { title: 'Team Position' }
  },
  { path: '',
    redirectTo: '/list',
    pathMatch: 'full'
  }
];

Open and edit src/app/app.component.html and you will see the existing router outlet. Next, modify this HTML page to fit the list, details, or maps page.

<div class="container">
  <router-outlet></router-outlet>
</div>

Open and edit src/app/app.component.scss then replace all SASS codes with this.

.container {
  padding: 20px;
}
Step #6. Show List of Data

We will show list of teams data using Angular Material and component. First, we will need to load data from the array of objects file. Open and edit src/app/list/list.component.ts then add this import of Team array.

import { Team } from '../team';

Add the variables before the constructor to handle an array of objects.

teams = Team;

Next, open and edit src/app/list/list.component.html then add these lines of HTML tags that display the grid list from the array of objects.

<h1>English Premier League Teams 2019/2020</h1>
<mat-grid-list cols="4" rowHeight="400">
  <mat-grid-tile *ngFor="let team of teams" [routerLink]="['/details/', team.id]">
      <mat-card class="example-card">
        <mat-card-header>
          <mat-card-title>{{team.name}}</mat-card-title>
        </mat-card-header>
        <img mat-card-image src="{{team.image}}" alt="{{team.name}}">
        <mat-card-content>
          <p>
            {{team.location}}
          </p>
        </mat-card-content>
      </mat-card>
  </mat-grid-tile>
</mat-grid-list>

As you can see, the teams array is iterate on and display the information and image using . Finally, add a little style to src/app/list/list.component.scss with these SCSS codes.

.example-card {
  width: 200px;
}
Step #7. Show Data Details

From the list of teams, users can click the item of the grid then navigate to the details page with the ID params. The ID should receive in the details page using Angular ActivatedRoute. Open and edit src/app/details/details.component.ts then add these imports of Angular ActivatedRoute, Router, and Team array.

import { ActivatedRoute, Router } from '@angular/router';
import { Team } from '../team';

Inject the Angular ActivatedRoute and Router to the constructor.

constructor(public route: ActivatedRoute, public router: Router) {}

Add the variables before the constructor that hold details object and Team array.

details = { id: null, name: '',    image: '', location: '', stadium:    '', capacity:    null, manager: '', captain: '', lat: null, lng: null };
teams = Team;

Add a function to the Angular ngOnInit that finds an object from the team array.

ngOnInit() {
  if (this.route.snapshot.paramMap.get('id') !== 'null') {
    const id = parseInt(this.route.snapshot.paramMap.get('id'), 0);
    this.details = this.teams.find(x => x.id === id);
  }
}

We use Angular ngOnInit because want to load the details from the array every detail component showed up. Next, add a function to navigate to the maps component to show the Google Maps with the latitude and longitude params.

showMap(lat: any, lng: any) {
  this.router.navigate(['/maps', lat, lng]);
}

Next, open and edit src/app/details/details.component.html then replace all Angular HTML templates with these.

<mat-card class="example-card">
  <mat-card-header>
    <mat-card-title>{{details.name}}</mat-card-title>
    <mat-card-subtitle>{{details.location}}</mat-card-subtitle>
  </mat-card-header>
  <img mat-card-image src="{{details.image}}" alt="{{details.name}}">
  <mat-card-content>
    <dl>
      <dt>Stadium</dt>
      <dd>{{details.stadium}}</dd>
      <dt>Stadium Capacity:</dt>
      <dd>{{details.capacity}}</dd>
      <dt>Manager</dt>
      <dd>{{details.manager}}</dd>
      <dt>Captain</dt>
      <dd>{{details.captain}}</dd>
    </dl>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button (click)="showMap(details.lat, details.lng)">Show Location</button>
  </mat-card-actions>
</mat-card>

Finally, add a little style to src/app/details/details.component.scss with these SCSS codes.

.example-card {
  max-width: 400px;
}
Step #8. Show Google Maps

You can use your own or someone else Google Maps API Key to implementing Google Maps on the web page. To display the Google Maps, first, we need to load the Google Maps Javascript API by open and edit src/index.html then add these lines of Google Maps Javascript before the closing of the Body tag.

<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBgZZK8umUqJn8d5CoIZqWPJ_qtMfqD9q0"></script>

Next, open and edit src/app/maps/maps.component.html then add or modify these imports of Angular ViewChild, ElementRef, and ActivatedRoute.

import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

Inject the Angular ActivatedRoute to the constructor.

constructor(public route: ActivatedRoute) { }

Declare the basic required Google maps variables (map, marker, options) before the Angular @Component.

declare var google: any;
let map: any;
let marker: any;
const options = {
  enableHighAccuracy: true,
  timeout: 5000,
  maximumAge: 0
};
let infowindow: any;
const iconBase = 'http://maps.google.com/mapfiles/ms/icons/';

Load map element, after the title variable.

@ViewChild('map', {static: false}) mapElement: ElementRef;

Create a function to load Google Maps and set a default marker of the current browser location.

initMap(latitude: any, longitude: any) {
  navigator.geolocation.getCurrentPosition((location) => {
    map = new google.maps.Map(this.mapElement.nativeElement, {
      center: { lat: latitude, lng: longitude },
      zoom: 15
    });

    infowindow = new google.maps.InfoWindow();

    marker = new google.maps.Marker({
      position: { lat: latitude, lng: longitude },
      map,
      title: 'Click to zoom',
      icon: iconBase + 'blue-dot.png'
    });
  }, (error) => {
    console.log(error);
  }, options);
}

Call that function inside the constructor body with the params of latitude and longitude from the previous component.

constructor(public route: ActivatedRoute) {
  this.initMap(parseInt(this.route.snapshot.paramMap.get('lat'), 0), parseInt(this.route.snapshot.paramMap.get('lng'), 0));
}

Next, open and edit src/app/maps/maps.component.html then replace all HTML tags with just this map

.

<div #map id="map"></div>

Finally, to make Google Maps working in the Angular page, add a height of the

to the src/app/maps/maps.component.scss.

#map {
  height: 690px;
}
Step #9. Run and Test the Angular 8 Web App

As usual, to run the Angular 8 web app simply type this command.

ng serve --open

The Angular web app will open automatically in the default browser. The whole Angular web app will look like this.



That it's, the Angular 8 Tutorial: How to Create an Angular Web App Quickly. You can find the full source code from our GitHub.

Thanks!

How to build a React Web App with Ionic?

How to build a React Web App with Ionic?

With the latest version of the Ionic framework, which lets you write mobile apps with web technologies, you can use React to build your mobile apps. In this React tutorial, you'll learn how to build a React web app with Ionic.

With the latest version of the Ionic framework, which lets you write mobile apps with web technologies, you can use React to build your mobile apps. Versions previous to 4 only has components for Angular, but now React support is complete. It has all the components for building apps to your liking.

Ionic 4 and later is a mobile app framework and a component library. You can build mobile apps, progressive web apps, and normal web apps. The component library can be used on its own. Of course, the hardware support remains in place for you to use if you wish. Right now, the React version of Ionic can only be used to build web apps. Only the Angular version can be built into a mobile app.

The full reference for the React version of Ionic is at https://ionicframework.com/docs.

In this article, we will build a React web app with Ionic that does currency conversion. The home page will display the list of the latest exchange rates and another page will have a form to convert the currencies of your choice. Tp get the data we use the Foreign exchange rates API, located at https://exchangeratesapi.io/ to get the exchange rates, and we use the Open Exchange Rates API, located at https://openexchangerates.org/, to get the list of currencies.

To start, we will start by installing the Ionic CLI. We run:

npm install -g [email protected]

to install the latest version to your computer. Next we run:

ionic start currency-converter --type=react

then select the Sidenav option to create an Ionic project with a left side bar.

Next we install our own packages. We need Axios to make HTTP requests and MobX for state management. Run npm i axios mobx mobx-react in our project folder to install them.

Now we are ready to create some pages. In the pages folder, create ConvertCurrencyPage.jsx and add:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonList,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonInput,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonButton
} from "@ionic/react";import React from "react";
import { observer } from "mobx-react";
import { getExchangeRates } from "../requests";const ConvertCurrencyPage = ({ currenciesStore }) => {
  const [fromCurrencies, setFromCurrencies] = React.useState({});
  const [toCurrencies, setToCurrencies] = React.useState({});
  const [values, setValues] = React.useState({ amount: 0 } as any);
  const [submitting, setSubmitting] = React.useState(false);
  const [toAmount, setToAmount] = React.useState(0);const convertCurrency = async () => {
    setSubmitting(true);
    if (values.amount <= 0 || !values.from || !values.to) {
      return;
    }
    const { data } = await getExchangeRates(values.from);
    const rate = data.rates[values.to];
    setToAmount(values.amount * rate);
  };React.useEffect(() => {
    const fromCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.to) {
        fromCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setFromCurrencies(fromCurrencies);const toCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.from) {
        toCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setToCurrencies(toCurrencies);
  }, [currenciesStore.currencies, values.from, values.to]);return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Convert Currency</IonTitle>
        </IonToolbar>
      </IonHeader><IonContent>
        <IonList lines="none">
          <IonItem>
            <IonInput
              type="number"
              value={values.amount}
              color={!values.amount && submitting ? "danger" : undefined}
              min="0"
              onIonChange={ev =>
                setValues({ ...values, amount: (ev.target as any).value })
              }
            ></IonInput>
          </IonItem><IonItem>
            <IonLabel>Currency to Convert From</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.from && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, from: (ev.target as any).value })
              }
            >
              {Object.keys(fromCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(fromCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem><IonItem>
            <IonLabel>Currency to Convert To</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.to && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, to: (ev.target as any).value })
              }
            >
              {Object.keys(toCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(toCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem><IonItem>
            <IonButton size="default" fill="solid" onClick={convertCurrency}>
              Convert
            </IonButton>
          </IonItem>{toAmount ? (
            <IonItem>
              {values.amount} {values.from} is {toAmount} {values.to}.
            </IonItem>
          ) : (
            undefined
          )}
        </IonList>
      </IonContent>
    </IonPage>
  );
};export default observer(ConvertCurrencyPage);

to add a form to convert currency from one to another. In this form, we filter by choices by excluding from the currency being converted to from the first drop down and exclude the currency being converted from in the second drop down. Also we have an ion-input for the amount being converted. We get the values of the currency from the currenciesStore , which is the MobX store that gets the list of currencies from. In the IonSelect components we set the onIonChange props to handler functions which set the drop down’s values. We also set the placeholder for all the inputs and selects. In the IonInput component, we do the same with the onIonChange handler. We show the right value by using the variables set in the values object during change events.

When the user clicks Convert, we run the convertCurrency function. We check if the values are set correctly before running the rest of the code. If that succeeds, then we run the getExchangeRates function imported from requests.js , then we set the final toAmount by multuplying the rate with the amount.

The useEffect callback is used for excluding the currency to convert from from the currency to convert to list and vice versa. The array in the second argument of useEffect specifies which values to watch for.

The observer function in the last line is for designating the component to watch the latest values from the MobX stores.

Next in Home.tsx , we replace the existing code with:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonLabel,
  IonList,
  IonListHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonSelect,
  IonSelectOption
} from "@ionic/react";
import React from "react";
import "./Home.css";
import { getExchangeRates } from "../requests";
import { CurrencyStore } from "../stores";
import { observer } from "mobx-react";const HomePage = ({ currencyStore, currenciesStore }) => {
  const [rates, setRates] = React.useState({});const getRates = async () => {
    const { data } = await getExchangeRates(
      (currencyStore as CurrencyStore).currency
    );
    setRates(data.rates);
  };React.useEffect(() => {
    getRates();
  }, [(currencyStore as CurrencyStore).currency]);
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonListHeader>Latest Exchange Rates</IonListHeader>          <IonItem>
            <IonLabel>Currency</IonLabel>
            <IonSelect
              placeholder="Select One"
              onIonChange={ev => {
                (currencyStore as CurrencyStore).setCurrency(
                  ev.target && (ev.target as any).value
                );
              }}
            >
              {Object.keys(currenciesStore.currencies).map(key => {
                return (
                  <IonSelectOption
                    value={key}
                    key={key}
                    selected={
                      (currencyStore as CurrencyStore).currency
                        ? key == (currencyStore as CurrencyStore).currency
                        : key == "AUD"
                    }
                  >
                    {(currenciesStore.currencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
        </IonList>        <IonList lines="none">
          <IonListHeader>Exchange Rates</IonListHeader>
          {Object.keys(rates).map(key => {
            console.log(rates);
            return (
              <IonItem>
                {key}: {rates[key]}
              </IonItem>
            );
          })}
        </IonList>
      </IonContent>
    </IonPage>
  );
};export default observer(HomePage);

In this file, we display the exchange rates from an API. We get the currencies from the currenciesStore so that users can see exchange rates based on different currencies. The items are display in an IonList provided by Ionic.

The observer function in the last line is for designating the component to watch the latest values from the MobX stores.

Next in App.tsx , replace the following code with:

import React from "react";
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { AppPage } from "./declarations";import Menu from "./components/Menu";
import Home from "./pages/Home";
import { home, cash } from "ionicons/icons";/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";/* Theme variables */
import "./theme/variables.css";
import ConvertCurrencyPage from "./pages/ConvertCurrencyPage";
import { CurrencyStore, CurrenciesStore } from "./stores";
import { getCurrenciesList } from "./requests";const currencyStore = new CurrencyStore();
const currenciesStore = new CurrenciesStore();const appPages: AppPage[] = [
  {
    title: "Home",
    url: "/home",
    icon: home
  },
  {
    title: "Convert Currency",
    url: "/convertcurrency",
    icon: cash
  }
];const App: React.FC = () => {
  const [initialized, setInitialized] = React.useState(false);  const getCurrencies = async () => {
    const { data } = await getCurrenciesList();
    currenciesStore.setCurrencies(data);
    setInitialized(true);
  };React.useEffect(() => {
    if (!initialized) {
      getCurrencies();
    }
  });  return (
    <IonApp>
      <IonReactRouter>
        <IonSplitPane contentId="main">
          <Menu appPages={appPages} />
          <IonRouterOutlet id="main">
            <Route
              path="/home"
              render={() => (
                <Home
                  currencyStore={currencyStore}
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route
              path="/convertcurrency"
              render={() => (
                <ConvertCurrencyPage
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route exact path="/" render={() => <Redirect to="/home" />} />
          </IonRouterOutlet>
        </IonSplitPane>
      </IonReactRouter>
    </IonApp>
  );
};export default App;

We modified the routes so that we use the render prop instead of the component prop since we want to pass in our MobX stores into the components. The stores store the currently selected currency for the home page and the list of currencies for both pages.

We get the list of currencies here for both components and set the data in store for both to retrieve by setting it in store.

Next create requests.ts in the src folder and add:

const axios = require('axios');
const APIURL = 'https://api.exchangeratesapi.io';
const OPEN_EXCHANGE_RATES_URL = 'http://openexchangerates.org/api/currencies.json';

export const getExchangeRates = (baseCurrency: string) => axios.get(`${APIURL}/latest?base=${baseCurrency}`)

export const getCurrenciesList = () => axios.get(OPEN_EXCHANGE_RATES_URL)

These are the code for making the HTTP requests to get the currencies and exchange rates.

Next create store.ts to and add:

import { observable, action } from "mobx";class CurrencyStore {
    @observable currency: string = 'AUD';    @action setCurrency(currency: string) {
        this.currency = currency;
    }
}class CurrenciesStore {
    @observable currencies = {};    @action setCurrencies(currencies) {
        this.currencies = currencies;
    }
}export { CurrencyStore, CurrenciesStore };

The CurrencyStore is for storing the selected currency in the home page to show the exchange rates based on the selected currency. currency is the value observed by the home page and setCurrency sets the currency value. Similarly, CurrenciesStore stores a list of currencies retrieved in App.tsx where setCurrencies is called.

Next in tsconfig.json , we replace the existing code with:

{
  "compilerOptions": {
      "experimentalDecorators": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "noImplicitAny": false
  },
  "include": [
    "src"
  ]
}

to noImplicitAny and set it to true .

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Currency Converter</title><base href="/" /><meta
      name="viewport"
      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <meta name="format-detection" content="telephone=no" />
    <meta name="msapplication-tap-highlight" content="no" /><link
      rel="shortcut icon"
      type="image/png"
      href="%PUBLIC_URL%/assets/icon/favicon.png"
    /><!-- add to homescreen for ios -->
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-title" content="Ionic App" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
  </head><body>
    <div id="root"></div>
  </body>
</html>

to change the title.

After all the hard work is done, we get: