Compare Frameworks
Overview
Vaadin Flow is a full-stack web framework that runs on Java. It has:
- A Java-based component API.
- Automatic server-client communication over XHR or WebSockets.
- A customizable design system with 40+ UI components.
- Routing.
- Forms.
- Internationalization.
- Dependency injection (supports Spring and CDI).
Vaadin Flow is a unique framework because it lets developers build single-page applications fully in Java. HTML, JavaScript, and CSS can be used for customization, but they are not required to build an app. When using the Java API, Vaadin apps run on the server and give you access to data and services, without the need to create REST APIs.
Hilla is a full-stack web framework that runs on Java. It has:
- A TypeScript-based component model.
- Reactive, declarative templates.
- Asynchronous, type-safe communication to Java backends.
- A customizable design system with 40+ UI components.
- Routing and code splitting.
- Efficient DOM rendering.
Hilla is designed for building client-side apps. Views are based on web components, built with LitElement and TypeScript. The Vaadin server exports typed, async functions for server access, giving you the same type information both on the server and in the client. The type information is automatically generated based on the server classes, which means you'll notice breaking API changes at compile time, but not at runtime.
Vaadin also supports building components in Java with the Flow framework. Flow is covered separately for easier comparison. Both frameworks can be used simultaneously in the same application.
React is a JavaScript library for building user interfaces. It includes:
- A component model.
- Reactive, declarative templates.
- Efficient DOM rendering.
React is often considered a framework, but it's in fact a component model.
React's component model is flexible and can be combined with third-party libraries for features, like routing and state management. This allows developers to build "frameworks" that are specific to the project they are working on.
Angular is a frontend web and mobile app framework. It has:
- A TypeScript-based component model.
- Reactive, declarative templates.
- Dependency injection.
- Internationalization.
- Animations.
- Forms.
- Modules.
- Material design UI components.
Angular is a frontend framework with many of the features enterprise developers are used to, like modules and dependency injection. Angular abstracts away from underlying web technologies and introduces several framework-specific concepts.
The size and complexity of the Angular framework can make it more difficult to learn than a component model like React or LitElement that Vaadin uses.
Vue is a frontend web application framework. It has:
- A component model.
- Reactive, declarative templates.
- Virtual DOM-based rendering.
- Routing.
- Forms.
- State management.
Vue is a light-weight frontend framework that contains the essentials for building a web app. In addition to an API for creating individual components, it supports routing and state management for building more complex applications. It also includes support for creating forms.
Vaadin Flow is a full-stack web framework that runs on Java. It has:
- A Java-based component API.
- Automatic server-client communication over XHR or WebSockets.
- A customizable design system with 40+ UI components.
- Routing.
- Forms.
- Internationalization.
- Dependency injection (supports Spring and CDI).
Vaadin Flow is a unique framework because it lets developers build single-page applications fully in Java. HTML, JavaScript, and CSS can be used for customization, but they are not required to build an app. When using the Java API, Vaadin apps run on the server and give you access to data and services, without the need to create REST APIs.
Hilla is a full-stack web framework that runs on Java. It has:
- A TypeScript-based component model.
- Reactive, declarative templates.
- Asynchronous, type-safe communication to Java backends.
- A customizable design system with 40+ UI components.
- Routing and code splitting.
- Efficient DOM rendering.
Hilla is designed for building client-side apps. Views are based on web components, built with LitElement and TypeScript. The Vaadin server exports typed, async functions for server access, giving you the same type information both on the server and in the client. The type information is automatically generated based on the server classes, which means you'll notice breaking API changes at compile time, but not at runtime.
Vaadin also supports building components in Java with the Flow framework. Flow is covered separately for easier comparison. Both frameworks can be used simultaneously in the same application.
React is a JavaScript library for building user interfaces. It includes:
- A component model.
- Reactive, declarative templates.
- Efficient DOM rendering.
React is often considered a framework, but it's in fact a component model.
React's component model is flexible and can be combined with third-party libraries for features, like routing and state management. This allows developers to build "frameworks" that are specific to the project they are working on.
Angular is a frontend web and mobile app framework. It has:
- A TypeScript-based component model.
- Reactive, declarative templates.
- Dependency injection.
- Internationalization.
- Animations.
- Forms.
- Modules.
- Material design UI components.
Angular is a frontend framework with many of the features enterprise developers are used to, like modules and dependency injection. Angular abstracts away from underlying web technologies and introduces several framework-specific concepts.
The size and complexity of the Angular framework can make it more difficult to learn than a component model like React or LitElement that Vaadin uses.
Vue is a frontend web application framework. It has:
- A component model.
- Reactive, declarative templates.
- Virtual DOM-based rendering.
- Routing.
- Forms.
- State management.
Vue is a light-weight frontend framework that contains the essentials for building a web app. In addition to an API for creating individual components, it supports routing and state management for building more complex applications. It also includes support for creating forms.
Discover in practice what makes Vaadin better & learn the business benefits over other frameworks!
Component model
Vaadin components can extend other components, HTML elements (like Div
) or higher-level Vaadin layouts, like VerticalLayout
. Applications and views are built by composing one or more components together.
MainView.java
public class MainView extends VerticalLayout {
public MainView() {
add(new H1("Hello World!"));
}
}
Components are instantiated as Java objects and can be added to layouts with the add()
method. You can interact with components using setters and getters.
Hilla uses the W3C web components standard as its component model through the LitElement library. LitElement is a lightweight library that produces standard web components (custom HTML elements). The components use a reactive template and can be composed to build views.
Components are defined as classes that extend the LitElement base class. The template is defined as a tagged JavaScript template literal.
hello-world.ts
@customElement("hello-world")
export class HelloWorld extends LitElement {
render() {
return html`
<h1>Hello world!</h1>
`;
}
function HelloWorld() {
return <h1>Hello world</h1>;
}
hello-world.component.ts
@Component({
selector: 'app-hello-world',
templateUrl: './hello-world.component.html',
})
export class HelloWorldComponent {}
hello-world.component.html
<h1>Hello world</h1>
Vue components are defined in a .vue
file that contains a template and optionally a script containing a component definition, and a CSS style block.
HelloWorld.vue
<template>
<h1>Hello world</h1>
</template>
<script setup></script>
<style></style>
Vaadin components can extend other components, HTML elements (like Div
) or higher-level Vaadin layouts, like VerticalLayout
. Applications and views are built by composing one or more components together.
MainView.java
public class MainView extends VerticalLayout {
public MainView() {
add(new H1("Hello World!"));
}
}
Components are instantiated as Java objects and can be added to layouts with the add()
method. You can interact with components using setters and getters.
Hilla uses the W3C web components standard as its component model through the LitElement library. LitElement is a lightweight library that produces standard web components (custom HTML elements). The components use a reactive template and can be composed to build views.
Components are defined as classes that extend the LitElement base class. The template is defined as a tagged JavaScript template literal.
hello-world.ts
@customElement("hello-world")
export class HelloWorld extends LitElement {
render() {
return html`
<h1>Hello world!</h1>
`;
}
function HelloWorld() {
return <h1>Hello world</h1>;
}
hello-world.component.ts
@Component({
selector: 'app-hello-world',
templateUrl: './hello-world.component.html',
})
export class HelloWorldComponent {}
hello-world.component.html
<h1>Hello world</h1>
Vue components are defined in a .vue
file that contains a template and optionally a script containing a component definition, and a CSS style block.
HelloWorld.vue
<template>
<h1>Hello world</h1>
</template>
<script setup></script>
<style></style>
Templating
Vaadin Flow is different from the other frameworks in this comparison in that it doesn't require templates. Instead, you can build views programmatically with Java.
Vaadin also supports using Lit templates for declaratively defining components . Templates can be created manually or visually with the Vaadin Designer tool, and have access to the server state and Java methods.
Hilla uses lit-html templates. They are plain HTML inside JavaScript template literals with some added helpers.
React templates are declared using JSX, a JavaScript syntax extension that allows developers to write HTML-like syntax in JavaScript.
Because JSX is not HTML, you need to distinguish between native HTML elements from React components. Native elements, like <div>
, are declared in lower case, whereas React components start with a capital letter, like the <HelloWorld>
example above.
Angular has a comprehensive, framework-specific template syntax that builds on HTML.
Vue uses a template syntax that extends HTML and includes support for declaratively binding to attributes, properties, and events.
Vaadin Flow is different from the other frameworks in this comparison in that it doesn't require templates. Instead, you can build views programmatically with Java.
Vaadin also supports using Lit templates for declaratively defining components . Templates can be created manually or visually with the Vaadin Designer tool, and have access to the server state and Java methods.
Hilla uses lit-html templates. They are plain HTML inside JavaScript template literals with some added helpers.
React templates are declared using JSX, a JavaScript syntax extension that allows developers to write HTML-like syntax in JavaScript.
Because JSX is not HTML, you need to distinguish between native HTML elements from React components. Native elements, like <div>
, are declared in lower case, whereas React components start with a capital letter, like the <HelloWorld>
example above.
Angular has a comprehensive, framework-specific template syntax that builds on HTML.
Vue uses a template syntax that extends HTML and includes support for declaratively binding to attributes, properties, and events.
Templating - Data binding
Text values are set either through the constructor or through setters. It is easy to find the possible properties using autocomplete in your IDE.
new H1("Todo list for " + name);
new Image(profileImage.src, profileImage.alt);
Because Vaadin views are constructed in Java, all APIs are typed. Components, such as selects and data grids, use generics to specify the type of data used.
Grid<Todo> list = new Grid<>(Todo.class);
list.setItems(Arrays.asList(
new Todo("Buy snacks"),
new Todo("Go for a run")
));
You can use the ${}
syntax to bind values to text content or attributes.
<h1>Todo list for ${this.name}!</h1>
<img src=${profileImage.src} alt=${profileImage.alt}>
Templates use standard HTML. This means that you need to differentiate between attributes and properties when binding data in components.
Properties are the programmatic API of a component. They can be any complex data type, like an object or an array. Properties are not reflected in the HTML markup: you will not see them in the source of the page, but they can be inspected with the development tools. Attributes are used to initialize the properties of an element. They are always represented as a string, like the src=""
attribute on <img>
.
The binding syntax depends on the type of attribute or property you are binding to:
- Attribute:
<div id=${...}>
. - Boolean attribute:
<div ?hidden=${...}>
(added when true, removed when false). - Property:
<vaadin-grid .items=${...}>
.
<vaadin-grid
id=${this.id}
.items=${this.people}>
<vaadin-grid-column
header="Name"
path="name"></vaadin-grid-column>
<vaadin-grid-column
header="Email"
path="email"
?hidden=${this.hideEmail}></vaadin-grid-column>
</vaadin-grid>
You can bind values in the template using brackets {}. You can bind values, like variables or functions. Binding works for both text content and properties.
function HelloWorld(props) {
return <h1>Hello {props.name}</h1>;
}
Unlike HTML, JSX does not differentiate between attributes and properties. You can bind both primitive values and complex values, like objects or arrays.
function HelloWorld(props) {
return (
<div>
<img src={props.imgSrc} alt={props.imgAlt} />
<ListComponent array={props.array} />
</div>
);
}
The data binding syntax in Angular depends on the binding type:
{{ variable }}
: Double curly brackets are used to interpolate dynamic values into text content. Interpolation supports simple operations like addition, so long as the value can be converted to a string. You cannot use arbitrary JavaScript, instead Angular has a concept called pipes to transform data.[property]="value"
: Used to bind a value to a property. Values can be primitives or complex data types.
<h1>Todo list for {{username}}</h1>
<img [src]="profileImage.src" [alt]="profileImage.alt">
<app-list-component [todos]="todos"></app-list-component>
{{ variable }}
: Double curly brackets are used to interpolate dynamic values into text content. You can use simple JavaScript expressions within the brackets as long as they evaluate to a string.:property="value"
: a colon before the property name indicates a binding. This is a shorthand for the longerv-bind:property="value"
syntax.
<h1>Hello {{ user.name }} </h1>
<img :src="user.avatar" :alt="user.name">
<ListComponent :todos="user.todos"/>
Text values are set either through the constructor or through setters. It is easy to find the possible properties using autocomplete in your IDE.
new H1("Todo list for " + name);
new Image(profileImage.src, profileImage.alt);
Because Vaadin views are constructed in Java, all APIs are typed. Components, such as selects and data grids, use generics to specify the type of data used.
Grid<Todo> list = new Grid<>(Todo.class);
list.setItems(Arrays.asList(
new Todo("Buy snacks"),
new Todo("Go for a run")
));
You can use the ${}
syntax to bind values to text content or attributes.
<h1>Todo list for ${this.name}!</h1>
<img src=${profileImage.src} alt=${profileImage.alt}>
Templates use standard HTML. This means that you need to differentiate between attributes and properties when binding data in components.
Properties are the programmatic API of a component. They can be any complex data type, like an object or an array. Properties are not reflected in the HTML markup: you will not see them in the source of the page, but they can be inspected with the development tools. Attributes are used to initialize the properties of an element. They are always represented as a string, like the src=""
attribute on <img>
.
The binding syntax depends on the type of attribute or property you are binding to:
- Attribute:
<div id=${...}>
. - Boolean attribute:
<div ?hidden=${...}>
(added when true, removed when false). - Property:
<vaadin-grid .items=${...}>
.
<vaadin-grid
id=${this.id}
.items=${this.people}>
<vaadin-grid-column
header="Name"
path="name"></vaadin-grid-column>
<vaadin-grid-column
header="Email"
path="email"
?hidden=${this.hideEmail}></vaadin-grid-column>
</vaadin-grid>
You can bind values in the template using brackets {}. You can bind values, like variables or functions. Binding works for both text content and properties.
function HelloWorld(props) {
return <h1>Hello {props.name}</h1>;
}
Unlike HTML, JSX does not differentiate between attributes and properties. You can bind both primitive values and complex values, like objects or arrays.
function HelloWorld(props) {
return (
<div>
<img src={props.imgSrc} alt={props.imgAlt} />
<ListComponent array={props.array} />
</div>
);
}
The data binding syntax in Angular depends on the binding type:
{{ variable }}
: Double curly brackets are used to interpolate dynamic values into text content. Interpolation supports simple operations like addition, so long as the value can be converted to a string. You cannot use arbitrary JavaScript, instead Angular has a concept called pipes to transform data.[property]="value"
: Used to bind a value to a property. Values can be primitives or complex data types.
<h1>Todo list for {{username}}</h1>
<img [src]="profileImage.src" [alt]="profileImage.alt">
<app-list-component [todos]="todos"></app-list-component>
{{ variable }}
: Double curly brackets are used to interpolate dynamic values into text content. You can use simple JavaScript expressions within the brackets as long as they evaluate to a string.:property="value"
: a colon before the property name indicates a binding. This is a shorthand for the longerv-bind:property="value"
syntax.
<h1>Hello {{ user.name }} </h1>
<img :src="user.avatar" :alt="user.name">
<ListComponent :todos="user.todos"/>
Templating - Events
add*Listener
API. Events are typed.
Button button = new Button("Click me");
button.addClickListener(e ->
System.out.println("Clicked!"));
You can bind events by adding @ before the event name.
render() {
return html`
<vaadin-button
@click=${this.doThings}>Click me</vaadin-button>
`;
}
doThings() {
console.log("Clicked!");
}
You can listen to events by binding to a handler function. Unlike HTML, event names in JSX are in camel case.
function HelloWorld(props) {
const clickHandler = () => console.log("Clicked!");
return (
<div>
<button
</div>
);
}
You can bind to events by placing the event name in parentheses. You can use the $event
token to pass the event to the handler.
hello-world.component.html
<button (click)="clickHandler($event)">
Click
</button>
hello-world.component.ts
export class HelloWorldComponent {
clickHandler(event: MouseEvent) {
console.log('Clicked!');
}
}
Angular uses RxJS observables for event handling and asynchronous programming.
You can listen to events with the v-on:event
syntax, or the @event
shorthand.
<script setup>
const clickHandler = (e) => {
console.log(e);
}
</script>
<template>
<button @click="clickHandler">Click me</button>
</template>
add*Listener
API. Events are typed.
Button button = new Button("Click me");
button.addClickListener(e ->
System.out.println("Clicked!"));
You can bind events by adding @ before the event name.
render() {
return html`
<vaadin-button
@click=${this.doThings}>Click me</vaadin-button>
`;
}
doThings() {
console.log("Clicked!");
}
You can listen to events by binding to a handler function. Unlike HTML, event names in JSX are in camel case.
function HelloWorld(props) {
const clickHandler = () => console.log("Clicked!");
return (
<div>
<button
</div>
);
}
You can bind to events by placing the event name in parentheses. You can use the $event
token to pass the event to the handler.
hello-world.component.html
<button (click)="clickHandler($event)">
Click
</button>
hello-world.component.ts
export class HelloWorldComponent {
clickHandler(event: MouseEvent) {
console.log('Clicked!');
}
}
Angular uses RxJS observables for event handling and asynchronous programming.
You can listen to events with the v-on:event
syntax, or the @event
shorthand.
<script setup>
const clickHandler = (e) => {
console.log(e);
}
</script>
<template>
<button @click="clickHandler">Click me</button>
</template>
Templating - Conditionals
You can use normal Java syntax, like if-statements, inline if operators or switch-statements, to control what happens in your UI.
Paragraph greeting = new Paragraph("Hello, " + loggedIn ? "friend" : "stranger");
add(greeting);
Button loginButton = new Button(
loggedIn ? "Log out" : "Log in");
add(loginButton);
Use inline if-else conditional operators for simple content and bind to a method for more complex logic.
render() {
return html`
<p>
Hello, ${loggedIn ? "friend" : "stranger"}
</p>
${this.loginButton}
`;
}
// Can also be inline
loginButton() {
if (this.loggedIn) {
return html`<vaadin-button @click=${this.logout}>Log out</vaadin-button>`;
} else {
return html`<vaadin-button @click=${this.login}>Log in</vaadin-button>`;
}
}
Use inline if-else conditional operators for simple content, &&
short-circuiting to toggle larger blocks, or bind to a method for complex logic.
function HelloWorld(props) {
const logout = () => {};
const login = () => {};
const loginButton = () => {
if (props.loggedIn) {
return <button
} else {
return <button
}
};
return (
<div>
<p>Hello, {props.loggedIn ? "friend" : "stranger"}</p>
{!props.loggedIn && <p>Log in to see content</p>}
{loginButton()}
</div>
);
}
The &&
syntax may require a bit more explanation if you are new to JavaScript: JavaScript evaluates true && expression
to expression
, and false && expression
to false
. We can take advantage of this to conditionally display markup.
Angular uses the *ngIf
directive for conditional rendering.
<p>Hello {{loggedIn ? "friend" : "stranger"}}</p>
<button *ngIf="loggedin" (click)="logout()">
Logout
</button>
<button *ngIf="!loggedin" (click)="login()">
Login
</button>
Use the `v-if` directive to display content conditionally.
<script setup>
const loggedIn = true
</script>
<template>
<button v-if="loggedIn">Log out</button>
<button v-else>Log in</button>
</template>
You can use normal Java syntax, like if-statements, inline if operators or switch-statements, to control what happens in your UI.
Paragraph greeting = new Paragraph("Hello, " + loggedIn ? "friend" : "stranger");
add(greeting);
Button loginButton = new Button(
loggedIn ? "Log out" : "Log in");
add(loginButton);
Use inline if-else conditional operators for simple content and bind to a method for more complex logic.
render() {
return html`
<p>
Hello, ${loggedIn ? "friend" : "stranger"}
</p>
${this.loginButton}
`;
}
// Can also be inline
loginButton() {
if (this.loggedIn) {
return html`<vaadin-button @click=${this.logout}>Log out</vaadin-button>`;
} else {
return html`<vaadin-button @click=${this.login}>Log in</vaadin-button>`;
}
}
Use inline if-else conditional operators for simple content, &&
short-circuiting to toggle larger blocks, or bind to a method for complex logic.
function HelloWorld(props) {
const logout = () => {};
const login = () => {};
const loginButton = () => {
if (props.loggedIn) {
return <button
} else {
return <button
}
};
return (
<div>
<p>Hello, {props.loggedIn ? "friend" : "stranger"}</p>
{!props.loggedIn && <p>Log in to see content</p>}
{loginButton()}
</div>
);
}
The &&
syntax may require a bit more explanation if you are new to JavaScript: JavaScript evaluates true && expression
to expression
, and false && expression
to false
. We can take advantage of this to conditionally display markup.
Angular uses the *ngIf
directive for conditional rendering.
<p>Hello {{loggedIn ? "friend" : "stranger"}}</p>
<button *ngIf="loggedin" (click)="logout()">
Logout
</button>
<button *ngIf="!loggedin" (click)="login()">
Login
</button>
Use the `v-if` directive to display content conditionally.
<script setup>
const loggedIn = true
</script>
<template>
<button v-if="loggedIn">Log out</button>
<button v-else>Log in</button>
</template>
Templating - Loops
There are several options to display collections of items: traditional loops, collection operators and component-specific APIs.
for(Todo todo : todos) {
add(new Paragraph(todo.getTask()));
}
Use the JavaScript map operator to create templates for items in an array. You can also use the lit-html repeat-directive for more efficient DOM updates.
<ul>
${this.todos.map(todo => html`<li>${todo.task}</li>`)}
</ul>
Use the JavaScript map operator to create templates for items in an array. You need to define a unique key for each item.
<ul>
{props.todos.map((todo, i) => (
<li key={i}>{todo.task}</li>
))}
</ul>
You can iterate over an array with the *ngFor
directive.
<ul>
<li *ngFor="let todo of todos">{{todo.task}}</li>
</ul>
Use the v-for
directive to repeat items. You need to specify a unique key for each item using :key
to optimize rendering.
<ul>
<li v-for="todo of todos" :key="todo.id">{{ todo.task }}</li>
</ul>
There are several options to display collections of items: traditional loops, collection operators and component-specific APIs.
for(Todo todo : todos) {
add(new Paragraph(todo.getTask()));
}
Use the JavaScript map operator to create templates for items in an array. You can also use the lit-html repeat-directive for more efficient DOM updates.
<ul>
${this.todos.map(todo => html`<li>${todo.task}</li>`)}
</ul>
Use the JavaScript map operator to create templates for items in an array. You need to define a unique key for each item.
<ul>
{props.todos.map((todo, i) => (
<li key={i}>{todo.task}</li>
))}
</ul>
You can iterate over an array with the *ngFor
directive.
<ul>
<li *ngFor="let todo of todos">{{todo.task}}</li>
</ul>
Use the v-for
directive to repeat items. You need to specify a unique key for each item using :key
to optimize rendering.
<ul>
<li v-for="todo of todos" :key="todo.id">{{ todo.task }}</li>
</ul>
State and rendering
Vaadin components maintain their state in class fields or variables.
Vaadin Flow components are not reactive, you need to remember to update the UI whenever the state changes.
TodoView.java
public class TodoView extends VerticalLayout {
private VerticalLayout todoLayout = new VerticalLayout();
TodoView(Person person, List<Todo> todos) {
add(
new H1("Todos for " + person.getName()),
todoLayout
);
setTodos(todos);
}
public void setTodos(List<Todo> todos){
todoLayout.removeAll();
for(Todo todo : todos){
todoLayout.add(new Paragraph(todo.getTask()));
}
}
}
Hilla views use LitElement, which has a reactive programming model. The template is re-rendered every time a declared property changes. The template should be a pure function of the state, that is, it should only depend on the properties of the component and should not cause any side effects, like updating property values.
Use properties to define the component state. Add an @property()
decorator to a class field to declare it as a property.
todo-view.ts
@customElement("todo-view")
export class TodoView extends LitElement {
@state
person: Person = { name: "Marcus" };
@state
todos: Todo[] = [];
protected render() {
return html`
<h1>Todos for ${this.person.name}</h1>
<ul>
${this.todos.map((todo) => html`
<li>
${todo.task}
</li>
`)}
</ul>
`;
}
}
Note that the change detection only looks at the object reference: changing properties on an existing object or manipulating an existing array does not trigger a render. You can use immutable data structures and assign a new copy to the property on changes instead. The spread syntax on arrays and objects is a simple way to create modified copies.
// Incorrect, will not trigger render
this.person.name = "Michelle";
// Correct: triggers render
this.person = {
...this.person,
name: "Michelle"
};
// Incorrect, will not trigger render
this.todos.push({task: "Sleep"});
// Correct, triggers render
this.todos = [...this.todos, {task: "Sleep"}];
React uses a reactive programming model. This means that the template should be a function of the state. The template is re-rendered every time the state changes. The template should only depend on the state of the component and it should not cause any side effects.
Previously, class-based React components used a state property to track the state. The current best practice is to use functional components and hooks for the state of the functional component.
TodoView.js
function TodoView() {
const [person, setTask] = useState({name: "Marcus"});
const [todos, setTodos] = useState([]);
return (
<div className="TodoView">
<h1>Todos for {person.name}</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}
</li>
))}
</ul>
</div>
);
}
The useState hook returns a two-item array containing the current state and a function for updating the state. You need to use the function to update the state, otherwise React will not notice it.
setPerson({name: "Michelle"})
setTodos([...todos, {task: "Sleep"}]);
Angular components can store their state as fields of the TypeScript class. The template re-renders whenever any bound value changes.
hello-world.component.ts
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent {
person: Person = { name: 'Marcus' };
todos: Todo[] = [];
}
hello-world.component.html
<h1>Todos for {{ person.name }}</h1>
<ul>
<li *ngFor="let todo of todos">
{{ todo.task }}
</li>
</ul>
Vue uses a reactive programming model. This means that the template is a function of the state. The template is re-rendered every time the state changes. The template should only depend on the state of the component and it should not cause any side effects.
The state of a component is defined using ref
.
<script setup>
import { ref } from 'vue'
const name = ref('Marcus');
const todos = ref([]);
</script>
<template>
<h1>Todos for {{ name }}</h1>
<ul>
<li v-for="todo of todos" :key="todo.id">{{ todo.task }}</li>
</ul>
</template>
Vaadin components maintain their state in class fields or variables.
Vaadin Flow components are not reactive, you need to remember to update the UI whenever the state changes.
TodoView.java
public class TodoView extends VerticalLayout {
private VerticalLayout todoLayout = new VerticalLayout();
TodoView(Person person, List<Todo> todos) {
add(
new H1("Todos for " + person.getName()),
todoLayout
);
setTodos(todos);
}
public void setTodos(List<Todo> todos){
todoLayout.removeAll();
for(Todo todo : todos){
todoLayout.add(new Paragraph(todo.getTask()));
}
}
}
Hilla views use LitElement, which has a reactive programming model. The template is re-rendered every time a declared property changes. The template should be a pure function of the state, that is, it should only depend on the properties of the component and should not cause any side effects, like updating property values.
Use properties to define the component state. Add an @property()
decorator to a class field to declare it as a property.
todo-view.ts
@customElement("todo-view")
export class TodoView extends LitElement {
@state
person: Person = { name: "Marcus" };
@state
todos: Todo[] = [];
protected render() {
return html`
<h1>Todos for ${this.person.name}</h1>
<ul>
${this.todos.map((todo) => html`
<li>
${todo.task}
</li>
`)}
</ul>
`;
}
}
Note that the change detection only looks at the object reference: changing properties on an existing object or manipulating an existing array does not trigger a render. You can use immutable data structures and assign a new copy to the property on changes instead. The spread syntax on arrays and objects is a simple way to create modified copies.
// Incorrect, will not trigger render
this.person.name = "Michelle";
// Correct: triggers render
this.person = {
...this.person,
name: "Michelle"
};
// Incorrect, will not trigger render
this.todos.push({task: "Sleep"});
// Correct, triggers render
this.todos = [...this.todos, {task: "Sleep"}];
React uses a reactive programming model. This means that the template should be a function of the state. The template is re-rendered every time the state changes. The template should only depend on the state of the component and it should not cause any side effects.
Previously, class-based React components used a state property to track the state. The current best practice is to use functional components and hooks for the state of the functional component.
TodoView.js
function TodoView() {
const [person, setTask] = useState({name: "Marcus"});
const [todos, setTodos] = useState([]);
return (
<div className="TodoView">
<h1>Todos for {person.name}</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}
</li>
))}
</ul>
</div>
);
}
The useState hook returns a two-item array containing the current state and a function for updating the state. You need to use the function to update the state, otherwise React will not notice it.
setPerson({name: "Michelle"})
setTodos([...todos, {task: "Sleep"}]);
Angular components can store their state as fields of the TypeScript class. The template re-renders whenever any bound value changes.
hello-world.component.ts
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent {
person: Person = { name: 'Marcus' };
todos: Todo[] = [];
}
hello-world.component.html
<h1>Todos for {{ person.name }}</h1>
<ul>
<li *ngFor="let todo of todos">
{{ todo.task }}
</li>
</ul>
Vue uses a reactive programming model. This means that the template is a function of the state. The template is re-rendered every time the state changes. The template should only depend on the state of the component and it should not cause any side effects.
The state of a component is defined using ref
.
<script setup>
import { ref } from 'vue'
const name = ref('Marcus');
const todos = ref([]);
</script>
<template>
<h1>Todos for {{ name }}</h1>
<ul>
<li v-for="todo of todos" :key="todo.id">{{ todo.task }}</li>
</ul>
</template>
Forms
Vaadin uses Binder
to bind UI form controls to data. Vaadin supports validation on both the field and the form level. You can also define how to convert between input values and values stored in the model.
Vaadin lets you define validations using Bean Validation annotations on the model object so you can reuse the same validation logic everywhere in your application.
public class Person {
@Pattern(regexp = "Marcus", message = "Your name should be Marcus")
private String firstName;
@Email(message = "Email is not valid")
private String email;
// getters and setters
}
@Component
public class Service {
public Person getPerson() {
return new Person();
}
public void savePerson(Person person) {
// save to DB
}
}
FormView.java
public class FormView extends VerticalLayout {
TextField firstName = new TextField("First Name");
EmailField email = new EmailField("Email");
public FormView(Service service) {
Person model = service.getPerson();
Binder<Person> binder = new BeanValidationBinder<>(Person.class);
// Binds view fields to the model based on name
binder.bindInstanceFields(this);
binder.readBean(model);
Button saveButton = new Button("Save", e -> {
if (binder.writeBeanIfValid(model)) {
service.savePerson(model);
}
});
add(firstName, email, saveButton);
}
}
Vaadin uses Binder
to bind UI form controls to data. Vaadin supports validation on both the field and form level.
You define validation constraints using Bean Validation annotations on your backend Java model, and use them both for client-side and server-side validation. Vaadin will automatically re-run the validations on the server for added security.
Note: Client-side forms are available in Vaadin 17 and later.
public class Person {
@Pattern(regexp = "Marcus", message = "Your name should be Marcus")
private String firstName;
@Email(message = "Email is not valid")
private String email;
// getters and setters
}
@Endpoint
@AnonymousAllowed
public class Service {
public Person getPerson() {
return new Person();
}
public void savePerson(Person person) {
// save to DB
}
}
import { Binder, field } from "@vaadin/form";
import PersonModel from "../generated/com/example/application/backend/PersonModel";
import { savePerson } from "../generated/Service";
@customElement("form-view")
export class FormView extends LitElement {
private binder = new Binder(this, PersonModel);
save() {
this.binder.submitTo(savePerson);
}
render() {
return html`
<vaadin-text-field
label="First name"
${field(this.binder.model.firstName)}>
</vaadin-text-field>
<vaadin-email-field
label="Email"
${field(this.binder.model.email)}>
</vaadin-email-field>
<vaadin-button @click=${this.save}>
Save
</vaadin-button>
`;
}
}
You can also add client-side only validations if you do not have a corresponding Java bean or want to perform additional validation only on the client.
To create forms with React, you bind input fields to a model value and update the model based on the input. This pattern is called "controlled components".
In the following example, the person model is bound to two input fields. On the change event, the model is updated based on the name attribute on the input, using a computed property name.
const emailRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const API_URL = "http://localhost:8181/api";
export default function NameForm() {
const [person, setPerson] = useState({
name: "Marcus",
email: "demo@example.com",
});
const [errors, setErrors] = useState([]);
const updateModel = (e) => {
setPerson({
...person,
[e.target.name]: e.target.value,
});
};
const isValid = () => {
const validationErrors = [];
if (person.name !== "Marcus") {
validationErrors.push("Your name should be Marcus");
}
if (!emailRegExp.test(person.email)) {
validationErrors.push("Email must be valid");
}
setErrors(validationErrors);
return validationErrors.length === 0;
};
const submit = async (e) => {
e.preventDefault();
if (isValid()) {
try {
await fetch(API_URL, {
method: "POST",
body: person,
});
setErrors([]);
setPerson({ name: "", email: "" });
} catch (e) {
setErrors(["Failed to save to server, please try again"]);
}
}
};
return (
<form className="NameForm"
<label>
Name
<input name="name" value={person.name} />
</label>
<label>
Email
<input name="email" value={person.email} />
</label>
<button type="submit">Save</button>
<p className="validation-errors">{errors.join(", ")}</p>
</form>
);
}
You can validate the form on submit. You need to remember to re-validate the values in the backend.
For more complex forms it's easier to use an external form library. There are several form libraries for React, such as Formik, but not all component sets and form libraries are compatible.
There are two ways to define forms in Angular: template-driven and reactive.
Reactive forms are the recommended approach. Forms need to be explicitly enabled by importing ReactiveFormsModule
into the module your component is in.
Forms support single-field and cross-field validators.
You need to remember to re-validate data in the backend service.
person.ts
export interface Person {
firstName: string;
email: string;
}
backend.service.ts
@Injectable({
providedIn: 'root',
})
export class Service {
constructor(private http: HttpClient) {}
getPerson(): Observable<Person> {
return this.http.get<Person>(API_URL);
}
savePerson(person: Person) {
this.http.post(API_URL, person);
}
}
form.component.ts
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {
constructor(private service: BackendService) {}
nameForm = new FormGroup({
firstName: new FormControl('', [
Validators.required,
nameEquals('Marcus')
]),
email: new FormControl('', [
Validators.email
]),
});
// Getter for accessing first name control in template
get firstName() {
return this.nameForm.get('firstName');
}
// Getter for accessing email control in template
get email() {
return this.nameForm.get('email');
}
save() {
if (this.nameForm.valid) {
this.service.savePerson(this.nameForm.value);
}
}
ngOnInit(): void {
this.service.getPerson().subscribe((person) => {
this.nameForm.patchValue(person);
});
}
}
name-equals.directive.ts
export function nameEquals(name: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
return name === control.value
? null
: { forbiddenName: { value: control.value } };
};
}
form.component.html
<form [formGroup]="nameForm" (ngSubmit)="save()">
<label>
First name:
<input type="text" formControlName="firstName" />
<div
*ngIf="firstName.invalid && (firstName.dirty || firstName.touched)"
class="alert alert-danger">
<div *ngIf="firstName.errors.required">
Name is required.
</div>
<div *ngIf="firstName.errors.forbiddenName">
Your name should be Marcus.
</div>
</div>
</label>
<label>
Email:
<input type="text" formControlName="email" />
<div
*ngIf="email.invalid && (email.dirty || email.touched)"
class="alert alert-danger">
<div *ngIf="email.errors.email">
Email must be valid
</div>
</div>
</label>
<button type="submit">Save</button>
</form>
Vue includes basic form binding support. You can use two-way binding between input fields and a model with the v-model
directive. You need to take care of validating form input on submit. Remember to re-validate input on the server.
<script setup>
import { ref } from "vue";
const emailRegExp =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const API_URL = "http://localhost:8181/api";
const person = ref({ name: "Marcus", email: "demo@example.com" });
const errors = ref([]);
const validate = () => {
errors.value = [];
if (person.name !== "Marcus") {
errors.value.push("Your name should be Marcus");
}
if (!emailRegExp.test(this.person.email)) {
errors.value.push("Email must be valid");
}
return errors.length === 0;
};
const submit = async () => {
if (validate()) {
try {
await fetch(API_URL, {
method: "POST",
body: this.person,
});
errors.value = [];
person.value = { name: "", email: "" };
} catch {
errors.value.push("Failed to save to server, please try again");
}
}
};
</script>
<template>
<form @submit.prevent="submit">
<label>
Name
<input name="name" v-model="person.name" />
</label>
<label>
Email
<input name="email" v-model="person.email" />
</label>
<button type="submit">Save</button>
<p className="validation-errors"> {{ errors.join(", ") }} </p>
</form>
</template>
Vaadin uses Binder
to bind UI form controls to data. Vaadin supports validation on both the field and the form level. You can also define how to convert between input values and values stored in the model.
Vaadin lets you define validations using Bean Validation annotations on the model object so you can reuse the same validation logic everywhere in your application.
public class Person {
@Pattern(regexp = "Marcus", message = "Your name should be Marcus")
private String firstName;
@Email(message = "Email is not valid")
private String email;
// getters and setters
}
@Component
public class Service {
public Person getPerson() {
return new Person();
}
public void savePerson(Person person) {
// save to DB
}
}
FormView.java
public class FormView extends VerticalLayout {
TextField firstName = new TextField("First Name");
EmailField email = new EmailField("Email");
public FormView(Service service) {
Person model = service.getPerson();
Binder<Person> binder = new BeanValidationBinder<>(Person.class);
// Binds view fields to the model based on name
binder.bindInstanceFields(this);
binder.readBean(model);
Button saveButton = new Button("Save", e -> {
if (binder.writeBeanIfValid(model)) {
service.savePerson(model);
}
});
add(firstName, email, saveButton);
}
}
Vaadin uses Binder
to bind UI form controls to data. Vaadin supports validation on both the field and form level.
You define validation constraints using Bean Validation annotations on your backend Java model, and use them both for client-side and server-side validation. Vaadin will automatically re-run the validations on the server for added security.
Note: Client-side forms are available in Vaadin 17 and later.
public class Person {
@Pattern(regexp = "Marcus", message = "Your name should be Marcus")
private String firstName;
@Email(message = "Email is not valid")
private String email;
// getters and setters
}
@Endpoint
@AnonymousAllowed
public class Service {
public Person getPerson() {
return new Person();
}
public void savePerson(Person person) {
// save to DB
}
}
import { Binder, field } from "@vaadin/form";
import PersonModel from "../generated/com/example/application/backend/PersonModel";
import { savePerson } from "../generated/Service";
@customElement("form-view")
export class FormView extends LitElement {
private binder = new Binder(this, PersonModel);
save() {
this.binder.submitTo(savePerson);
}
render() {
return html`
<vaadin-text-field
label="First name"
${field(this.binder.model.firstName)}>
</vaadin-text-field>
<vaadin-email-field
label="Email"
${field(this.binder.model.email)}>
</vaadin-email-field>
<vaadin-button @click=${this.save}>
Save
</vaadin-button>
`;
}
}
You can also add client-side only validations if you do not have a corresponding Java bean or want to perform additional validation only on the client.
To create forms with React, you bind input fields to a model value and update the model based on the input. This pattern is called "controlled components".
In the following example, the person model is bound to two input fields. On the change event, the model is updated based on the name attribute on the input, using a computed property name.
const emailRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const API_URL = "http://localhost:8181/api";
export default function NameForm() {
const [person, setPerson] = useState({
name: "Marcus",
email: "demo@example.com",
});
const [errors, setErrors] = useState([]);
const updateModel = (e) => {
setPerson({
...person,
[e.target.name]: e.target.value,
});
};
const isValid = () => {
const validationErrors = [];
if (person.name !== "Marcus") {
validationErrors.push("Your name should be Marcus");
}
if (!emailRegExp.test(person.email)) {
validationErrors.push("Email must be valid");
}
setErrors(validationErrors);
return validationErrors.length === 0;
};
const submit = async (e) => {
e.preventDefault();
if (isValid()) {
try {
await fetch(API_URL, {
method: "POST",
body: person,
});
setErrors([]);
setPerson({ name: "", email: "" });
} catch (e) {
setErrors(["Failed to save to server, please try again"]);
}
}
};
return (
<form className="NameForm"
<label>
Name
<input name="name" value={person.name} />
</label>
<label>
Email
<input name="email" value={person.email} />
</label>
<button type="submit">Save</button>
<p className="validation-errors">{errors.join(", ")}</p>
</form>
);
}
You can validate the form on submit. You need to remember to re-validate the values in the backend.
For more complex forms it's easier to use an external form library. There are several form libraries for React, such as Formik, but not all component sets and form libraries are compatible.
There are two ways to define forms in Angular: template-driven and reactive.
Reactive forms are the recommended approach. Forms need to be explicitly enabled by importing ReactiveFormsModule
into the module your component is in.
Forms support single-field and cross-field validators.
You need to remember to re-validate data in the backend service.
person.ts
export interface Person {
firstName: string;
email: string;
}
backend.service.ts
@Injectable({
providedIn: 'root',
})
export class Service {
constructor(private http: HttpClient) {}
getPerson(): Observable<Person> {
return this.http.get<Person>(API_URL);
}
savePerson(person: Person) {
this.http.post(API_URL, person);
}
}
form.component.ts
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {
constructor(private service: BackendService) {}
nameForm = new FormGroup({
firstName: new FormControl('', [
Validators.required,
nameEquals('Marcus')
]),
email: new FormControl('', [
Validators.email
]),
});
// Getter for accessing first name control in template
get firstName() {
return this.nameForm.get('firstName');
}
// Getter for accessing email control in template
get email() {
return this.nameForm.get('email');
}
save() {
if (this.nameForm.valid) {
this.service.savePerson(this.nameForm.value);
}
}
ngOnInit(): void {
this.service.getPerson().subscribe((person) => {
this.nameForm.patchValue(person);
});
}
}
name-equals.directive.ts
export function nameEquals(name: string): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
return name === control.value
? null
: { forbiddenName: { value: control.value } };
};
}
form.component.html
<form [formGroup]="nameForm" (ngSubmit)="save()">
<label>
First name:
<input type="text" formControlName="firstName" />
<div
*ngIf="firstName.invalid && (firstName.dirty || firstName.touched)"
class="alert alert-danger">
<div *ngIf="firstName.errors.required">
Name is required.
</div>
<div *ngIf="firstName.errors.forbiddenName">
Your name should be Marcus.
</div>
</div>
</label>
<label>
Email:
<input type="text" formControlName="email" />
<div
*ngIf="email.invalid && (email.dirty || email.touched)"
class="alert alert-danger">
<div *ngIf="email.errors.email">
Email must be valid
</div>
</div>
</label>
<button type="submit">Save</button>
</form>
Vue includes basic form binding support. You can use two-way binding between input fields and a model with the v-model
directive. You need to take care of validating form input on submit. Remember to re-validate input on the server.
<script setup>
import { ref } from "vue";
const emailRegExp =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const API_URL = "http://localhost:8181/api";
const person = ref({ name: "Marcus", email: "demo@example.com" });
const errors = ref([]);
const validate = () => {
errors.value = [];
if (person.name !== "Marcus") {
errors.value.push("Your name should be Marcus");
}
if (!emailRegExp.test(this.person.email)) {
errors.value.push("Email must be valid");
}
return errors.length === 0;
};
const submit = async () => {
if (validate()) {
try {
await fetch(API_URL, {
method: "POST",
body: this.person,
});
errors.value = [];
person.value = { name: "", email: "" };
} catch {
errors.value.push("Failed to save to server, please try again");
}
}
};
</script>
<template>
<form @submit.prevent="submit">
<label>
Name
<input name="name" v-model="person.name" />
</label>
<label>
Email
<input name="email" v-model="person.email" />
</label>
<button type="submit">Save</button>
<p className="validation-errors"> {{ errors.join(", ") }} </p>
</form>
</template>
Styles
You can use CSS to style Vaadin apps. Use addClassName to add a CSS class name to a component and @CssImport
to load a CSS stylesheet. Styles are not scoped to the component, so remember to prefix any component-specific styles with the component class name.
StyledComponent.java
@CssImport("./styles/styled-component.css")
public class StyledComponent extends Div {
public StyledComponent() {
addClassName("styled-component");
}
}
styled-component.css
.styled-component {
background: red;
}
You can define CSS styles for the component by using the static styles
property. The :host selector refers to the component itself. Styles are scoped to the component using shadow DOM.
styled-component.ts
export class StyledComponent extends LitElement {
static styles = css`
:host {
background: red;
}
`;
render() {
return html` <h1>Hello world!</h1> `;
}
}
React has no prescribed way to load CSS. The simplest way is to import a CSS file and add classes to components. Note that you need to use className
instead of class in JSX, because class is a reserved keyword in JavaScript. Styles are not scoped to the component by default, so remember to define a class name for your component and use it to prefix CSS selectors.
StyledComponent.jsx
import React from "react";
import './StyledComponent.css';
function StyledComponent() {
return (
<div className="StyledComponent">
<h1>Hello world</h1>
</div>
);
}
StyledComponent.css
.StyledComponent {
background: red;
}
You can load a stylesheet in the @Component
decorator. Styles are scoped to the component.
styled.component.ts
@Component({
selector: 'app-styled',
templateUrl: './styled.component.html',
styleUrls: ['./styled.component.css'],
})
export class StyledComponent {
}
styled.component.css
:host {
background: red;
}
The :host
selector refers to the component itself, similar to Shadow DOM styling.
You can define styles for the component in the <style>
block. You can additionally add a scoped
attribute to the tag if you want to scope styles to the component.
<template>
<h1>Hello world</h1>
</template>
<style scoped>
h1 {
color: hotpink;
}
</style>
You can use CSS to style Vaadin apps. Use addClassName to add a CSS class name to a component and @CssImport
to load a CSS stylesheet. Styles are not scoped to the component, so remember to prefix any component-specific styles with the component class name.
StyledComponent.java
@CssImport("./styles/styled-component.css")
public class StyledComponent extends Div {
public StyledComponent() {
addClassName("styled-component");
}
}
styled-component.css
.styled-component {
background: red;
}
You can define CSS styles for the component by using the static styles
property. The :host selector refers to the component itself. Styles are scoped to the component using shadow DOM.
styled-component.ts
export class StyledComponent extends LitElement {
static styles = css`
:host {
background: red;
}
`;
render() {
return html` <h1>Hello world!</h1> `;
}
}
React has no prescribed way to load CSS. The simplest way is to import a CSS file and add classes to components. Note that you need to use className
instead of class in JSX, because class is a reserved keyword in JavaScript. Styles are not scoped to the component by default, so remember to define a class name for your component and use it to prefix CSS selectors.
StyledComponent.jsx
import React from "react";
import './StyledComponent.css';
function StyledComponent() {
return (
<div className="StyledComponent">
<h1>Hello world</h1>
</div>
);
}
StyledComponent.css
.StyledComponent {
background: red;
}
You can load a stylesheet in the @Component
decorator. Styles are scoped to the component.
styled.component.ts
@Component({
selector: 'app-styled',
templateUrl: './styled.component.html',
styleUrls: ['./styled.component.css'],
})
export class StyledComponent {
}
styled.component.css
:host {
background: red;
}
The :host
selector refers to the component itself, similar to Shadow DOM styling.
You can define styles for the component in the <style>
block. You can additionally add a scoped
attribute to the tag if you want to scope styles to the component.
<template>
<h1>Hello world</h1>
</template>
<style scoped>
h1 {
color: hotpink;
}
</style>
Backend communication
Vaadin Flow views run on the server. This gives you direct access to any services available on the JVM, without needing to expose them as REST services. In many cases, Vaadin is used together with a dependency-injection container like Spring, which makes it easy to access backend services.
There is no serialization overhead when calling services, and you have full type safety. It is easy to debug program flow all the way from the view down to the database.
TodoList.java
@Component
@Scope("prototype")public class TodoList extends VerticalLayout {
private List<Todo> todos = new ArrayList<>();
// TodoService is injected by Spring
public TodoList(@Autowired TodoService service) {
setTodos(service.getTodos());
}
...
}
Hilla uses asynchronous, type-safe endpoints to communicate with the backend. The framework generates TypeScript interfaces for all Java data types, so you have full-stack type checking.
todo-view.ts
import {getTodos} from "../generated/Service";
@customElement("todo-view")
class TodoList extends LitElement {
@property()
private todos: Todo[] = [];
async firstUpdated() {
this.todos = await getTodos();
}
TodoService.java (running on the server)
@Endpoint
public class TodoService {
public List<Todo> getTodos() {
return todos;
}
}
React has no opinion on how you connect to your backend. The most common way is to use the browser fetch API to call a REST API.
In a functional component, you should use an effect hook for the call.
function TodoApp() {
const [todos, setTodos] = useState([]);
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
return (
<div className="TodoApp">
...
</div>
);
}
Angular uses HttpClient
for server communication. Requests can be typed and they return an observable stream. To use HttpClient
, you need to first import the HttpClientModule
into your Angular module. You can then inject it in the component constructor.
export class TodoViewComponent implements OnInit {
todos: Todo[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
}
HttpClient
returns an RxJS observable.
Vue has no opinion on how you connect to your backend. The most common way is to use the browser fetch API to call a REST API.
<script setup>
import { ref } from "vue";
const todos = ref([]);
const fetchData = async () => {
const result = await fetch(API_URL);
todos.value = await result.json();
}
fetchData();
</script>
Vaadin Flow views run on the server. This gives you direct access to any services available on the JVM, without needing to expose them as REST services. In many cases, Vaadin is used together with a dependency-injection container like Spring, which makes it easy to access backend services.
There is no serialization overhead when calling services, and you have full type safety. It is easy to debug program flow all the way from the view down to the database.
TodoList.java
@Component
@Scope("prototype")public class TodoList extends VerticalLayout {
private List<Todo> todos = new ArrayList<>();
// TodoService is injected by Spring
public TodoList(@Autowired TodoService service) {
setTodos(service.getTodos());
}
...
}
Hilla uses asynchronous, type-safe endpoints to communicate with the backend. The framework generates TypeScript interfaces for all Java data types, so you have full-stack type checking.
todo-view.ts
import {getTodos} from "../generated/Service";
@customElement("todo-view")
class TodoList extends LitElement {
@property()
private todos: Todo[] = [];
async firstUpdated() {
this.todos = await getTodos();
}
TodoService.java (running on the server)
@Endpoint
public class TodoService {
public List<Todo> getTodos() {
return todos;
}
}
React has no opinion on how you connect to your backend. The most common way is to use the browser fetch API to call a REST API.
In a functional component, you should use an effect hook for the call.
function TodoApp() {
const [todos, setTodos] = useState([]);
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
return (
<div className="TodoApp">
...
</div>
);
}
Angular uses HttpClient
for server communication. Requests can be typed and they return an observable stream. To use HttpClient
, you need to first import the HttpClientModule
into your Angular module. You can then inject it in the component constructor.
export class TodoViewComponent implements OnInit {
todos: Todo[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
}
HttpClient
returns an RxJS observable.
Vue has no opinion on how you connect to your backend. The most common way is to use the browser fetch API to call a REST API.
<script setup>
import { ref } from "vue";
const todos = ref([]);
const fetchData = async () => {
const result = await fetch(API_URL);
todos.value = await result.json();
}
fetchData();
</script>
Routing
@Route
annotation. The router supports nested views and view parameters.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
MainView.java
// localhost:8080
@Route("")
class MainView extends VerticalLayout {
public MainView() {
add(new H1("Main view"));
}
}
UsersLayout.java
@RoutePrefix("users")
public class UsersLayout
extends VerticalLayout
implements RouterLayout {
public UsersLayout() {
add(new H1("Users view"));
}
}
UserListView.java
// localhost:8080/users
@Route(value = "", layout = UsersLayout.class)
class UserListView extends VerticalLayout {
public UserListView() {
add(new H2("User list"));
}
}
UserProfileView.java
// localhost:8080/users/ae658c08d
@Route(value = "", layout = UsersLayout.class)
class UserProfileView extends VerticalLayout
implements HasUrlParameter<String> {
public UserProfileView() {
add(new H2("User profile"));
}
@Override
public void setParameter(BeforeEvent event,
@WildcardParameter String parameter) {
if (parameter.isEmpty()) {
add(new Paragraph("User id: not set"));
} else {
add(new Paragraph("User id: " + parameter));
}
}
}
Vaadin includes a router that supports nested routes, parameters, redirects, code splitting and actions.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
index.ts
const routes: Route[] = [
{
path: "users",
component: "user-layout",
children: [
{ path: ":userId", component: "user-profile" },
{ path: "", component: "user-list" },
],
},
{ path: "", component: "main-view" },
];
const router = new Router(document.querySelector("#outlet"));
router.setRoutes(routes);
main-view.ts
@customElement("main-view")
export class MainView extends LitElement {
render() {
return html`
<h1>Main view</h1>
`;
}
}
user-layout.ts
@customElement("user-layout")
export class UserLayout extends LitElement {
render() {
return html`
<h1>Users view</h1>
<slot></slot>
`;
}
}
user-list.ts
@customElement("user-list")
export class UserList extends LitElement {
render() {
return html`
<h2>User list</h2>
`;
}
}
user-profile.ts
@customElement("user-profile")
export class UserProfile extends LitElement implements BeforeEnterObserver {
@property({ type: String })
userId: string | undefined;
onBeforeEnter(location: RouterLocation) {
this.userId = location.params.userId as string;
}
render() {
return html`
<h2>User profile</h2>
<p>User id: ${this.userId ? this.userId : "not set"}</p>
`;
}
}
Angular has a router that supports nested routes and checks before and after navigation. Larger apps can be composed of several router modules that can be lazily loaded.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
app-routing.module.ts
const routes: Routes = [
{
path: 'users',
component: UserLayoutComponent,
children: [
{ path: ':userId', component: UserProfileComponent },
{ path: '', component: UserListComponent },
],
},
{ path: '', component: MainViewComponent, pathMatch: 'full' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
main-view.component.ts
@Component({
selector: 'app-main-view',
templateUrl: './main-view.component.html',
})
export class MainViewComponent {
constructor() {}
}
main-view.component.html
<h1>Main view</h1>
user-layout.component.ts
@Component({
selector: 'app-user-layout',
templateUrl: './user-layout.component.html'
})
export class UserLayoutComponent {
constructor() { }
}
user-layout.component.html
<h1>User view</h1>
<router-outlet></router-outlet>
user-list.component.ts
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent {
constructor() { }
}
user-list.component.html
<h2>User list</h2>
user-profile.component.ts
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
})
export class UserProfileComponent implements OnInit {
userId: string | undefined;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.params.subscribe((params) => {
this.userId = params.userId;
});
}
}
user-profile.component.html
<h2>User profile</h2>
<p>User id: {{ userId ? userId : 'not set'}}</p>
Vue has an official router called Vue Router. It is not included in the core framework, so you need to install it separately with `npm install vue-router`.Vue Router supports nested routes, parameters, redirects, actions, and code splitting.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
const routes = [
{
path: "/users",
component: UserLayout,
children: [
{ path: ":id", component: UserProfile },
{ path: "", component: UserList },
],
},
{ path: "/", component: MainView },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
createApp(App)
.use(router)
.mount("#app");
MainView.vue
<template>
<h1>Main View</h1>
</template>
UserLayout.vue
<template>
<h1>Users view</h1>
<router-view></router-view>
</template>
UserList.vue
<template>
<h2>User list</h2>
</template>
UserProfile.vue
<script setup>
import { useRoute } from 'vue-router';
import { ref, watch } from 'vue';
const route = useRoute();
const userId = ref('');
watch(
() => route.params.userId,
async newId => userId.value = await fetchUser(newId)
);
</script>
<template>
<h2>User profile</h2>
<p>User id: not set</p>
</template>
@Route
annotation. The router supports nested views and view parameters.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
MainView.java
// localhost:8080
@Route("")
class MainView extends VerticalLayout {
public MainView() {
add(new H1("Main view"));
}
}
UsersLayout.java
@RoutePrefix("users")
public class UsersLayout
extends VerticalLayout
implements RouterLayout {
public UsersLayout() {
add(new H1("Users view"));
}
}
UserListView.java
// localhost:8080/users
@Route(value = "", layout = UsersLayout.class)
class UserListView extends VerticalLayout {
public UserListView() {
add(new H2("User list"));
}
}
UserProfileView.java
// localhost:8080/users/ae658c08d
@Route(value = "", layout = UsersLayout.class)
class UserProfileView extends VerticalLayout
implements HasUrlParameter<String> {
public UserProfileView() {
add(new H2("User profile"));
}
@Override
public void setParameter(BeforeEvent event,
@WildcardParameter String parameter) {
if (parameter.isEmpty()) {
add(new Paragraph("User id: not set"));
} else {
add(new Paragraph("User id: " + parameter));
}
}
}
Vaadin includes a router that supports nested routes, parameters, redirects, code splitting and actions.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
index.ts
const routes: Route[] = [
{
path: "users",
component: "user-layout",
children: [
{ path: ":userId", component: "user-profile" },
{ path: "", component: "user-list" },
],
},
{ path: "", component: "main-view" },
];
const router = new Router(document.querySelector("#outlet"));
router.setRoutes(routes);
main-view.ts
@customElement("main-view")
export class MainView extends LitElement {
render() {
return html`
<h1>Main view</h1>
`;
}
}
user-layout.ts
@customElement("user-layout")
export class UserLayout extends LitElement {
render() {
return html`
<h1>Users view</h1>
<slot></slot>
`;
}
}
user-list.ts
@customElement("user-list")
export class UserList extends LitElement {
render() {
return html`
<h2>User list</h2>
`;
}
}
user-profile.ts
@customElement("user-profile")
export class UserProfile extends LitElement implements BeforeEnterObserver {
@property({ type: String })
userId: string | undefined;
onBeforeEnter(location: RouterLocation) {
this.userId = location.params.userId as string;
}
render() {
return html`
<h2>User profile</h2>
<p>User id: ${this.userId ? this.userId : "not set"}</p>
`;
}
}
Angular has a router that supports nested routes and checks before and after navigation. Larger apps can be composed of several router modules that can be lazily loaded.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
app-routing.module.ts
const routes: Routes = [
{
path: 'users',
component: UserLayoutComponent,
children: [
{ path: ':userId', component: UserProfileComponent },
{ path: '', component: UserListComponent },
],
},
{ path: '', component: MainViewComponent, pathMatch: 'full' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
main-view.component.ts
@Component({
selector: 'app-main-view',
templateUrl: './main-view.component.html',
})
export class MainViewComponent {
constructor() {}
}
main-view.component.html
<h1>Main view</h1>
user-layout.component.ts
@Component({
selector: 'app-user-layout',
templateUrl: './user-layout.component.html'
})
export class UserLayoutComponent {
constructor() { }
}
user-layout.component.html
<h1>User view</h1>
<router-outlet></router-outlet>
user-list.component.ts
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html'
})
export class UserListComponent {
constructor() { }
}
user-list.component.html
<h2>User list</h2>
user-profile.component.ts
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
})
export class UserProfileComponent implements OnInit {
userId: string | undefined;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.params.subscribe((params) => {
this.userId = params.userId;
});
}
}
user-profile.component.html
<h2>User profile</h2>
<p>User id: {{ userId ? userId : 'not set'}}</p>
Vue has an official router called Vue Router. It is not included in the core framework, so you need to install it separately with `npm install vue-router`.Vue Router supports nested routes, parameters, redirects, actions, and code splitting.
As an example, here is the routing configuration for the following view structure:
/
- main view- [parent layout for user views]
/users
- user list view/users/<id>
- user profile view
const routes = [
{
path: "/users",
component: UserLayout,
children: [
{ path: ":id", component: UserProfile },
{ path: "", component: UserList },
],
},
{ path: "/", component: MainView },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
createApp(App)
.use(router)
.mount("#app");
MainView.vue
<template>
<h1>Main View</h1>
</template>
UserLayout.vue
<template>
<h1>Users view</h1>
<router-view></router-view>
</template>
UserList.vue
<template>
<h2>User list</h2>
</template>
UserProfile.vue
<script setup>
import { useRoute } from 'vue-router';
import { ref, watch } from 'vue';
const route = useRoute();
const userId = ref('');
watch(
() => route.params.userId,
async newId => userId.value = await fetchUser(newId)
);
</script>
<template>
<h2>User profile</h2>
<p>User id: not set</p>
</template>
Included Components
Vaadin includes a design system of 40+web components that can be customized to your liking.
There are also third-party web component design systems and component libraries, for instance, Adobe Spectrum and SAP UI5, that can be used with Vaadin.
Vue does not include components. There are third-party component libraries for Vue, and many developers build their own components.
Vaadin includes a design system of 40+web components that can be customized to your liking.
There are also third-party web component design systems and component libraries, for instance, Adobe Spectrum and SAP UI5, that can be used with Vaadin.
Vue does not include components. There are third-party component libraries for Vue, and many developers build their own components.
Sample application
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
TodoApp.java
@Route("")
public class TodoApp extends VerticalLayout {
private TextField task = new TextField("Task");
private Button button = new Button("Add");
private UnorderedList taskList = new UnorderedList();
private TodoService service;
private Binder<Todo> binder = new BeanValidationBinder<>(Todo.class);
TodoApp(TodoService service) {
this.service = service;
HorizontalLayout form = new HorizontalLayout(task, button);
form.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
add(
new H1("Todo"),
form,
taskList
);
binder.bindInstanceFields(this);
button.addClickListener(this::addTask);
updateTasks();
}
private void updateTasks() {
taskList.removeAll();
for (Todo todo : service.getTodos()) {
HorizontalLayout taskLayout = new HorizontalLayout(
new Span(todo.getTask()),
new Button("Delete", e -> deleteTask(todo.getId()))
);
taskLayout.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
taskList.add(new ListItem(taskLayout));
}
}
private void addTask(ClickEvent<Button> e) {
Todo todo = new Todo();
if (binder.writeBeanIfValid(todo)) {
service.saveTodo(todo);
binder.readBean(new Todo());
updateTasks();
}
}
private void deleteTask(Long id) {
service.deleteTodo(id);
updateTasks();
}
}
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
todo-view.ts
@customElement("todo-view")
export class TodoView extends LitElement {
@property({ type: Array })
private todos: Todo[] = [];
private binder = new Binder(this, TodoModel);
protected render() {
return html`
<h1>Todo</h1>
<div class="form">
<vaadin-text-field
label="Task"
...=${field(this.binder.model.task)}
></vaadin-text-field>
<vaadin-button @click=${this.add}>Add</vaadin-button>
</div>
<ul>
${this.todos.map((todo) => html`
<li>
${todo.task}
<vaadin-button @click=${() => this.clear(todo.id)}>
Delete
</vaadin-button>
</li>
`)}
</ul>
`;
}
async firstUpdated() {
this.todos = await getTodos();
}
async add() {
const saved = await this.binder.submitTo(saveTodo);
if (saved) {
this.todos = [...this.todos, saved];
this.binder.clear();
}
}
async clear(id: any) {
await deleteTodo(id);
this.todos = this.todos.filter((t) => t.id !== id);
}
}
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
TodoApp.jsx
function TodoApp() {
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
const [error, setError] = useState("");
const addTodo = async (e) => {
e.preventDefault();
setError("");
if (!task) {
setError("Task cannot be empty");
return;
}
const res = await fetch(API_URL, { method: "POST", body: task });
setTodos([...todos, await res.json()]);
setTask("");
};
const clearTodo = async (id) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
setTodos(todos.filter((t) => t.id !== id));
};
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
return (
<div className="TodoApp">
<h1>Todo</h1>
<form
<input
type="text"
value={task}
=> setTask(e.target.value)} />
<div className="errors">{error}</div>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}{" "}
<button => clearTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
todo-view.component.ts
const API_URL = 'https://vaadin-todo-api.herokuapp.com/todos';
interface Todo {
id?: number;
task: string;
}
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent implements OnInit {
taskForm = new FormGroup({
task: new FormControl('', [
Validators.required
]),
});
todos: Todo[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
async addTodo(taskForm: FormGroup, formDirective: FormGroupDirective) {
const { task } = taskForm.value;
this.http.post<Todo>(API_URL, task).subscribe((todo: Todo) => {
this.todos = [...this.todos, todo];
// Double reset workaround needed to reset form validations
// https://github.com/angular/components/issues/4190
taskForm.reset();
formDirective.resetForm();
});
}
async clearTodo(id: string) {
this.http
.delete(`${API_URL}/${id}`)
.subscribe((_) => (this.todos = this.todos.filter((t) => t.id !== id)));
}
get task() {
return this.taskForm.get('task');
}
}
todo-view.component.html
<h1>Todo</h1>
<form
[formGroup]="taskForm"
#formDirective="ngForm"
(ngSubmit)="addTodo(taskForm, formDirective)"
>
<mat-form-field>
<input matInput type="text" formControlName="task" />
<mat-error *ngIf="task.invalid">Task cannot be empty</mat-error>
</mat-form-field>
<button mat-button type="submit">Add</button>
</form>
<ul>
<li *ngFor="let todo of todos">
{{ todo.task }}
<button mat-button (click)="clearTodo(todo.id)">Delete</button>
</li>
</ul>
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
<script setup lang="ts">
import { ref } from 'vue';
interface Todo {
id?: number;
task: string;
done: boolean;
}
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
const task = ref("");
const error = ref("");
const todos = ref([] as Todo[]);
const fetchTodos = async () => {
const todosResponse = await fetch(API_URL);
todos.value = await todosResponse.json();
}
fetchTodos();
const addTodo = async () => {
error.value = "";
if (!task.value) {
error.value ="Task cannot be empty";
return;
}
const res = await fetch(API_URL, { method: "POST", body: task.value });
todos.value.push(await res.json());
task.value = "";
}
const deleteTodo = async (id: number) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
todos.value = todos.value.filter((t) => t.id !== id);
}
</script>
<template>
<h1>Todo</h1>
<form @submit.prevent="addTodo">
<input type="text" v-model="task" />
<button type="submit">Add</button>
</form>
<div className="errors">{{error}} </div>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.task}}
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
</template>
<style scoped>
.errors {
color: red;
}
</style>
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
TodoApp.java
@Route("")
public class TodoApp extends VerticalLayout {
private TextField task = new TextField("Task");
private Button button = new Button("Add");
private UnorderedList taskList = new UnorderedList();
private TodoService service;
private Binder<Todo> binder = new BeanValidationBinder<>(Todo.class);
TodoApp(TodoService service) {
this.service = service;
HorizontalLayout form = new HorizontalLayout(task, button);
form.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
add(
new H1("Todo"),
form,
taskList
);
binder.bindInstanceFields(this);
button.addClickListener(this::addTask);
updateTasks();
}
private void updateTasks() {
taskList.removeAll();
for (Todo todo : service.getTodos()) {
HorizontalLayout taskLayout = new HorizontalLayout(
new Span(todo.getTask()),
new Button("Delete", e -> deleteTask(todo.getId()))
);
taskLayout.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
taskList.add(new ListItem(taskLayout));
}
}
private void addTask(ClickEvent<Button> e) {
Todo todo = new Todo();
if (binder.writeBeanIfValid(todo)) {
service.saveTodo(todo);
binder.readBean(new Todo());
updateTasks();
}
}
private void deleteTask(Long id) {
service.deleteTodo(id);
updateTasks();
}
}
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
todo-view.ts
@customElement("todo-view")
export class TodoView extends LitElement {
@property({ type: Array })
private todos: Todo[] = [];
private binder = new Binder(this, TodoModel);
protected render() {
return html`
<h1>Todo</h1>
<div class="form">
<vaadin-text-field
label="Task"
...=${field(this.binder.model.task)}
></vaadin-text-field>
<vaadin-button @click=${this.add}>Add</vaadin-button>
</div>
<ul>
${this.todos.map((todo) => html`
<li>
${todo.task}
<vaadin-button @click=${() => this.clear(todo.id)}>
Delete
</vaadin-button>
</li>
`)}
</ul>
`;
}
async firstUpdated() {
this.todos = await getTodos();
}
async add() {
const saved = await this.binder.submitTo(saveTodo);
if (saved) {
this.todos = [...this.todos, saved];
this.binder.clear();
}
}
async clear(id: any) {
await deleteTodo(id);
this.todos = this.todos.filter((t) => t.id !== id);
}
}
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
TodoApp.jsx
function TodoApp() {
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
const [error, setError] = useState("");
const addTodo = async (e) => {
e.preventDefault();
setError("");
if (!task) {
setError("Task cannot be empty");
return;
}
const res = await fetch(API_URL, { method: "POST", body: task });
setTodos([...todos, await res.json()]);
setTask("");
};
const clearTodo = async (id) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
setTodos(todos.filter((t) => t.id !== id));
};
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
return (
<div className="TodoApp">
<h1>Todo</h1>
<form
<input
type="text"
value={task}
=> setTask(e.target.value)} />
<div className="errors">{error}</div>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}{" "}
<button => clearTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
todo-view.component.ts
const API_URL = 'https://vaadin-todo-api.herokuapp.com/todos';
interface Todo {
id?: number;
task: string;
}
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent implements OnInit {
taskForm = new FormGroup({
task: new FormControl('', [
Validators.required
]),
});
todos: Todo[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
async addTodo(taskForm: FormGroup, formDirective: FormGroupDirective) {
const { task } = taskForm.value;
this.http.post<Todo>(API_URL, task).subscribe((todo: Todo) => {
this.todos = [...this.todos, todo];
// Double reset workaround needed to reset form validations
// https://github.com/angular/components/issues/4190
taskForm.reset();
formDirective.resetForm();
});
}
async clearTodo(id: string) {
this.http
.delete(`${API_URL}/${id}`)
.subscribe((_) => (this.todos = this.todos.filter((t) => t.id !== id)));
}
get task() {
return this.taskForm.get('task');
}
}
todo-view.component.html
<h1>Todo</h1>
<form
[formGroup]="taskForm"
#formDirective="ngForm"
(ngSubmit)="addTodo(taskForm, formDirective)"
>
<mat-form-field>
<input matInput type="text" formControlName="task" />
<mat-error *ngIf="task.invalid">Task cannot be empty</mat-error>
</mat-form-field>
<button mat-button type="submit">Add</button>
</form>
<ul>
<li *ngFor="let todo of todos">
{{ todo.task }}
<button mat-button (click)="clearTodo(todo.id)">Delete</button>
</li>
</ul>
Here is a complete application example that shows all the concepts combined. You can open the application in your browser and try it out for yourself.
<script setup lang="ts">
import { ref } from 'vue';
interface Todo {
id?: number;
task: string;
done: boolean;
}
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
const task = ref("");
const error = ref("");
const todos = ref([] as Todo[]);
const fetchTodos = async () => {
const todosResponse = await fetch(API_URL);
todos.value = await todosResponse.json();
}
fetchTodos();
const addTodo = async () => {
error.value = "";
if (!task.value) {
error.value ="Task cannot be empty";
return;
}
const res = await fetch(API_URL, { method: "POST", body: task.value });
todos.value.push(await res.json());
task.value = "";
}
const deleteTodo = async (id: number) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
todos.value = todos.value.filter((t) => t.id !== id);
}
</script>
<template>
<h1>Todo</h1>
<form @submit.prevent="addTodo">
<input type="text" v-model="task" />
<button type="submit">Add</button>
</form>
<div className="errors">{{error}} </div>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.task}}
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
</template>
<style scoped>
.errors {
color: red;
}
</style>
Application Walkthrough
@Route("")
public class TodoApp extends VerticalLayout {
-
The main layout of the application is a
VerticalLayout
, which places child components vertically and adds a space between them. - The
@Route
annotation maps the view to the empty route, making it the root view.
private Binder<Todo> binder = new BeanValidationBinder<>(Todo.class);
- Create a binder for handling the form and validation.
TodoApp(TodoService service) {
this.service = service;
HorizontalLayout form = new HorizontalLayout(task, button);
form.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
add(
new H1("Todo"),
form,
taskList
);
binder.bindInstanceFields(this);
button.addClickListener(this::addTask);
updateTasks();
}
- The constructor takes in a backend service as a parameter and saves it to a field for later use. The service is Autowired through Spring dependency injection.
- Create a
HorizontalLayout
to hold the form components next to each other. Align the components. - The add method adds the child components to the main layout.
- Call
binder.bindInstanceFields(this)
to bind thetask
field inTodoApp
to thetask
field in theTodo
model object. - The button-click listener maps to the
addTask
method. - Finally, the
updateTasks
method updates the list of todo items.
private void updateTasks() {
taskList.removeAll();
for(Todo todo : service.getTodos()) {
HorizontalLayout taskLayout = new HorizontalLayout(
new Span(todo.getTask()),
new Button("Delete", e -> deleteTask(todo.getId()))
);
taskLayout.setDefaultVerticalComponentAlignment(
Alignment.);
taskList.add(new ListItem(taskLayout));
}
}
- The
removeAll
method clears old content from the list. - Next, we get a list of Todo objects from the backend and loop over them. We:
- Create a
HorizontalLayout
with the todo text and a delete button. - Align the components.
- Add the layout to the
UnorderedList
(<ul>
) as aListItem
(<li>
).
private void addTask(ClickEvent<Button> e) {
Todo todo = new Todo();
if (binder.writeBeanIfValid(todo)) {
service.saveTodo(todo);
binder.readBean(new Todo());
updateTasks();
}
}
private void deleteTask(Long id) {
service.deleteTodo(id);
updateTasks();
}
- The
addTask
method: - Checks that the form is valid (
task
is not empty) and writes the value to a newTodo
object. - Saves the todo to the backend through
service
. - Resets the binder by reading an empty
Todo
object. - Calls
updateTasks
to update the UI. - The
deleteTask
method: - Delegates the delete operation to the backend service.
- Calls
updateTasks
to update the UI.
import { Binder, field } from "@vaadin/flow-frontend/form";
import { saveTodo, deleteTodo, getTodos } from "../generated/TodoService";
import Todo from "../generated/com/example/application/backend/Todo";
import TodoModel from "../generated/com/example/application/backend/TodoModel";
- Imports the data types and functions for accessing the backend. You can find the backend code in the Appendix.
@customElement("todo-view")
export class TodoView extends LitElement {
- Defines a component that extends
LitElement
and the tag name. Note that the name must include a dash.
@property({ type: Array })
private todos: Todo[] = [];
private binder = new Binder(this, TodoModel);
- Defines an array of
Todo
s as the state of the component. Any time these change, the template is re-rendered. - Creates a binder for the form and loads the generated model info.
<div class="form">
<vaadin-text-field
label="Task"
...=${field(this.binder.model.task)}
></vaadin-text-field>
<vaadin-button @click=${this.add}>Add</vaadin-button>
</div>
- Creates a form for inputting new items.
- The text field uses a spread operator ...=${} to bind all properties needed by the form.
- The button calls the
add
method when clicked.
<ul>
${this.todos.map(
(todo) => html`
<li>
${todo.task}
<vaadin-button @click=${() => this.clear(todo.id)}
>Delete</vaadin-button
>
</li>
`
)}
</ul>
- Lists all todos in a
<ul>
. - Maps over the todo array and creates a
<li>
for each todo. - Adds a delete button to each item that calls the
clear
method with the id of the todo.
async firstUpdated() {
this.todos = await getTodos();
}
- Calls the backend to fetch the list of
Todo
s when the component is first updated.
async add() {
const saved = await this.binder.submitTo(saveTodo);
if (saved) {
this.todos = [...this.todos, saved];
this.binder.clear();
}
}
async clear(id: any) {
await deleteTodo(id);
this.todos = this.todos.filter((t) => t.id !== id);
}
- The
add
method:- Validates the form and saves the new todo to the backend.
- Creates a new array of todos, including the new todo that it assigns to the todo property.
- Clears the task property.
- The changed property automatically triggers a render.
- The
clear
method:- Calls the backend to delete the todo.
- Creates a new array (without the deleted todo) and assigns it to the todos property.
- The changed property automatically triggers a render.
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
const [error, setError] = useState("");
- Defines the state of the component with
useState
hooks.
const addTodo = async (e) => {
e.preventDefault();
setError("");
if (!task) {
setError("Task cannot be empty");
return;
}
const res = await fetch(API_URL, { method: "POST", body: task });
setTodos([...todos, await res.json()]);
setTask("");
};
const clearTodo = async (id) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
setTodos(todos.filter((t) => t.id !== id));
};
- The
addTodo
method is triggered by the form submit: - It prevents the default so that the browser doesn't handle the submit.
- It validates that the task is set. If not, it sets the error state and returns
- It persists the task to the backend over REST.
- It adds the returned todo to the state.
- It clears out the task.
- The state change triggers a render.
- The
clearTodo
method is called to delete a todo: - It deletes the todo from the backend.
- It updates the state.
- The state change triggers a render.
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
- The
useEffect
hook allows you to run code with side effects. The second parameter is used for memoization. An empty array means the effect will only be run once. - It fetches the todos from the backend and updates the state.
- The state change triggers a render
<form
<input
type="text"
value={task}
=> setTask(e.target.value)} />
<div className="errors">{error}</div>
<button type="submit">Add</button>
</form>
- Maps the onSubmit event to the
addTodo
function. - Controls the input by binding the value and updating the state based on change events.
- Displays an error if it's set
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}{" "}
<button => clearTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
- Uses a
map
operator to create list items for each todo in the array. - Uses the todo id as a key for the list item.
- Creates a button for deleting the todo:
- Binds the
onClick
listener to theclearTodo
method with the todo id as the parameter.
interface Todo {
id?: number;
task: string;
}
- Define an interface that matches the backend data type
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent implements OnInit {
- Defines the component by providing a selector and a URL to its template.
- Implements
OnInit
to enable thengOnInit
callback.
taskForm = new FormGroup({
task: new FormControl('', [
Validators.required
]),
});
- Defines a reactive form using
FormGroup
. - Defines one
FormControl
for the task input. - Adds a required validation
constructor(private http: HttpClient) {}
-
Injects
HttpClient
for accessing the backend over REST.
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
- Gets the initial set of
Todo
s from the server when the component is initialized.
async addTodo(taskForm: FormGroup, formDirective: FormGroupDirective) {
const { task } = taskForm.value;
this.http.post<Todo>(API_URL, task).subscribe((todo: Todo) => {
this.todos = [...this.todos, todo];
// Double reset workaround needed to reset form validations
// https://github.com/angular/components/issues/4190
taskForm.reset();
formDirective.resetForm();
});
}
async clearTodo(id: string) {
this.http
.delete(`${API_URL}/${id}`)
.subscribe((_) => (
this.todos = this.todos.filter((t) => t.id !== id)));
}
- The
addTodo
method: - Destructures the task value out of the form.
- Makes a POST request to the backend with the task.
- Resets the form
- Adds the returned
todo
to the local array. - The array change triggers a render.
- The
clearTodo
method: - Makes a DELETE request to the backend using the id of the task.
- Removes the
todo
from the local array when complete. - The array change triggers a render.
get task() {
return this.taskForm.get('task');
}
- Returns the
FormControl
for showing errors in the template
<form
[formGroup]="taskForm"
#formDirective="ngForm"
(ngSubmit)="addTodo(taskForm, formDirective)"
>
<mat-form-field>
<input matInput type="text" formControlName="task" />
<mat-error *ngIf="task.invalid">Task cannot be empty</mat-error>
</mat-form-field>
<button mat-button type="submit">Add</button>
</form>
- Binds the
<form>
to thetaskForm
by binding to theformGroup
property. - Binds to the
FormGroupDirective
so it can be used to reset the form validations. - The form listens to the Angular-specific
ngSubmit
event and triggers the addTodo method. - The input is bound to the form with the
formControlName
attribute. - Displays an error if the task is invalid.
- The input and button are Angular material components.
<ul>
<li *ngFor="let todo of todos">
<button mat-button (click)="clearTodo(todo.id)">Delete</button>
</li>
</ul>
- Loops over all todos, creating a
<li>
for each. - Adds a delete button to each item that calls the
clearTodo
method with the todo id.
<script setup lang="ts">
...
</script>
- Contains the component definition and logic
- The
lang="ts"
attribute signals to the compiler that the component uses TypeScript. TypeScript is optional. We use it here to keep the apps similar.
import { ref } from 'vue';
interface Todo {
id?: number;
task: string;
done: boolean;
}
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
- The
ref
import is used to create a reactive state. - Define a
Todo
TypeScript interface that matches the server model.
const task = ref("");
const error = ref("");
const todos = ref([] as Todo[]);
- Defines the properties that make up the state of the component using
ref
.
const addTodo = async () => {
error.value = "";
if (!task.value) {
error.value ="Task cannot be empty";
return;
}
const res = await fetch(API_URL, { method: "POST", body: task.value });
todos.value.push(await res.json());
task.value = "";
}
- Clears any old validation errors.
- Validates the input and breaks if it is invalid.
- Posts the task to the API endpoint.
- Pushes the returned
Todo
onto thetodos
array, triggering a re-render.
const deleteTodo = async (id: number) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
todos.value = todos.value.filter((t) => t.id !== id);
}
- Makes a
DELETE
call to the endpoint to delete theTodo
on the server. - Removes the todo from the local
todos
array.
const fetchTodos = async () => {
const todosResponse = await fetch(API_URL);
todos.value = await todosResponse.json();
}
fetchTodos();
- Fetches all
Todo
items from the server.
<template>
...
</template>
- Defines the component template
<form @submit.prevent="addTodo">
<input type="text" v-model="task" />
<button type="submit">Add</button>
</form>
<div className="errors">{{error}}</div>
- Defines the form.
- Binds the input to the task property using a
v-model
directive. - The form submit event is bound to the addTodo method. The listener uses the
.prevent
modifier to callpreventDefault
on the event. - Errors are optionally shown in the
errors
div if present
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.task}}
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
- Todos are shown in an unordered list.
- List items are repeated with the
v-for
directive. Each element needs a unique key. - The Delete button is bound to the
deleteTodo
, passing in the todo id.
@Route("")
public class TodoApp extends VerticalLayout {
-
The main layout of the application is a
VerticalLayout
, which places child components vertically and adds a space between them. - The
@Route
annotation maps the view to the empty route, making it the root view.
private Binder<Todo> binder = new BeanValidationBinder<>(Todo.class);
- Create a binder for handling the form and validation.
TodoApp(TodoService service) {
this.service = service;
HorizontalLayout form = new HorizontalLayout(task, button);
form.setDefaultVerticalComponentAlignment(Alignment.BASELINE);
add(
new H1("Todo"),
form,
taskList
);
binder.bindInstanceFields(this);
button.addClickListener(this::addTask);
updateTasks();
}
- The constructor takes in a backend service as a parameter and saves it to a field for later use. The service is Autowired through Spring dependency injection.
- Create a
HorizontalLayout
to hold the form components next to each other. Align the components. - The add method adds the child components to the main layout.
- Call
binder.bindInstanceFields(this)
to bind thetask
field inTodoApp
to thetask
field in theTodo
model object. - The button-click listener maps to the
addTask
method. - Finally, the
updateTasks
method updates the list of todo items.
private void updateTasks() {
taskList.removeAll();
for(Todo todo : service.getTodos()) {
HorizontalLayout taskLayout = new HorizontalLayout(
new Span(todo.getTask()),
new Button("Delete", e -> deleteTask(todo.getId()))
);
taskLayout.setDefaultVerticalComponentAlignment(
Alignment.);
taskList.add(new ListItem(taskLayout));
}
}
- The
removeAll
method clears old content from the list. - Next, we get a list of Todo objects from the backend and loop over them. We:
- Create a
HorizontalLayout
with the todo text and a delete button. - Align the components.
- Add the layout to the
UnorderedList
(<ul>
) as aListItem
(<li>
).
private void addTask(ClickEvent<Button> e) {
Todo todo = new Todo();
if (binder.writeBeanIfValid(todo)) {
service.saveTodo(todo);
binder.readBean(new Todo());
updateTasks();
}
}
private void deleteTask(Long id) {
service.deleteTodo(id);
updateTasks();
}
- The
addTask
method: - Checks that the form is valid (
task
is not empty) and writes the value to a newTodo
object. - Saves the todo to the backend through
service
. - Resets the binder by reading an empty
Todo
object. - Calls
updateTasks
to update the UI. - The
deleteTask
method: - Delegates the delete operation to the backend service.
- Calls
updateTasks
to update the UI.
import { Binder, field } from "@vaadin/flow-frontend/form";
import { saveTodo, deleteTodo, getTodos } from "../generated/TodoService";
import Todo from "../generated/com/example/application/backend/Todo";
import TodoModel from "../generated/com/example/application/backend/TodoModel";
- Imports the data types and functions for accessing the backend. You can find the backend code in the Appendix.
@customElement("todo-view")
export class TodoView extends LitElement {
- Defines a component that extends
LitElement
and the tag name. Note that the name must include a dash.
@property({ type: Array })
private todos: Todo[] = [];
private binder = new Binder(this, TodoModel);
- Defines an array of
Todo
s as the state of the component. Any time these change, the template is re-rendered. - Creates a binder for the form and loads the generated model info.
<div class="form">
<vaadin-text-field
label="Task"
...=${field(this.binder.model.task)}
></vaadin-text-field>
<vaadin-button @click=${this.add}>Add</vaadin-button>
</div>
- Creates a form for inputting new items.
- The text field uses a spread operator ...=${} to bind all properties needed by the form.
- The button calls the
add
method when clicked.
<ul>
${this.todos.map(
(todo) => html`
<li>
${todo.task}
<vaadin-button @click=${() => this.clear(todo.id)}
>Delete</vaadin-button
>
</li>
`
)}
</ul>
- Lists all todos in a
<ul>
. - Maps over the todo array and creates a
<li>
for each todo. - Adds a delete button to each item that calls the
clear
method with the id of the todo.
async firstUpdated() {
this.todos = await getTodos();
}
- Calls the backend to fetch the list of
Todo
s when the component is first updated.
async add() {
const saved = await this.binder.submitTo(saveTodo);
if (saved) {
this.todos = [...this.todos, saved];
this.binder.clear();
}
}
async clear(id: any) {
await deleteTodo(id);
this.todos = this.todos.filter((t) => t.id !== id);
}
- The
add
method:- Validates the form and saves the new todo to the backend.
- Creates a new array of todos, including the new todo that it assigns to the todo property.
- Clears the task property.
- The changed property automatically triggers a render.
- The
clear
method:- Calls the backend to delete the todo.
- Creates a new array (without the deleted todo) and assigns it to the todos property.
- The changed property automatically triggers a render.
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
const [error, setError] = useState("");
- Defines the state of the component with
useState
hooks.
const addTodo = async (e) => {
e.preventDefault();
setError("");
if (!task) {
setError("Task cannot be empty");
return;
}
const res = await fetch(API_URL, { method: "POST", body: task });
setTodos([...todos, await res.json()]);
setTask("");
};
const clearTodo = async (id) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
setTodos(todos.filter((t) => t.id !== id));
};
- The
addTodo
method is triggered by the form submit: - It prevents the default so that the browser doesn't handle the submit.
- It validates that the task is set. If not, it sets the error state and returns
- It persists the task to the backend over REST.
- It adds the returned todo to the state.
- It clears out the task.
- The state change triggers a render.
- The
clearTodo
method is called to delete a todo: - It deletes the todo from the backend.
- It updates the state.
- The state change triggers a render.
useEffect(() => {
const getTodos = async () => {
const result = await fetch(API_URL);
setTodos(await result.json());
};
getTodos();
}, []);
- The
useEffect
hook allows you to run code with side effects. The second parameter is used for memoization. An empty array means the effect will only be run once. - It fetches the todos from the backend and updates the state.
- The state change triggers a render
<form
<input
type="text"
value={task}
=> setTask(e.target.value)} />
<div className="errors">{error}</div>
<button type="submit">Add</button>
</form>
- Maps the onSubmit event to the
addTodo
function. - Controls the input by binding the value and updating the state based on change events.
- Displays an error if it's set
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.task}{" "}
<button => clearTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
- Uses a
map
operator to create list items for each todo in the array. - Uses the todo id as a key for the list item.
- Creates a button for deleting the todo:
- Binds the
onClick
listener to theclearTodo
method with the todo id as the parameter.
interface Todo {
id?: number;
task: string;
}
- Define an interface that matches the backend data type
@Component({
selector: 'app-todo-view',
templateUrl: './todo-view.component.html',
})
export class TodoViewComponent implements OnInit {
- Defines the component by providing a selector and a URL to its template.
- Implements
OnInit
to enable thengOnInit
callback.
taskForm = new FormGroup({
task: new FormControl('', [
Validators.required
]),
});
- Defines a reactive form using
FormGroup
. - Defines one
FormControl
for the task input. - Adds a required validation
constructor(private http: HttpClient) {}
-
Injects
HttpClient
for accessing the backend over REST.
ngOnInit(): void {
this.http
.get<Todo[]>(API_URL)
.subscribe((todos: Todo[]) => (this.todos = todos));
}
- Gets the initial set of
Todo
s from the server when the component is initialized.
async addTodo(taskForm: FormGroup, formDirective: FormGroupDirective) {
const { task } = taskForm.value;
this.http.post<Todo>(API_URL, task).subscribe((todo: Todo) => {
this.todos = [...this.todos, todo];
// Double reset workaround needed to reset form validations
// https://github.com/angular/components/issues/4190
taskForm.reset();
formDirective.resetForm();
});
}
async clearTodo(id: string) {
this.http
.delete(`${API_URL}/${id}`)
.subscribe((_) => (
this.todos = this.todos.filter((t) => t.id !== id)));
}
- The
addTodo
method: - Destructures the task value out of the form.
- Makes a POST request to the backend with the task.
- Resets the form
- Adds the returned
todo
to the local array. - The array change triggers a render.
- The
clearTodo
method: - Makes a DELETE request to the backend using the id of the task.
- Removes the
todo
from the local array when complete. - The array change triggers a render.
get task() {
return this.taskForm.get('task');
}
- Returns the
FormControl
for showing errors in the template
<form
[formGroup]="taskForm"
#formDirective="ngForm"
(ngSubmit)="addTodo(taskForm, formDirective)"
>
<mat-form-field>
<input matInput type="text" formControlName="task" />
<mat-error *ngIf="task.invalid">Task cannot be empty</mat-error>
</mat-form-field>
<button mat-button type="submit">Add</button>
</form>
- Binds the
<form>
to thetaskForm
by binding to theformGroup
property. - Binds to the
FormGroupDirective
so it can be used to reset the form validations. - The form listens to the Angular-specific
ngSubmit
event and triggers the addTodo method. - The input is bound to the form with the
formControlName
attribute. - Displays an error if the task is invalid.
- The input and button are Angular material components.
<ul>
<li *ngFor="let todo of todos">
<button mat-button (click)="clearTodo(todo.id)">Delete</button>
</li>
</ul>
- Loops over all todos, creating a
<li>
for each. - Adds a delete button to each item that calls the
clearTodo
method with the todo id.
<script setup lang="ts">
...
</script>
- Contains the component definition and logic
- The
lang="ts"
attribute signals to the compiler that the component uses TypeScript. TypeScript is optional. We use it here to keep the apps similar.
import { ref } from 'vue';
interface Todo {
id?: number;
task: string;
done: boolean;
}
const API_URL = "https://vaadin-todo-api.herokuapp.com/todos";
- The
ref
import is used to create a reactive state. - Define a
Todo
TypeScript interface that matches the server model.
const task = ref("");
const error = ref("");
const todos = ref([] as Todo[]);
- Defines the properties that make up the state of the component using
ref
.
const addTodo = async () => {
error.value = "";
if (!task.value) {
error.value ="Task cannot be empty";
return;
}
const res = await fetch(API_URL, { method: "POST", body: task.value });
todos.value.push(await res.json());
task.value = "";
}
- Clears any old validation errors.
- Validates the input and breaks if it is invalid.
- Posts the task to the API endpoint.
- Pushes the returned
Todo
onto thetodos
array, triggering a re-render.
const deleteTodo = async (id: number) => {
await fetch(`${API_URL}/${id}`, { method: "DELETE" });
todos.value = todos.value.filter((t) => t.id !== id);
}
- Makes a
DELETE
call to the endpoint to delete theTodo
on the server. - Removes the todo from the local
todos
array.
const fetchTodos = async () => {
const todosResponse = await fetch(API_URL);
todos.value = await todosResponse.json();
}
fetchTodos();
- Fetches all
Todo
items from the server.
<template>
...
</template>
- Defines the component template
<form @submit.prevent="addTodo">
<input type="text" v-model="task" />
<button type="submit">Add</button>
</form>
<div className="errors">{{error}}</div>
- Defines the form.
- Binds the input to the task property using a
v-model
directive. - The form submit event is bound to the addTodo method. The listener uses the
.prevent
modifier to callpreventDefault
on the event. - Errors are optionally shown in the
errors
div if present
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.task}}
<button @click="deleteTodo(todo.id)">Delete</button>
</li>
</ul>
- Todos are shown in an unordered list.
- List items are repeated with the
v-for
directive. Each element needs a unique key. - The Delete button is bound to the
deleteTodo
, passing in the todo id.
Appendix: Backend code
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@Service
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
public List<Todo> getTodos() {
return repo.findAll();
}
public Todo saveTodo(Todo todo) {
return repo.save(todo);
}
public void deleteTodo(Long id) {
repo.deleteById(id);
}
}
- Gets a
TodoRepository
injected in the constructor - Defers operations to the repository
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@Endpoint
@AnonymousAllowed
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
public List<Todo> getTodos() {
return repo.findAll();
}
public Todo saveTodo(Todo todo) {
return repo.save(todo);
}
public void deleteTodo(Long id) {
repo.deleteById(id);
}
}
@Endpoint
makes the methods and data types available in TypeScript for the frontend- Gets
TodoRepository
injected in the constructor - Defers operations to the repository
Note: React does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Note: Angular does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Note: Vue does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@Service
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
public List<Todo> getTodos() {
return repo.findAll();
}
public Todo saveTodo(Todo todo) {
return repo.save(todo);
}
public void deleteTodo(Long id) {
repo.deleteById(id);
}
}
- Gets a
TodoRepository
injected in the constructor - Defers operations to the repository
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@Endpoint
@AnonymousAllowed
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
public List<Todo> getTodos() {
return repo.findAll();
}
public Todo saveTodo(Todo todo) {
return repo.save(todo);
}
public void deleteTodo(Long id) {
repo.deleteById(id);
}
}
@Endpoint
makes the methods and data types available in TypeScript for the frontend- Gets
TodoRepository
injected in the constructor - Defers operations to the repository
Note: React does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Note: Angular does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Note: Vue does not place restrictions on the backend. This application uses a Spring Boot backend to make it easier to compare with the other implementations. It exposes a standard REST API.
Todo.java
@Entity
public class Todo {
@Id
@GeneratedValue
private Long id;
@NotEmpty(message = "Task cannot be empty")
private String task;
public Todo() {
}
public Todo(String task) {
this.setTask(task);
}
public Long getId() {
return id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
- Defines a JPA Entity
- Makes the task field required by adding a @NotEmpty bean validation annotation
TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
- A Spring Data JPA Repository for
Todo
TodoService.java
@RestController
@CrossOrigin(origins = "*")
public class TodoService {
private TodoRepository repo;
TodoService(TodoRepository repo) {
this.repo = repo;
}
@GetMapping("/todos")
public List<Todo> getTodos() {
return repo.findAll();
}
@PostMapping(value = "/todos")
public Todo addTodo(@RequestBody String task) {
return repo.save(new Todo(task));
}
@DeleteMapping("/todos/{id}")
public void deleteTodo(@PathVariable Long id) {
repo.deleteById(id);
}
}
Discover in practice what makes Vaadin better & learn the business benefits over other frameworks!