Compare

Flow Hilla React Angular Vue
&
Flow Hilla React Angular Vue

Overview

[
vaadin_icon-1

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-icon

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

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

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-logo

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_icon-1

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-icon

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

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

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-logo

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. 

]
arrow-white

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>
   `;
}

 

,
React components can be defined either as classes or as functions. Functional components are the recommended approach for new applications. The simplest React component is a function that returns a React element, and is typically defined using JSX syntax. 
 
HelloWorld.jsx
function HelloWorld() {
 return <h1>Hello world</h1>;
}

 

,
Angular components consist of a TypeScript definition and a HTML template. In very simple cases the template can be inlined, but it is mostly defined in a separate file. Components need to be imported to and defined in the Angular module where you want to use them: it is not enough to only import the component where you use it.

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>
   `;
}

 

,
React components can be defined either as classes or as functions. Functional components are the recommended approach for new applications. The simplest React component is a function that returns a React element, and is typically defined using JSX syntax. 
 
HelloWorld.jsx
function HelloWorld() {
 return <h1>Hello world</h1>;
}

 

,
Angular components consist of a TypeScript definition and a HTML template. In very simple cases the template can be inlined, but it is mostly defined in a separate file. Components need to be imported to and defined in the Angular module where you want to use them: it is not enough to only import the component where you use it.

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>
,
The data binding syntax in Vue templates depends on the type of binding:
  • {{ 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 longer v-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>
,
The data binding syntax in Vue templates depends on the type of binding:
  • {{ 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 longer v-bind:property="value" syntax.
<h1>Hello {{ user.name }} </h1>
<img :src="user.avatar" :alt="user.name">

<ListComponent :todos="user.todos"/>
]

Templating - Events

[
Any component with which a user interacts, fires events. You can subscribe to these events through the 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 onClick={clickHandler}>Click</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>
]
[
Any component with which a user interacts, fires events. You can subscribe to these events through the 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 onClick={clickHandler}>Click</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 onClick={logout}>Logout</button>;
   } else {
     return <button onClick={login}>Login</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 onClick={logout}>Logout</button>;
   } else {
     return <button onClick={login}>Login</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.

Person.java
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
}
 
Service.java
@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.

Person.java
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
}

 

Service.java
@Endpoint
@AnonymousAllowed
public class Service {

  public Person getPerson() {
    return new Person();
  }

  public void savePerson(Person person) {
    // save to DB
  }
}

 

form-view.ts
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" onSubmit={submit}>
      <label>
        Name
        <input name="name" value={person.name} onChange={updateModel} />
      </label>
      <label>
        Email
        <input name="email" value={person.email} onChange={updateModel} />
      </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.

Person.java
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
}
 
Service.java
@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.

Person.java
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
}

 

Service.java
@Endpoint
@AnonymousAllowed
public class Service {

  public Person getPerson() {
    return new Person();
  }

  public void savePerson(Person person) {
    // save to DB
  }
}

 

form-view.ts
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" onSubmit={submit}>
      <label>
        Name
        <input name="name" value={person.name} onChange={updateModel} />
      </label>
      <label>
        Email
        <input name="email" value={person.email} onChange={updateModel} />
      </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

[
You can make any Vaadin component a navigation target by adding the @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>
    `;
  }
}
,
Routing is not part of the React core library. Many projects use third-party libraries, like React Router.
,

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>

]
[
You can make any Vaadin component a navigation target by adding the @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>
    `;
  }
}
,
Routing is not part of the React core library. Many projects use third-party libraries, like React Router.
,

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.
,

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.

,
React does not include any components. There are third-party component libraries for React, and many developers build their own components.
,
Angular includes a collection of 35 Material Design components that cover basic needs. The components can be themed to match your color scheme. 
 
There are also several third-party component sets that are compatible with Angular.
,

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.
,

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.

,
React does not include any components. There are third-party component libraries for React, and many developers build their own components.
,
Angular includes a collection of 35 Material Design components that cover basic needs. The components can be themed to match your color scheme. 
 
There are also several third-party component sets that are compatible with Angular.
,

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. 

Todo app built with Vaadin

Open project in GitPod
GitHub

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 app built with Vaadin

Open project in GitPod
GitHub

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. 

Todo app built with React

Open project in GitPod
GitHub

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 onSubmit={addTodo}>
        <input
          type="text"
          value={task}
          onChange={(e) => 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 onClick={() => 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 app built with Angular

Open project in GitPod
GitHub

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.

vue-todo

Open project in GitPod
GitHub

 <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. 

Todo app built with Vaadin

Open project in GitPod
GitHub

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 app built with Vaadin

Open project in GitPod
GitHub

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. 

Todo app built with React

Open project in GitPod
GitHub

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 onSubmit={addTodo}>
        <input
          type="text"
          value={task}
          onChange={(e) => 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 onClick={() => 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 app built with Angular

Open project in GitPod
GitHub

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.

vue-todo

Open project in GitPod
GitHub

 <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 the task field in TodoApp to the task field in the Todo 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 a ListItem (<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 new Todo 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 Todos 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 Todos 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 onSubmit={addTodo}>
  <input
    type="text"
    value={task}
    onChange={(e) => 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 onClick={() => 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 the clearTodo 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 the ngOnInit 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 Todos 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 the taskForm by binding to the formGroup 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 the todos 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 the Todo 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 call preventDefault 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 the task field in TodoApp to the task field in the Todo 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 a ListItem (<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 new Todo 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 Todos 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 Todos 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 onSubmit={addTodo}>
  <input
    type="text"
    value={task}
    onChange={(e) => 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 onClick={() => 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 the clearTodo 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 the ngOnInit 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 Todos 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 the taskForm by binding to the formGroup 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 the todos 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 the Todo 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 call preventDefault 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);
  }
}

 

]
arrow-white

Discover in practice what makes Vaadin better & learn the business benefits over other frameworks!