Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

An Angular Autocomplete From UI to DB

DZone's Guide to

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.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

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

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
angular ,spring boot ,web dev ,rest ,rxjs

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}