An Angular Autocomplete From UI to DB
A developer shows a big of code he's written for Angular and Spring Boot that he has used to create a full stack web application.
Join the DZone community and get the full member experience.
Join For FreeThe MovieManager project is used to manage my collection of movies and uses Angular and Spring Boot with PostgreSQL to do it.
The project is used to show how to build the autocomplete search box for the movie titles. The autocomplete box uses Bootstrap 4 for styling and Angular on the front-end and Spring Boot with PostgreSQL on the backend. The focus is on how to use native Angular/RxJS together with Spring Boot and a relational database.
The Front-End
The front-end uses Angular with RxJS and Bootstrap 4 for styling. Angular and RxJS have the features to create an input and connect it to the backend REST service.
The result of the service is then displayed and updated in a div below the input after every keystroke. Bootstrap provides the CSS classes to style the components.
The front-end has three parts:
- The template for the div with an input tag and the output div.
- The component class with form control to connect the input tag with the service method.
- The service class that makes the REST request to the server.
In the Search Component, I've used the following template:
<div class="container-fluid">
<div class="row">
...
<div class="col-3">
<div class="input-group-prepend">
<span class="input-group-text" id="searchMovieTitle">Searchfor Movie Title</span>
<input type="text" class="form-control" placeholder="title" aria-describedby="searchMovieTitle" [formControl]="movieTitle">
</div>
<div *ngIf="movieTitle.value" class="searchList">
<span *ngIf="moviesLoading">Loading</span>
<a class="dropdown-item" [routerLink]="['/movie',movie.id]" *ngFor="let movie of movies | async">{{movie.title}}</a>
</div>
</div>
...
</div>
</div>
Lines 1-2 create a Bootstrap container of variable width and a row.
Line 4 creates the Bootstrap column for the Autocomplete feature.
Lines 5-8 create an input group that displays a styled input with a label and links the Angular form control movieTitle
to the input.
Line 9 creates the result div if something is typed in the input.
Line 10 shows the text 'Loading' if the service is retrieving the movies from the server.
Line 11 shows router links for the movies that have been loaded from the server. The routerLink
is the route to the Movie Component with the movie id. *ngFor
iterates over the movies that the server sends and async
means that the movies are an observable whose data is displayed on arrival. The text of the link is the movie title.
In the search component I've used the following class:
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
generes: Genere[];
movieTitle = new FormControl();
movies: Observable<Movie[]>;
movieActor = new FormControl();
actors: Observable<Actor[]>;
importMovies: Movie[] = [];
importMovieTitle = new FormControl();
actorsLoading = false;
moviesLoading = false;
importMoviesLoading = false;
showMenu = false;
moviesByGenere: Movie[] = [];
moviesByGenLoading = false;
constructor(private actorService: ActorsService, private movieService: MoviesService, private userService: UsersService) { }
ngOnInit() {
this.actors = this.movieActor.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.do(() => this.actorsLoading = true)
.switchMap(name => this.actorService.findActorByName(name))
.do(() => this.actorsLoading = false);
this.movies = this.movieTitle.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.do(() => this.moviesLoading = true)
.switchMap(title => this.movieService.findMovieByTitle(title))
.do(() => this.moviesLoading = false);
this.userService.allGeneres().subscribe(res => this.generes = res);
}
Lines 1-6 create the Search component with the annotation and OnInit
interface.
Line 9 creates the movieTitle
form control for the user input.
Line 10 declares the movie observables for the server result.
Line 16 creates the moviesLoading
boolean to show the loading text.
Line 22 declares the constructor and gets the MovieService
and others injected by Angular.
Line 24 declares the ngOnInit
method to initialize form controls with the services and set the genres.
Line 31 links the movieTitle
form control with the valueChanges
observable and the result Observable movies
.
Lines 32-33 add a 400 ms timeout between requests to the backend and only send requests if the input has changed.
Line 34 sets the boolean that shows the loading text in the template.
Line 35 uses switchMap
to discard the running requests and sends a new request to the service.
Line 36 sets the boolean that sets the loading text to false.
This is the movie service that manages all the movie data:
@Injectable()
export class MoviesService {
private _reqOptionsArgs = { headers: new HttpHeaders().set( 'Content-Type', 'application/json' ) };
constructor(private http: HttpClient) { }
...
public findMovieByTitle(title: string) :Observable<Movie[]> {
if(!title) {
return Observable.of([]);
}
return this.http.get('/rest/movie/'+title, this._reqOptionsArgs).catch(error => {
console.error( JSON.stringify( error ) );
return Observable.throw( error );
});
}
...
}
Lines 1-2 creates the MoviesService
with the annotation.
Line 3 creates the HTTP headers.
Line 5 is the constructor that gets the HttpClient that's injected.
Line 9 declares the method findMoveByTitle
that returns an Observable
movie array.
Lines 10-12 return an empty Observable
if the title is empty.
Lines 13-16 send the HTTP request to the server, catch server errors, log them, and throw an error.
The Backend
The backend is done in Spring Boot with JPA and PostgreSQL. It uses Spring Boot features to serve a REST interface and a service to manage the repositories and create transactions around service calls. The transactions are needed to support the importation and deletion of functions for movies in the MovieManager.
The repository uses JPA to create a query that finds the movie titles of the movies of the user that contains (case sensitive) the title string, that the user typed.
The REST service is created in this class:
@RestController
@RequestMapping("rest/movie")
public class MovieController {
@Autowired
private MovieManagerService service;
...
@RequestMapping(value="/{title}", method=RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<List<MovieDto>> getMovieSearch(@PathVariable("title") String titleStr) throws InterruptedException {
List<MovieDto> movies = this.service.findMovie(titleStr);
return new ResponseEntity<List<MovieDto>>(movies, HttpStatus.OK);
}
...
}
Lines 1-3 declare the MovieController
as Restcontroller
with the RequestMapping
.
Lines 5-6 have the MovieManagerService
injected. This manages the transactions and is the common service for movie data.
Lines 10-11 set up the RequestMapping
and get the title value of the request string.
Line 12 reads the movie list from the MovieManagerService
for this title string.
Line 13 wraps the movie list in a ResponseEntity
and returns it.
The main movie service is created in this class:
@Transactional
@Service
public class MovieManagerService {
@Autowired
private CrudMovieRepository crudMovieRep;
@Autowired
private CrudCastRepository crudCastRep;
@Autowired
private CrudActorRepository crudActorRep;
@Autowired
private CrudGenereRepository crudGenereRep;
@Autowired
private CrudUserRepository crudUserRep;
@Autowired
private CustomRepository customRep;
@Autowired
private AppUserDetailsService auds;
...
public List<MovieDto> findMovie(String title) {
List<MovieDto> result = this.customRep.findByTitle(title).stream()
.map(m -> Converter.convert(m)).collect(Collectors.toList());
return result;
}
...
}
Lines 1-3 declare the MovieManagerService
with the Service annotation to make the class injectable and the Transactional annotation wraps a transaction around the service requests. The transactions are needed for importing or deleting movies in the DB.
Lines 14-15 get the CustomRepository
injected. That is the repo for all the non-CRUD DB requests.
Lines 21-22 declare the findMovie
method that is used by the RestController
and gets the movies for the title string from the customRepo
.
Lines 23-24 convert the List of Movie entities in Movie Dtos that can be serialized by the RestController
and returns the Dto List.
The custom repository is created in this class:
@Repository
public class CustomRepository {
@PersistenceContext
private EntityManager em;
@Autowired
private CrudUserRepository crudUserRep;
...
public List<Movie> findByTitle(String title) {
User user = getCurrentUser();
List<Movie> results = em.createQuery("select e from Movie e join e.users u where e.title like :title and u.id = :userid", Movie.class).setParameter("title", "%"+title+"%").setParameter("userid", user.getId()).getResultList();
return results;
}
...
}
Line 1-2 declare the CustomRepository class and and add the Repository annotation to make the repository injectable.
Line 3-4 get the EntitiyManager of Jpa injected.
Line 5-6 get the CrudUserRepository injected to access the current logged in user. The current user is needed for the multi user support.
Line 10-11 declare the findByTitle method that is used in the MovieManagerService and get the current user.
Line 12-13 queries the EntityManager for the movies where the movie title contains the title string and the current user is the owner of the movie. For that the Movie and User entities need to be joined and then filtered by movie title and userId. Then the result is returned.
Summary
Angular and RxJs have the features to create an autocomplete in a few lines of code. Features like timeout between keystrokes, no double requests, and the discarding of stale results are supported by native features. The form control linked to the valueChanges
observable does the updating of the results in the div.
DZone has a nice article that describes Angular and some of its features. A lot of the features like dependency injection, static typing, and annotations are familiar to Java devs and make Angular easy to learn. That makes Angular a very good framework for Java devs that are interested in front-end development.
The backend is a small REST Controller with a service and a repository with JPA. Spring Boot makes it possible to create the backend with few lines of code and a few annotations.
Opinions expressed by DZone contributors are their own.
Comments