The training document to download here.
The installation process supposes to download and install a list of software. You will find an installation process bases on Windows OS, but same installation could be done with Linux and macOS just correct the path.
NOTE: I used a special folder call c:\DEV23\ more convenience with Windows OS with no space, no special characters and no path length limitation.
Software programs :
- Oracle Java JDK 17 : https://www.oracle.com/java/technologies/downloads/#java17 into C:\DEV23\Java\jdk-17.0.2
- Apache Maven 3.9 to install into c:\DEV23\apache-maven-3.9.4-bin\ folder
- Vscode 1.9 zip download for standalone version into c:\DEV23\VSCode-win32-x64-1.82.0\
- MySQL 8 Community ZIP archive for standalone version into C:\DEV23\mysql-8.1.0-winx64\
Installation standalone
We install software into a standalone version to avoid conflict with other development. It is also close to a production installation with docker or any other solution.
Java JDK installation is a common install, we will just use an environment variable to manage the JDK version used.
Maven installation in standalone version is just unzip into the correct folder.
> Mysql standalone
Mysql installation is just unzipping into a folder, but you have to make some configuration a command line the first time to configure.
- Unzip Mysql into c:\DEV23\mysql-8.1.0-winx64\ folder
- Create a config.ini text file into c:\DEV23\mysql-8.1.0-winx64\ with content show below
- Create an initialize.cmd text file into c:\DEV23\mysql-8.1.0-winx64\ with content show below
We suppose the port 12345 is available, but you can change to another one. You can add a root user password, but default no password is more convenience for development.
Content of config.ini file :
[mysqld] sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES # set basedir to your installation path basedir = ".\\" # set datadir to the location of your data directory datadir = ".\\mydb" # The port number to use when listening for TCP/IP connections. On Unix and Unix-like systems, the port number must be # 1024 or higher unless the server is started by the root system user. port = "12345" # Log errors and startup messages to this file. log-error = ".\\error_log.err" secure_file_priv = ".\\" [mysqladmin] user = "root" port = "12345"
Content of initialize.cmd file, to start one time to create initial database and configuration.
".\bin\mysqld.exe" --defaults-file=".\\config.ini" --initialize-insecure --console
You will see some lines as following to show MySql system databases are created and a root user with an empty password is also added :
[Warning] [MY-010915] [Server] 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE' and 'ERROR_FOR_DIVISION_BY_ZERO' sql modes should be used with strict mode. They will be merged with strict mode in a future release.
[System] [MY-013169] [Server] C:\Dev\mysql-8.0.39-winx64\bin\mysqld.exe (mysqld 8.0.39) initializing of server in progress as process 25940
[System] [MY-013576] [InnoDB] InnoDB initialization has started.
[System] [MY-013577] [InnoDB] InnoDB initialization has ended.
[Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
Then, if everything is ok, you can create 3 text files start.cmd, stop.cmd and mysql.cmd. I use the windows command cd « %~dp0 » to move from any folder into the folder where the script is stored. The Linux bash equivalence is cd $(dirname « $0 ») .
Content of start.cmd :
cd "%~dp0" .\bin\mysqld.exe --defaults-file=".\\config.ini"
Content of stop.cmd :
cd "%~dp0" .\bin\mysqladmin.exe --defaults-file=".\\config.ini" shutdown
Content of mysql.cmd :
cd "%~dp0" .\bin\mysql.exe --port=12345 --user=root --password
We could start MySQL server with start.cmd at the start of a development session and access to the MySQL client software with mysql.cmd.
Create the todo database and todo mysql user with the following SQL commands :
CREATE DATABASE todo; CREATE USER 'todo'@'localhost' IDENTIFIED WITH mysql_native_password BY 'todo'; GRANT all on todo.* TO 'todo'@'localhost'; flush privileges;
NOTE: The new version of MySql do not use mysql_native_password so the line will be :
CREATE USER 'todo'@'localhost' IDENTIFIED BY 'todo';
NOTE2 : a confirmation from Windows Defender firewall could be display, but you can cancel this confirmation, the local connection from Spring to the database will ignore the firewall.
You can find the related rules (one for TCP one for UDP protocol) into the firewall mangament software input rules :
But your mysql.cmd script will work to prove this rule is for incoming network TCP/UDP connection and not for the localhost connexion.
> VScode standalone
To use VSCode as a standalone with environment variable set by default, I create a code.cmd text file script with the following content :
cd "%~dp0"
SET VSCODE_PATH=VSCode-win32-x64-1.82.2
SET MAVEN_PATH=apache-maven-3.9.4-bin
SET JAVA_HOME=C:\DEV23\Java\jdk-17.0.2
SET MYSQL_PATH=mysql-8.1.0-winx64
SET WORKSPACE_PATH=Workspace-vscode-192.0
SET PATH=%PATH%;%cd%\%MAVEN_PATH%\bin;
if not exist .\%VSCODE_PATH%\extensions-dir\ mkdir .\%VSCODE_PATH%\extensions-dir
if not exist .\%VSCODE_PATH%\data\ mkdir .\%VSCODE_PATH%\data\
if not exist .\%WORKSPACE_PATH% mkdir .\%WORKSPACE_PATH%\
cd .\%WORKSPACE_PATH%
..\%VSCODE_PATH%\code.exe --extensions-dir=..\%VSCODE_PATH%\extensions-dir\ --user-data-dir=..\%VSCODE_PATH%\data\ .
cd ..
The script create environment variables of each software, add maven to the default PATH, create extensions-dir and data into VSCode subfolder, create a Workspace-vscode folder, start VScode.
You can add MySQL start and stop scripts to manage everything into one place.
1/ Use Spring Initializr
Open the https://start.spring.io/ website to create the template application with compatibles configurations. We will Maven and Java, the add a group/package name with Jar, JDK 17 settings. More important, we include Spring Web, Spring Data JPA and MySQL Drivers dependencies into our configuration :
Then press the « Generate » button to download the todo.zip file created, and unzip it into the c:\DEV23\Workspace-vscode\todo\ folder (but avoid c:\DEV23\Workspace-vscode\todo\todo\ folder ).
2/ Use VScode first time
Start VScode with the code.cmd script for the first time, so you have to confirm you trust the Workspace folder prior to anything:
Then you will have to install plugins for development with Java and Spring boot, then press « Extensions » button at the left :
The « spring boot extension pack » is the main extension, and it will trigger install of several dependent other plugins.
Then you need also « Extension pack for Java »
Then you can open the « Explorer » by clicking on the top left menu icon. We will use most of the time the « Java Project » area more than the « Explorer » part, so increase the size of this area :
Then we must use a terminal for severals commands, you can open the Command Palette with CTRL+SHIFT+P and enter View: Toggle Terminal.
Or Menu View > Terminal or an other shortcut as CTRL+ù (french version) or CTRL+` (US version). We start with a basic command to check the Maven and the JDK installation are OK:
mvn --version
The output should display something close to :
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9) Maven home: C:\Dev23\apache-maven-3.9.4-bin Java version: 17.0.2, vendor: Oracle Corporation, runtime: C:\Dev23\Java\jdk-17.0.2 Default locale: fr_FR, platform encoding: Cp1252 OS name: "windows 11", version: "10.0", arch: "amd64", family: "windows"
Maven is located into the PATH environment variable, JDK is also found with JAVA_HOME environment variable, so with this message everything is OK. Do not start VS Code from a start menu or from directly the executable, start VS Code with the start.cmd script to provide variable.
NOTE: you can change the default terminal location with the settings : terminal.integrated.cwd. Two ways to changes :
- Edit the file settings.json into Vscode sub-folders: ./data/user-data/User/ with the following settings (unfortunatly no variables) :
« terminal.integrated.cwd »: « C:/Dev/Workspace-vscode-192.0/todo/ », - Or via Menu File > Preferences > Settings and search « terminal.integrated.cwd » and sets the same folder path : C:/Dev/Workspace-vscode-192.0/todo/
3/ Maven and Spring configuration
Into the terminal change folder to be into todo directory with cd todo command and you could also update the code.cmd script to be into this directory by default. NOT SURE IS IT WORKING ?
Into the terminal, start the mvn install command to rescue jar and install everything, but you will have an error. In short, this line explains there is no data source find
o.s.w.c.s.GenericWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource
So we will have to create a text file application.properties into src/main/ressources folder ith the following content (you can let the initial line is already there : spring.application.name=todo) :
spring.application.name=todo
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:12345/todo?zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=UTC
spring.datasource.username=todo
spring.datasource.password=todo
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
You have to check Mysql information about:
- MySQL server address as localhost or 127.0.0.1
- Mysql server port as 12345 or default 3306 value
- Mysql username as todo or if you create a specific user
- Mysql password as todo
So create the file and restart mvn install command which will go through
Scanning for projects... -------------------------< fr.ema.ceris:todo >-------------------------- Building todo 0.0.1-SNAPSHOT from pom.xml --------------------------------[ jar ]--------------------------------- --- resources:3.2.0:resources (default-resources) @ todo --- Copying 1 resource --- compiler:3.10.1:compile (default-compile) @ todo --- Nothing to compile - all classes are up to date --- resources:3.2.0:testResources (default-testResources) @ todo --- Using 'UTF-8' encoding to copy filtered resources. Using 'UTF-8' encoding to copy filtered properties files. skip non existing resourceDirectory C:\Dev23\Workspace-vscode-182.2\todo\src\test\resources --- compiler:3.10.1:testCompile (default-testCompile) @ todo --- Nothing to compile - all classes are up to date --- surefire:2.22.2:test (default-test) @ todo --- ------------------------------------------------------- T E S T S ------------------------------------------------------- Running fr.ema.ceris.todo.TodoApplicationTests . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.7.15) fr.ema.ceris.todo.TodoApplicationTests : Starting TodoApplicationTests using Java 17.0.2 on DESKTOP-B8A57NP with PID 24940 (started by pierre.jean in C:\Dev23\Workspace-vscode-182.2\todo) fr.ema.ceris.todo.TodoApplicationTests : No active profile set, falling back to 1 default profile: "default" .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 3 ms. Found 0 JPA repository interfaces. o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.15.Final o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final} com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' WARN JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning fr.ema.ceris.todo.TodoApplicationTests : Started TodoApplicationTests in 2.912 seconds (JVM running for 3.497) Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.417 s - in fr.ema.ceris.todo.TodoApplicationTests Results: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 --- jar:3.2.2:jar (default-jar) @ todo --- Building jar: C:\Dev23\Workspace-vscode-182.2\todo\target\todo-0.0.1-SNAPSHOT.jar --- spring-boot:2.7.15:repackage (repackage) @ todo --- Replacing main artifact with repackaged archive --- install:2.5.2:install (default-install) @ todo --- Installing C:\Dev23\Workspace-vscode-182.2\todo\target\todo-0.0.1-SNAPSHOT.jar to C:\Users\pierre.jean\.m2\repository\fr\ema\ceris\todo\0.0.1-SNAPSHOT\todo-0.0.1-SNAPSHOT.jar Installing C:\Dev23\Workspace-vscode-182.2\todo\pom.xml to C:\Users\pierre.jean\.m2\repository\fr\ema\ceris\todo\0.0.1-SNAPSHOT\todo-0.0.1-SNAPSHOT.pom ------------------------------------------------------------------------ BUILD SUCCESS ------------------------------------------------------------------------ Total time: 8.661 s
How do we know to call maven command as mvn alias plus a parameter as install ?
You can display the list of phase for a maven project with :
mvn fr.jcgay.maven.plugins:buildplan-maven-plugin:list
To display the following table include the maven plugin (full name to convert from spring-boot-maven-plugin to spring-boot), the phase, the ID and the goal:
----------------------------------------------------------------------------------------- PLUGIN | PHASE | ID | GOAL ----------------------------------------------------------------------------------------- maven-resources-plugin | process-resources | default-resources | resources maven-compiler-plugin | compile | default-compile | compile maven-resources-plugin | process-test-resources | default-testResources | testResources maven-compiler-plugin | test-compile | default-testCompile | testCompile maven-surefire-plugin | test | default-test | test maven-jar-plugin | package | default-jar | jar spring-boot-maven-plugin | package | repackage | repackage maven-install-plugin | install | default-install | install maven-deploy-plugin | deploy | default-deploy | deploy
The list of phase is the most important part, but I detect some missing goal into spring-boot maven plugin:
process-resources compile process-test-resources test-compile test package install deploy
Each phase will include the previous one, we will use mainly mvn install because we do not provide architecture for the mvn deploy command on Github for example.
A custom spring maven command to start a project could be :
mvn spring-boot:run
You can check the project has access to todo database into our project and you can check a « Whitelabel Error Page » is display on the URL http://127.0.0.1:8080/.
4/ Entity
Create a package model to store a POJO Entity class for Spring into the src/main/java/ folder with the following code:
package fr.ema.ceris.todo.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Todo {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String texte;
private Boolean actif;
}
Add getter/setter and constructor with right click Menu > Source Action or CTRL+SHIFT+P then « source action » into the command palette:
Don’t forget to create multiples constructors and at least one with no parameter useful later.
Run again the project with maven command or run with VSCode command at the top right menu :
You can after open mysql.cmd client to display the new todo table linked to the Todo entity :
5/ MVC Control class basic part
Create a new class TodoControl into the same package as TodoApplication to manage RESTFull request with the following code :
package fr.ema.ceris.todo; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @RestController public class TodoControl { @RequestMapping(value="/todos", method=RequestMethod.GET) public String listeTodos() { return "{todos:[]}"; } }
Restart the Spring project and open the URL http://127.0.0.1:8080/todos to display the following JSON content :
{todos:[]}
Later we will find a JSON list of todo item as this format :
{"todos":[ {"id":1,"texte":"todo-1","actif": false} ,{"id":2,"texte":"todo-1","actif": false} ]}
6/ Debug configuration with VScode
To Debug and Open a browser with VScode, add a configuration with Menu Run > Open configurations, then edit and adapt the launch.json content based on my file :
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Current File",
"request": "launch",
"mainClass": "${file}"
},
{
"type": "java",
"name": "TodoApplication",
"request": "launch",
"mainClass": "fr.ema.ceris.todo.TodoApplication",
"projectName": "todo"
},
{
"name": "chrome",
"type": "chrome",
"runtimeExecutable":"C:/Users/pierre.jean/AppData/Local/Chromium/Application/chrome.exe",
"runtimeArgs": [
"--auto-open-devtools-for-tabs",
"--new-window"
],
"url": "http://127.0.0.1:8080/todos",
"webRoot": "${workspaceFolder}",
}
]
}
Then you can debug the TodoApplication with F5 shortcut (or Menu Run > Start Debugging), then open command palette (CTRL+SHIFT+P) and enter debug chrome (first time you have to enter debug chrome and NOT > debug chrome , YES remove > symbol please ) to start a Chrome/Chromium browser with the web developers tools already open.
You can also add a compounds settings to group starting two launching configuration as below (url et WebRoot are only here to understand where to add compounds code) :
"url": "http://127.0.0.1:8080/todos/",
"webRoot": "${workspaceFolder}",
}
]
,"compounds": [
{
"name": "Start TodoApplication and chrome",
"configurations": ["TodoApplication","chrome"],
}
]
}
7/ MVC Control class next part
We can see the controller class as the Servlet of our previous practical class, last year. Controller could manage HTTP request from GET and POST trigger from a specific URL part.
You can also read the table about « Relation entre URI et méthodes HTTP » (sorry the French version is more clear than the English one), to understand a direct relation between HTTP method and the UR/URI part.
We can add a method trigger by a GET URL to add a new Todo into our controller :
@RequestMapping(value = "/todo/{id}", method = RequestMethod.GET) public Todo getTodo(@PathVariable int id) { Todo todo = new Todo( "todo-" +id , false ); return todo; }
Try the following URL to debug the
We can also test with a curl command, but in PowerShell curl is an alias for Invoke-WebRequest command. The -d switch is for future JSON send object.
curl.exe -X GET -H "Content-Type: application/json" -d '{}' http://localhost:8080/todo/100
You can also use the following code into the Javascript console of Dev Web Tools inside your browser. This example will insert a new Todo item with a POST request to the future API url:
fetch("http://127.0.0.1:8080/api/todos", {
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"body": "{\"id\":1,\"texte\":\"test\",\"actif\":false}",
"method": "POST",
});
Now our entity POJO instance is only created in memory, we will make it persistent into the database.
8/ Hibernate persistence
Check your settings into applications.properties file
# ===============================
# DATABASE
# ===============================
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:12345/todo?zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=UTC
spring.datasource.username=todo
spring.datasource.password=todo
# ===============================
# JPA / HIBERNATE
# ===============================
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
spring.jpa.open-in-view=false
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type=TRACE
Maybe you could have to use this new maven dependency to manage date conversion from and to Mysql, so if you have this error :
Caused by: java.lang.ClassNotFoundException: org.threeten.bp.Instant
Please insert a new dependency into pom.xml file and start again mvn install to download the new package.
<dependency> <groupId>org.threeten</groupId> <artifactId>threetenbp</artifactId> <version>1.3.3</version> </dependency>
Then you can create a new package dao and a DAO manager class as TodoDao.java to read from database and persist Todo entities :
package fr.ema.ceris.todo.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import fr.ema.ceris.todo.model.Todo;
import java.util.List;
@Service
public class TodoDao {
@Autowired
private TodoRepository todoRepository;
public List<Todo> getAllTodos() {
return todoRepository.findAll();
}
public Todo getTodoById(Long id) {
return todoRepository.findById(id).orElse(null);
}
public Todo saveOrUpdate(Todo todo) {
return todoRepository.save(todo);
}
public void deleteTodoById(Long id) {
todoRepository.deleteById(id);
}
public List<Todo> getTodosActif() {
return todoRepository.findTodosByActif(true);
}
}
Then you have to create an interface TodoRespository to link
Then you can have a generic code about CRUD operation on Todo class just with this code :
package fr.ema.ceris.todo.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import fr.ema.ceris.todo.model.Todo;
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
Or add a specific HQL query to return specific Todo list items with :
package fr.ema.ceris.todo.dao;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import fr.ema.ceris.todo.model.Todo;
public interface TodoRepository extends JpaRepository<Todo, Long> {
@Query("SELECT t FROM Todo t WHERE t.actif = :actif")
List<Todo> findTodosByActif(@Param("actif") Boolean actif);
}
Then you can connect the TodoDao into our controller as a simple new property
@RestController
public class TodoControl {
@Autowired
private TodoDao todoDao;
@RequestMapping(value = "/todo/{id}", method = RequestMethod.GET)
public Todo getTodo(@PathVariable int id) {
Todo todo = new Todo( "todo-" +id , false );
todoDao.saveOrUpdate(todo);
return todo;
}
}
So now, you can create all the CRUD methods to manage CRUD operation and more customs methods. We also add on the class a new @RequestMapping to apply a new base URL as http://127.0.0.1:8080/api/todos/ for all methods.
@RestController
@RequestMapping("/api/todos")
public class TodoControl {
@Autowired
private TodoDao todoDao;
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET)
public Todo getTodo(@PathVariable int id) {
Todo todo = new Todo( "todo-" +id , false );
return todo;
}
@GetMapping
public List<Todo> getAllTodos() {
return todoDao.getAllTodos();
}
@GetMapping("/{id}")
public Todo getTodoById(@PathVariable Long id) {
return todoDao.getTodoById(id);
}
@PostMapping
public Todo createTodo(@RequestBody Todo todo) {
return todoDao.saveOrUpdate(todo);
}
@PutMapping("/{id}")
public Todo updateTodo(@PathVariable Long id, @RequestBody Todo todo) {
todo.setId(id);
return todoDao.saveOrUpdate(todo);
}
@DeleteMapping("/{id}")
public void deleteTodoById(@PathVariable Long id) {
todoDao.deleteTodoById(id);
}
}
NOTE: the old method which returns a String concatenation of todos JSON file is not useful, the conversion from the Todo instance into Json format is managed by Spring framework.
@RequestMapping(value = "/todos", method = RequestMethod.GET)
public String listeTodos() {
return "{todos:[]}";
}
Create all the element to manage TodoApplication at this point.
9/Static content into Spring
The src/main/ressources/static folder will be really useful to provide static HTML, CSS, JavaScript and Image content, we can imagine to place all the production file from a JavaScript framework as Angular, Reac, ViewJs, etc.
Create a IndexControl new class for provide index.html file stores into src/main/ressources/static folder.
IndexControl.java content will be :
@Controller public class IndexController { @RequestMapping("/") public String welcome() { return "index.html"; } }
Suppose you want to provide any images, javascripts, css files from a src/main/ressources/static/js/ folder, you can add to TodoApplication class this method to handle:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js/");
}
You can also add CSS and images folders and manage also contents inside.
Most of JavaScript framework will build a package of files for a final distribution, so place those files into the src/main/ressources/static/ folder will be a simple way to integrate the frontend with the backend.
10/ React development with VS Code
To the frontend development, we will use a separate standalone VS Code install. Why, to avoid conflict between Vscode plugins version and separate the backend to the frontend.
At first, we need to download a version of NodeJS server in binary version and unzip the content into C:\Dev23\node-v18.20.4-win-x64 folder for example.
Then download Vscode 1.92.0 zip download for standalone version into c:\DEV23\VSCode-win32-x64-1.82.2-react\ folder and create a start a file to manage the VS Code configuration for React as code-182.2.react.cmd :
SET VSCODE_PATH=VSCode-win32-x64-1.82.2-react SET WORKSPACE_PATH=Workspace-vscode-182.2-react SET NODE_PATH=node-v18.18.0-win-x64 SET BUILD_PATH=C:\Dev23\Workspace-vscode-182.2\todo\src\main\resources\static\ SET PATH=%PATH%;%cd%\%NODE_PATH%\; if not exist .\%VSCODE_PATH%\extensions-dir\ mkdir .\%VSCODE_PATH%\extensions-dir if not exist .\%VSCODE_PATH%\data\ mkdir .\%VSCODE_PATH%\data\ if not exist .\%WORKSPACE_PATH% mkdir .\%WORKSPACE_PATH%\ cd .\%WORKSPACE_PATH% ..\%VSCODE_PATH%\code.exe --extensions-dir=..\%VSCODE_PATH%\extensions-dir\ --user-data-dir=..\%VSCODE_PATH%\data\ . cd ..
The script need to know where we will deploy into the Spring boot project a build of our React front end project.
An environment variable BROWSER could store the path to a specific browser but this settings is not working at present :
SET BROWSER=C:/Users/pierre.jean/AppData/Local/Chromium/Application/chrome.exe
Open VS Code with a terminal and enter the following command to check npm program is available into your PATH and also update npm to the last version :
npm update npm
Then install create-React-app package with :
npm install -g create-react-app
We are ready to start to use React framework with the following command:
npx create-react-app todo
This command will create an architecture of React application where you can create a new launch.json file into .vscode folder with Menu Run > Add configuration… :
The following content will start Chromium browser with the classical default settings we used commonly :
{ "configurations": [ { "type": "chrome", "name": "chrome", "request": "launch", "runtimeExecutable":"C:/Users/pierre.jean/AppData/Local/Chromium/Application/chrome.exe", "runtimeArgs": [ "--auto-open-devtools-for-tabs", "--new-window", "--user-data-dir=${workspaceFolder}/chrome/" ], "url": "http://127.0.0.1:3000/", "webRoot": "${workspaceFolder}" } ] }
Later we will launch a debug session for React with the F5 shortcut, but do not forget to change directory with the command into the terminal:
cd todo
And start the default React application with the command :
npm start
If you forget to change directory, you will see the following error :
Could not read package.json: Error: ENOENT: no such file or directory, open 'C:\Dev23\Workspace-vscode-182.2-react\package.json'
Instead, display the default React application into a browser at http://127.0.0.1:3000/ :
On the left, the browser displays the React application, in the right I open Dev Web Tools include a React additional tabs install from React Developer Tools to display React Component easily into the browser.
11/ React files and folders structure
The structure of a basic React application could be display as, but we will ignore somes technical folders :
C:\Dev23\Workspace-vscode-182.2-react\todo\ ¦ ¦ .gitignore ¦ package-lock.json ¦ package.json ¦ README.md ¦ +---node_modules ¦ ¦ ¦ +--- 44200 files and folders for NodeJs ¦ +---public ¦ favicon.ico ¦ index.html ¦ logo192.png ¦ logo512.png ¦ manifest.json ¦ robots.txt ¦ +---src App.css App.js App.test.js index.css index.js logo.svg reportWebVitals.js setupTests.js
We can focus to start on the public folder content :
- favicon.ico : default file to create an icon if the web site is bookmarked
- index.html: the very basic HTML structure page which will receive Javascript based React components
- logo192.png : PNG image lofo of 192×192 pixels
- logo512.png : PNG image lofo of 512×512 pixels
- manifest.json: a file to describe a Web App if need by the build process of our application
- robots.txt: a file to give back instructions to index internet robots as Google.
Most of our work will be into src folder
- App.css: stylesheet file link to App React Component
- App.js: JavaScript file to render the JSX App React Component
- App.test.js: test file linked to App React Component
- index.css: default stylesheet file link to ../public/index.html file
- index.js: top level Javascript file link all files together and usually the top App React Component
- logo.svg: a default SVG file to be used by App.js file
- reportWebVitals.js: a JavaScript file to manage performance with Google Analytics for example, details of information are online
- setupTests.js: JavaScript configuration to manage test rules
We will mainly work with App.js file at first to render the top level React Component, edit the file with the following content :
import './App.css'; function App() { return ( <div> Hello World! </div> ); } export default App;
You will see a basic React Component to show a welcome message. You can simplefy with :
import './App.css'; export default function App() { return ( <div id="main" className="important"> Hello World! </div> ); }
NOTE: we cannot use class as attribute because class is already used in reserved word in JavaScript
Add some CSS rules into App.css to « pimp up » the div tag but 2 top HTML tags are not valid into JSX component. This will not working :
export default function App() { return ( <h1>Welcome</h1> <div id="main" className="important"> Hello World! </div> ); }
So you can use an empty tag as :
export default function App() { return ( <> <h1>Welcome</h1> <div id="main" className="important"> Hello World! </div> </> ); }
12/ Create a basic unit test
To test our very first Component, just edit App.test.js file to change the initial test to check if text « learn react » is present:
import { render, screen } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { render(<App />); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); });
This new test file include 2 tests one to check if the welcome message « Hello World: » is present and the second test use the DOM container to check if a divElement with id=main and class important is present :
import { render, screen } from '@testing-library/react'; import App from './App'; test('test content of App', () => { render(<App />); const textWelcome = screen.getByText("Hello World!") expect(textWelcome).toBeInTheDocument(); }); test('test id and class of App', () => { const {container} = render(<App />); const divElement = container.querySelector('[id="main"]') expect(divElement).toBeInTheDocument(); expect(Array.from(divElement.classList)).toContain("important"); });
To start React test, just enter the following command :
npm test
Test server will restart each time you change something into test file, output if everything is OK:
PASS src/App.test.js √ test content of App (24 ms) √ test id and class of App (2 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.026 s Ran all test suites related to changed files. Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run.
12/ Add component List and Todo
Now we will add two new React Component : List and Todo with the corresponding files :
- Liste.js
- Liste.test.js
- Liste.css
- Todo.js
- Todo.test.js
- Todo.css
At first, change the App.js to render a new List React component :
import logo from './logo.svg';
import './App.css';
import Liste from './Liste';
export default function App() {
return (
<div id="main" className="important"><Liste name="liste1" /></div>
);
}
Then we can create a new Liste component with Liste.js file content :
import './Liste.css'; import Todo from './Todo'; const todos = [{"id":1,"texte":"todo-1","actif": false},{"id":2,"texte":"todo-2","actif": true}]; export default function Liste({name}) { return ( <div> <h1>{name}</h1> <ul> {todos.map((todo) =><Todo key={todo.id} current_todo={todo} /> )} </ul> </div> ); }
We will work at first with a test JSON list of Todo item. The Liste component receives a property name from the App React Component.
The map function will generate a list of Todo React Component we will create now. The key attribute is important to let React Component Todo works.
import './Todo.css'; import { useState } from 'react'; export default function Todo({current_todo}) { const [todo, setTodo] = useState(current_todo) const id = "todo-" + todo.id; const classe = todo.actif ? "checked" : "default"; return ( <li id={id} className={classe} key={todo.id}> {todo.texte} </li> ); }
The property current_todo will contain data from the List React Component, then we will create several constants :
- todo: current and initial data information, each update of this variable will render the component
- setTodo: function to update todo element
- id: unique HTML tag id to make difference between <li> tag element
- classe: the css class in relation with Todo.css
The Todo.css will manage an actif ou not actif todo item as :
.default { text-decoration: underline ; } .checked { text-decoration: line-through; }
To render, you can start with npm run command.
14/ Ajax request
React do not provide a specific library to create Ajax request, but you remember the Spring server is not working on the 3000 port but on the 8080 port, so we need to create a proxy into React application with setupProxy.js file (into C:\Dev23\Workspace-vscode-182.2-react\todo\src\ folder):
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
// Redirect requests starting with /todos/ to http://127.0.0.1:8000/todos/
app.use(
'/api/todos',
createProxyMiddleware({
target: 'http://127.0.0.1:8080/',
changeOrigin: true,
})
);
};
URLs from http://127.0.0.1:3000/api/todos/ will be trasnfert to http://127.0.0.1:8080/api/todos/ to bypass CORS constraint using 2 server webs NodeJS for React and Tomcat inside Spring Boot.
Now we will send an update URL to Spring server ( http://127.0.0.1:3000/api/todos/test/1/true ) each time user clicks on the todo element,
At first, I will create a test method into the TodoControler.java file with the following code :
@RequestMapping(value = "/test/{id}/{actif}", method = RequestMethod.GET)
public Todo getToggleTodo(@PathVariable Long id, @PathVariable boolean actif) {
Todo todo = todoDao.getTodoById(id);
todo.setActif(actif);
todoDao.saveOrUpdate(todo);
return todo;
}
I can test it with a curl command or into the the console Dev Web Tools with at first to Spring boot web server with 8080 port :
fetch("http://127.0.0.1:8080/api/todos/test/1/true", {
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"method": "GET",
});
Then use the proxy from nodeJs web server with the 3000 port :
fetch("http://127.0.0.1:3000/api/todos/test/1/true", {
"headers": {
"Content-Type": "application/json; charset=UTF-8"
},
"method": "GET",
});
I add a breackpoint into Spring boot code to check the commande will update the todo idem with ID 1. This will help a lot to find errors.
Then update Todo.js file :
import './Todo.css';
import { useState } from 'react';
function Todo({current_todo}) {
const [todo, setTodo] = useState(current_todo)
const id = "todo-" + todo.id;
const classe = todo.actif ? "checked" : "default";
function handleClick(e) {
const actif = !todo.actif;
fetch('./api/todos/test/' + todo.id + '/' + actif )
.then((res) => res.json())
.then((data) => {
console.log(data);
setTodo(data);
})
.catch((err) => {
console.log(err.message);
});
}
return (
<li id={id} className={classe} key={todo.id} onClick={handleClick} >{todo.texte}</li>
);
}
export default Todo;
We add attribute onClick={handleClick} to the tag and create a function name handleClick().
return ( <li id={id} className={classe} key={todo.id} onClick={handleClick} >{todo.texte}</li> );
The function will invert the actif value then create an Ajax Http request to the server a this structure : http://127.0.0.1:3000/todo/1/true to update todo item number 1 to set the new actif value at true.
NOTE: it is also possible to send the actual todo item value and let the Spring boot method to reverse the value.
15/ Add a form
Create a new form React Component called FormTodo with a new FormTodo.js file. At first, we will just send a POST URL request to the server. The curl format will be :
curl.exe -X POST -H "Content-Type: application/json" -d '{\"texte\":\"chocolat\",\"actif\":\"true\"}' http://127.0.0.1:3000/api/todos
The FormTodo.js file at start :
export default function FormTodo() {
function handleSubmit(e) {
// Prevent the browser from reloading the page
e.preventDefault();
const formData = new FormData(e.target);
formData.set('actif', "false");
if ( formData.get('actif') === "on" ) formData.set('actif', "true")
const json = JSON.stringify(Object.fromEntries(formData));
fetch('./api/todos' , { method: e.target.method,headers: { 'Content-Type': 'application/json'}, body: json })
.then((res) => res.json())
.then((data) => {
console.log(data);
})
.catch((err) => {
console.log(err.message);
});
}
return (
<form method="post" onSubmit={handleSubmit}>
<label>
New Todo item: <input id="new_texte" name="texte" type="text" />
</label>
<label>
Default actif :<input id="new_actif" name="actif" type="checkbox" />
</label>
<button type="submit">Add new todo</button>
</form>
);
}
Note: we have to convert the default checkbox value from on or nothing to boolean true/false value.
17/ React useState to update a list
When we update a Todo on the server, we receive from the Http Ajax request a Json of the new created todo with the unique id from the database. The next step is to update the React list of all todos.
At first we need to receive a JSON list of all todos instead of use a JSON initial array. We will need to change lightly Spring boot controller with a new method to download a JSON array of Todo:
@RequestMapping(value = "/todos", method = RequestMethod.GET) public List<Todo> listeTodos() { List<Todo> listTodo = todoDao.listTodo(); return listTodo; }
Then we can change change List React Component to manage receive a list of todos. We use a variable isLoading with a method setIsLoading to manage the delay before loading list
import React, { useEffect, useState } from 'react';
export default function Liste({name}) {
const [todos, setTodos] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('./api/todos' )
.then((res) => res.json())
.then((data) => {
console.log(data);
setTodos(data);
setIsLoading(false);
})
.catch((err) => {
console.log(err.message);
});
}, []);
if (isLoading) {
return (<div>
<h1>{name}</h1>
Loading...
</div>);
}
So we can see the Loading… message before the actual list of todo items is available. To simulate a long delay we can open Web Dev Tools with Menu > More tools > Network conditions and use a « Slow 3G » Network throttling as show :
We see that isLoading variable when value change throw setIsLoading method will trigger a new render of our React Component.
18/ Callback from FormTodo to List
Now we will create a method into List React Component as add_new_todo which will receive from another React Component a todo item and add it to the list of todos :
function add_new_todo(new_todo){ setTodos([...todos, new_todo]); }
We cannot directly use the property todos to add element, we have to call setTodos() function to call react new rendering of the component Liste.
The add_new_todo function is supposed to be pass to the FormTodo React element from List.js :
<FormTodo callback_add_new_todo={add_new_todo} />
Then into FormTodo we can receive this function as callback_add_new_todo element and use it after receive data from the fetch method into handleSubmit function :
export default function FormTodo({callback_add_new_todo}) {
New handSubmit () function part of code :
fetch('./api/todos' , { method: form.method,headers: { 'Content-Type': 'application/json'}, body: json })
.then((res) => res.json())
.then((data) => {
console.log(data);
callback_add_new_todo(data); // <----- New callback
})
.catch((err) => {
console.log(err.message);
});
This is the way, the sub React Component is able to use a callback function from the top level. You will find more information from React tutorial about Hook and Sharing data.
NOTE: to deploy React application, the following command will create a bunch of html,css, javascript and more files to place into the BUILD_PATH environment variable :
npm run build
This will create the React application files into the src/main/ressources/static/ folder to be available into one Web full application with frontedn and backend :
19/ Angular Front-End alternative
We will install a different Visual Code version into folder C:\Dev23\VSCode-win32-x64-1.82.2-angular with a specific code-182.2.angular.cmd file as :
cd "%~dp0"
SET VSCODE_PATH=VSCode-win32-x64-1.82.2-angular
SET WORKSPACE_PATH=Workspace-vscode-182.2-angular
SET NODE_PATH=node-v20.7.0-win-x64
SET BUILD_PATH=C:\Dev23\Workspace-vscode-182.2\todo\src\main\resources\static\
SET PATH=%PATH%;%cd%\%NODE_PATH%\;
if not exist .\%VSCODE_PATH%\extensions-dir\ mkdir .\%VSCODE_PATH%\extensions-dir
if not exist .\%VSCODE_PATH%\data\ mkdir .\%VSCODE_PATH%\data\
if not exist .\%WORKSPACE_PATH% mkdir .\%WORKSPACE_PATH%\
cd .\%WORKSPACE_PATH%\
..\%VSCODE_PATH%\code.exe --extensions-dir=..\%VSCODE_PATH%\extensions-dir\ --user-data-dir=..\%VSCODE_PATH%\data\ .
cd ..
We will install a different standalone NodeJS into C:\Dev23\node-v20.7.0-win-x64\
Open Visual code terminal, confirm execution from the terminal and check which NodeJs your using with npm –version and node.exe –version as :
If you answer no you can force to allow execution from command line with :
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
At first, we will install Angular Framework into our NodeJS server:
npm install -g @angular/cli
Time to time, it could be useful to update all packages and also check integrity of packages with npm command :
npm i npm npm cache verify
We will create a new Angular project with the command
ng new todo
Then we can several questions :
- Would you line to add Angular routing : Yes to activate routing
- Which stylesheet format would you like to use (use arrow keys): choose CSS but one day have a look to SASS https://sass-lang.com/guide as CSS with variables and template
Then a new Angular project is created into todo folder. But Node.js will use 4200 port and our Spring Server stills listen on 8080. To avoid CORS conflict, we will create proxy.conf.json file with redirection as :
{
"/api/todos": {
"target": "http://localhost:8080/",
"secure": false,
"changeOrigin": true
}
}
Please be aware, Node.js configuration make a difference between 127.0.0.1 and localhost in URL.
To use our need created Angular application start with
ng serve --proxy-config proxy.conf.json
Open a web browser to display the Welcome interface :
Or may be this old version:
20/ Typescript and Angular
TypeScript is a kind of meta langage over JavaScript (TypeScript), the main concept is to write a code into TypeScript then convert it into JavaScript after verifications and controls to avoid for example a classical « Object doesn’t support property » as :
var todo = {"id":1,"texte":"lait","actif":false} console.log( todo.Id );
Previous year, we see the initial framework of AngularJS version that is not used a lot nowadays code only in JavaScript.
The new application main component is divided into several files :
app.component.ts
— main file for the main angular componentapp.component.html
— view part of the main angular componentapp.component.css
— CSS file link to the main angular componentapp.component.spec.ts
— unit test to the main angular component
The default main angular component app.component.html is far to complex, so remove everything and just enter :
<div id="title"> <h1>{{title}}</h1> </div>
Then add new style to this component with change into app.component.css :
/* CSS app.component */ #title > h1{ text-align: center; display: block; border: 1px solid blue; }
Then we will change the app.component.ts into this version. We create an interface Todo to just display some content and test:
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
export interface Todo {
id: number;
texte: string;
actif: boolean;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet,CommonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
lesTodos: Array<Todo> = [];
title = 'todo';
ngOnInit(): void {
this.lesTodos=[{"id":1,"texte":"lait","actif":false},{"id":2,"texte":"beurre","actif":false}];
}
}
Add into app.component.html some code to display the list of todo items as :
<ul> <li *ngFor="let todo of lesTodos">{{todo.texte}}</li> </ul>
This is not the « good » way to do, later we will create an Angular Component to manage Todo item.
21/ Deploy into Spring
As explain the final version of Angular development could be deployed into the Spring folder which manage the front-end, the following command will create the final group of files (html, css, js, images).
This version is of terminal command line :
ng build todo --output-path=%BUILD_PATH%
I you use Powershell terminal, the version will be :
ng build todo --output-path=$env:build_path
NOTE: the actual version of Angular create a content into a browser subfolder. There are 3 strategies: configuration of Angular deployment, change the static folder to point browser subfolder or move the content. I choose move the content because this setting will be change in the futur and I like to keep close to the classical Spring configuration so in Powershell :
ng build todo --output-path=$env:build_path ; move $env:build_path/browser/* $env:build_path/ ; rmdir $env:build_path/browser/
You can see the URL to call Spring project contains all the html and 3 JavaScript files:
Please be aware on the JSON format very strict by Angular, so :
- No sub object if the array of element is send back
- No single quote, JSON use double quote only
- No text value element without double quote
You can bypass some double quote with applications.properties settings :
spring.jackson.parser.allow-unquoted-field-names=true
Allows the following JSON :
{id: 10, texte: "yaourt", actif: true}
Instead of :
{"id": 10, "texte": "yaourt", "actif": true}
You can test with curl, Postman extension or Fiddler software to simulate request or event read later how to use the Console of your browser.
22/ Rescue data from Spring into Angular
We need to add a new module into app.config.ts file :
import { provideHttpClient } from '@angular/common/http';
And add aa provider into the same file adding the green part:
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()]
Then we can use into app.component.ts:
import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs';
Angular has a difference between a constructor() and a ngOnInit() method. Constructor will mainly create properties and ngOnInit() will use into the cycle of life of composants and dependences. Short version, first constructor then ngOnInit so :
export class AppComponent implements OnInit {
lesTodos: Array<Todo> = [];
title = 'todo';
constructor(private http: HttpClient) {
}
ngOnInit(): void {
// this.lesTodos=[{"id":1,"texte":"lait","actif":false},{"id":2,"texte":"beurre","actif":false}];
const desTodos:Observable<[]> = this.http.get<[]>('./api/todos');
desTodos.subscribe(desTodos => {
this.lesTodos = desTodos;
console.log(this.lesTodos);
});
}
But we want to type the return elements from the HTTP request with already provide at first :
export interface Todo { id: number; texte: string; actif: boolean; }
So we can then fetch data from HTTP request with :
// PJE test only :
// const desTodos:Observable<Todo[]> = this.http.get<Todo[]>('http://localhost:4200/api/todos');
const desTodos:Observable<Todo[]> = this.http.get<Todo[]>('./api/todos');
That an important advantage from Angular, a basic HTTP request to fetch data with JavaScript will not check return data type as :
const getTodos = async () => {
const response = await
fetch('http://localhost:4200/api/todos'
, { method: 'GET' ,'Content-Type': 'application/json'});
if (response.status === 200) {
const todos = await response.json();
console.log( todos );
}
}
getTodos();
You can test on the server HTTP request with a fetch, at first open an URL on the server to avoid CORS restriction then you enter the previous code to test the request :
You can simulate also a POST request in the console with :
fetch('localhost:4200/api/todos', {
method: 'POST',
body: JSON.stringify({
texte: 'chocolat',
actif: false,
id: 1 }),
headers: { 'Content-type': 'application/json; charset=UTF-8' }
})
.then(res => res.json())
.then(console.log)
23/ Create a new Angular component
We will replace the basic interface of Todo buy a fresh new Angular Component, into the terminal enter the following command:
ng generate component Todo
4 new files are created :
- todo.component.css — CSS file for the TODO angular component
- todo.component.html — view file for the TODO angular component
- todo.component.spec.ts — unit test file for the TODO angular component
- todo.component.ts — main file for the TODO angular component
We will not use the todo.component.css at first but mainly with the todo.component.html to transfers html element from Application component to Todo component don’t forget we will be into a HTML tag <li></li>.
<span>( {{currentTodo.id}} ) -> {{currentTodo.texte}} is {{currentTodo.actif}}</span>
Now we need to create the currentTodo instance into the TodoComponent as a property in Angular as @Input() currentTodo:Todo; . Edit the todo.component.ts :
import { Component, OnInit, Input } from '@angular/core'; export interface Todo { id: number; texte: string; actif: boolean; } @Component({ selector: 'app-todo', templateUrl: './todo.component.html', styleUrls: ['./todo.component.css'] }) export class TodoComponent implements OnInit { @Input() currentTodo!:Todo; constructor() { } ngOnInit(): void { } }
We will later keep only one version of export interface Todo but for this situation keep it in double.
Now we can use this new Angular component into app.component.html (keep th old version as HTML template to learn difference) :
<li *ngFor="let todo of lesTodos"> <!-- <span>{{todo.id}}</span> {{todo.texte}} --> <app-todo [currentTodo]=todo></app-todo> </li>
The link is made with the unique selector name as app-todo and test this component, but with Angular new version we need to add the TodoComponent into the import of app.component.ts file :
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet,CommonModule,TodoComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
Change some HTMl add a CSS class into todo.component.html as :
<span id="checkbox_{{currentTodo.id}}" class="todo_{{currentTodo.actif}}"> {{currentTodo.texte}} </span>
And tis time we need a CSS modificaiton as :
span.todo_false{ text-decoration: line-through; }
We can also imagine using a pipe or | symbol to manage the case to display as explain here as yesNo pipe element : https://fiyazhasan.me/angular-pipe-for-handling-boolean-types/ as something like following :
The command to create pipe files :
ng g pipe actifNotactif
Then edit actif-noactif.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'actifNotactifPipe',
standalone: true
})
export class ActifNotactifPipePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return value ? "todo_actif" : "todo_notactif";
}
}
Usage as :
<span id="checkbox_{{currentTodo.id}}" class="{{currentTodo.actif | actifNotactif }}"> {{currentTodo.texte}} </span>
Do not forget to create the pip with the command ng g pipe actifNotactif and to import into the Angular component todo.component.ts:
import { ActifNotactifPipe } from '../actif-notactif.pipe';
imports: [ActifNotactifPipe],
So you know how to customize a element from an external pipe component file.
24/ Form and data binding
Create a new event when span click is trigger in todo.component.html:
<span (click)="onEdit()" id="checkbox_{{currentTodo.id}}" class="todo_{{currentTodo.actif}}"> {{currentTodo.texte}} </span>
Now a new method in todo.component.ts :
export class TodoComponent implements OnInit { @Input() currentTodo!:Todo; edit:boolean = false; onEdit(): void { this.edit = !this.edit; } }
To use ngIf we have to import into Todo.component.ts :
import { CommonModule } from '@angular/common';
imports: [ActifNotactifPipe,CommonModule],
The value edit will display a span or an HTML tag for input text field as :
<span *ngIf="edit==false" (click)="onEdit()" id="checkbox_{{currentTodo.id}}" class="todo_{{currentTodo.actif}}"> {{currentTodo.texte}} </span>
<span *ngIf="edit==true"
id="checkbox_{{currentTodo.id}}">
<input type="text" value="{{currentTodo.texte}}" placeholder="Description"/>
</span>
We need to use a new module named FormsModule as import in todo.component.ts :
import { FormsModule } from '@angular/forms';
And also into the list of module in import part as bellow :
imports: [ActifNotactifPipe,CommonModule,FormsModule],
We will manage into TodoComponent to link the value of the input field with the currentTodo.texte. Any modification from the property will be transfers to the field and vice versa.
<input placeholder="Description" [(ngModel)]="currentTodo.texte" (focusout)="onValidation($event)" autofocus />
The new method onValidation will be trigger when the input field will lost the focus but only change the value edit as :
onValidation(event : any): void { this.edit = false; }
Now add a checkbox to manage the acti/not actif state of the Todo component as :
<input placeholder="Description" [(ngModel)]="currentTodo.texte" /> <input type="checkbox" [(ngModel)]="currentTodo.actif" /> <input type="button" (click)="onValidation($event)" value="Update" />
Lost of the focus is removed because when you click on the checkbox, the input field lost the focus. When you lost the focus, we need to stop propagation of event because click on the Update button will trigger the span tag to be editable :
onValidation(event : any): void { event.stopPropagation(); this.edit = false; this.updateTodo(); }
The to send an Ajax HTTP request to the Spring Boot server the URL will be call but do not forget to add a proxy rule to manage the new URL and to import the http module into the Todo Component :
updateTodo():void{ const newTodo:Observable<Todo> = this.http.post<Todo>('./updateTodo' , this.currentTodo ); newTodo.subscribe(newTodo =>{ this.currentTodo = newTodo; console.log(this.currentTodo); }); } constructor(private http: HttpClient) { }
We can now remove the interface Todo to create a new file todo.ts with following content :
export class Todo{ private _id:number = -1; private _texte: string = "<>"; private _actif:boolean = false; public constructor(newId:number,newTexte:string,newActif:boolean){ this._id = newId; this._texte = newTexte; this._actif = newActif; } public get id() { return this._id; } public set id(newId: number) { this._id = newId; } public get texte() { return this._texte; } public set texte(newTexte: string) { this._texte = newTexte; } public get actif() { return this._actif; } public set actif(newActif: boolean) { this._actif = newActif; } }
Just import when you need the class with :
import { Todo } from './todo';
If you place the todo.ts file into a sub folder, please consider using :
import { Todo } from './todo/todo';
25/ Test with Angular
At first to create a custom karma.conf.js file inside todo folder to add specific custom settings for the Chromium browser based from karma.conf.js.template :
// Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), require('@angular-devkit/build-angular/plugins/karma') ], client: { jasmine: { // you can add configuration options for Jasmine here // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, clearContext: false // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { suppressAll: true // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, '<%= relativePathToWorkspaceRoot %>/coverage/<%= folderName %>'), subdir: '.', reporters: [ { type: 'html' }, { type: 'text-summary' } ] }, reporters: ['progress', 'kjhtml'], browsers: ['Chrome_without_security'], customLaunchers: { Chrome_without_security: { base: 'Chromium', flags: ['--disable-web-security', '--disable-site-isolation-trials', '--auto-open-devtools-for-tabs' ] } }, restartOnFileChange: true }); };
We can call tests from the following command line :
ng test --karma-config=karma.conf.js
A lot of test will fails, because may be a missing <div id=’main’> to inclose the <ul> list of todo and also because TodoComponent do not have default value for the property currentTodo, so :
@Input() currentTodo:Todo = new Todo(-1,"",false);
You can modify app.component.spec.ts as follows :
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { provideHttpClient } from '@angular/common/http';
import {provideHttpClientTesting } from '@angular/common/http/testing';
import {By} from '@angular/platform-browser';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'todo' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('todo');
});
it('should render <h1 id="title">{{title}}</h1>', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('#title')?.textContent).toContain('todo');
});
it(`should have as list tag <ul>`, () => {
const fixture = TestBed.createComponent(AppComponent);
const inputElement = fixture.debugElement.query(By.css('div#main > ul'));
const app = fixture.componentInstance;
expect(inputElement).toBeTruthy();
});
});
You can create a distinct test for component with edit the file todo.component.spec.ts to test s:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TodoComponent } from './todo.component';
import { provideHttpClient } from '@angular/common/http';
import {provideHttpClientTesting } from '@angular/common/http/testing';
import {By} from '@angular/platform-browser';
import { Todo } from './todo';
describe('TodoComponent', () => {
let component: TodoComponent;
let fixture: ComponentFixture<TodoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TodoComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting()
]
})
.compileComponents();
fixture = TestBed.createComponent(TodoComponent);
component = fixture.componentInstance;
component.currentTodo = new Todo(1, "chocolat" , false );
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render chocolat text', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('#checkbox_1')?.textContent).toContain('chocolat');
});
it('should update currentTodo when input changes', () => {
const newTodo = new Todo(1, "ananas" , true );
component.currentTodo = newTodo;
fixture.detectChanges();
expect(component.currentTodo).toEqual(newTodo);
});
it('should validate input element', () => {
component.currentTodo = new Todo(1, "ananas" , true );
component.onEdit();
fixture.detectChanges();
fixture.whenStable().then(() => {
const inputElement = fixture.debugElement.query(By.css('input[placeholder="Description"]')).nativeElement;
console.log(inputElement.nativeNode.ngReflectModel);
expect(inputElement.value).toBe('ananas');
});
});
});
Postman API test a sample with Geo postcode
Install the Postman Vscode plugin or any kind of test API tools (Bruno is Ok also)
Then add your test, here a test with an open API to find places from their « postal zip code », test : https://api.zippopotam.us/fr/30100 will return :
{
"post code": "30100",
"country": "France",
"country abbreviation": "FR",
"places": [
{
"place name": "Alès",
"longitude": "4.0833",
"state": "Languedoc-Roussillon",
"state abbreviation": "A9",
"latitude": "44.1333"
}
]
}
Then tree basics tests will be, just to check if the URL match Alès city :
pm.test("Status test", function () {
pm.response.to.have.status(200);
});
pm.test("Contient une seule place", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.places.length).to.eql(1);
});
pm.test("Contient Alès", function () {
var jsonData = pm.response.json();
pm.expect(jsonData.places[0]["place name"]).to.eql("Alès");
});
Deliverables
- User stories as non-technical way
- Technicals tasks derived from User stories
- Tasks time evaluations
- API lists
- API Postman-like test scenario
- UML class diagram
- UML sequence diagram
- Wireframes interfaces
- Github source code commit each session
- Database schema
- JSON data exchange format
Demonstrations
User storie sample: a teacher displays training details and manage linked contents
Technical taks: click on a training to display contents, remove contents, add contents
Use a tasks poker to evaluate time of each User stories and tasks
API lists :
- GET /tasks – get all tasks
- POST /tasks – create a task
- PUT /tasks/{id} – update a task
- DELETE /tasks/{id} – delete a task
Wireframe by composant :
+------------------------------------------------+
| TODO List |
+------------------------------------------------+
| + Add New Task |
+------------------------------------------------+
| [ ] Task 1 - Edit - Delete |
| [ ] Task 2 - Edit - Delete |
| [ ] Task 3 - Edit - Delete |
| [ ] Task 4 - Edit - Delete |
| |
| |
+------------------------------------------------+
+------------------------------------------------+
| Add New Task |
+------------------------------------------------+
| Texte: [________________________] |
| |
| [ Save ] |
| |
| |
+------------------------------------------------+
Class Diagram
Additional subject
Hibernate Many to One and Many to Many
React native
Docker
ViewJs 3
NodeJS framework as Express
Django
J hipster
Maven
Gradle
SASS & LESS
GIT avancé
Méthode agile
Boostrap CSS
React avec Typescript
Next.js
Symfony 2
Ember.js
Copilot / IA integration withe IDE