JHipster, Angular, PrimeNG and AutoComplete

JHipster is a great way to bootstrap your application. Your app can be a monolith or be split into several microservices, use JWT or OAuth2, packaged with Docker, deployed on a cloud provider… JHipster is there to handle the heavy technical complexity. Great ! But… when it comes to choosing an item from a combobox, JHipster is not that great.

In this post I will show you how to improve the generated JHipster Angular code so you can have an (optimized) auto-completion instead of just a plain combo-box using PrimeNG.

Use Case

Let’s take a simple use-case: a contact (a person who can be contacted within an organisation) has one preferred language of communication. For example “Paul prefers to speak English” and “Paulo prefers to speak Portuguese“. Thanks to JDL Studio we can have a visual representation of such business model:

For those of you who know JHipster and its JDL language, here is the JDL syntax of such relationship:

entity Contact {
    firstName String required,
    lastName String required,
    email String
}

entity Language {
    alpha3b String required maxlength(3),
    alpha2 String required maxlength(2)
    name String required,
    flag32	String,
    flag128 String,
    activated Boolean
}

relationship ManyToOne {
    Contact{language(name) required} to Language
}

If you think of the user interface to handle such requirement, you can see that when creating a contact, you need to select a language from a combobox. And that’s exactly the topic of this blog post.

Bare JHipster Generated Application

When we use JHipster to generate a simple application with a many-to-one relation between Contact and Language, we get a combo-box limited to 20 items. And because in real life there is approximately 180 “official” languages, a combo-box of 20 items is not enough. So, here, when I tried to create a new contact, JHipster only gives me the 20 first languages (which is useless in real life).

With the default JHipster generated code, there is no way to get “English” as a language (too far in the alphabet).

In terms of code, the generated Angular component is made of two files: an HTML and a TypeScript file. This is what it looks like. The HTML file binds to the language of the contact([(ngModel)]="contact.language"]), and with an ngFor gets the list of languages from the back-end.

<div class="form-group">
    <label class="form-control-label" jhiTranslate="noautocompleteApp.contact.language" for="field_language">Language</label>
    <select class="form-control" id="field_language" name="language" [(ngModel)]="contact.language" required>
<option *ngIf="!editForm.value.language" [ngValue]="null" selected></option>
<option [ngValue]="languageOption.id === contact.language?.id ? contact.language : languageOption" *ngFor="let languageOption of languages; trackBy: trackLanguageById">{{languageOption.name}}</option>
    </select>
</div>
ngOnInit() {
    // ...    
    this.languageService.query().subscribe(
        (res: HttpResponse<ILanguage[]>) => {
            this.languages = res.body;
        },
        (res: HttpErrorResponse) => this.onError(res.message)
    );
}

PrimeNG Auto-Complete Component with Entities

That’s when you go to PrimeNG and pick up a more clever component: the auto-complete component. So instead of using the default JHipster combo-box, it’s just a matter of setting up PrimeNG and changing a few lines of code to get a combo-box that suggests languages as you type. The end result looks like this:

 

As you can see, we don’t have a dumb combobox anymore, but an auto-complete component that calls the back-end each time you type a few characters (e.g. typing en will bring back Armenian, Bengali, Chechen…). For this to work we need to install PrimeNG with the following NPM commands:

$ npm install primeng --save
$ npm install primeicons --save
$ npm install @angular/animations --save

Then, add the PrimeNG CSS in the JHipster vendor.scss file. Once PrimeNG is setup, we need to change the contact-update.component.html file and add the p-autoComplete component.

Notice the attributes of this component. First, the binding is made directly in the contact.language variable. The field attribute is important as it shows the language.name value (Armenian, Bengali, Chechen…) in the auto-complete component (otherwise you would get [Object object]). The completeMethod invokes the searchLanguages method which is responsible to invoke the back-end and get the list of suggested languages (suggestions attribute).

<div class="form-group">
    <label class="form-control-label" jhiTranslate="autocompleteentityApp.contact.language" for="field_language">Language</label>
    <div class="form-group">
        <p-autoComplete id="field_language" name="language" [(ngModel)]="contact.language" field="name" [suggestions]="suggestedLanguages" (completeMethod)="searchLanguages($event)" required>
        </p-autoComplete>
    </div>
</div>

Now it’s just a matter of coding the Typescript part of the component. Thanks to JHipster filtering we don’t have much to do. In fact, we re-use the generated method query and pass the right argument 'name.contains' and the value of what we have typed in the auto-complete component ($event.query). This is what contact-update.component.ts looks like:

suggestedLanguages: ILanguage[];

searchLanguages($event) {
    this.languageService.query({'name.contains': $event.query}).subscribe(
        (res: HttpResponse<ILanguage[]>) => {
            this.suggestedLanguages = res.body;
        },
        (res: HttpErrorResponse) => this.onError(res.message)
    );
}

PrimeNG Auto-Complete Component with DTOs

The previous examples use directly the generated entities (Contact and Language). In fact, that’s the default: JHipster uses the domain objects directly in its REST endpoints. But instead, it is better to ask JHipster to generate a DTO layer (the mapping Entity <-> DTO is made by MapStruct). It’s a better practice because it’s important to be precise in the JSON exchanged between the back-end and the front-end (not too much data if possible). So when we generate the application with DTOs, we don’t use the link between Contact and Language (we don’t have the contact.language relationship anymore) but instead the ContactDTO has a languageId and a languageName attribute. This looks like this:

This changes the way we do our two-way binding in Angular. On the example above, using directly the entities, our auto-complete component was bound to contact.language ([(ngModel)]="contact.language"). But now, we need to bind our component to the ContactDTO which does not have a link to LanguageDTO but instead a languageId and languageName attribute.

<p-autoComplete id="field_language" name="language" [(ngModel)]="selectedLanguage" field="name" [suggestions]="suggestedLanguages" (completeMethod)="searchLanguages($event)" (onSelect)="captureSelectedLanguage($event)" required>
</p-autoComplete>

So the trick is to do the two-way binding on an external selectedLanguage attribute, and then use the onSelect to capture the languageId and languageName that were selected in the auto-complete component. The typescript looks like this:

selectedLanguage: ILanguage;

ngOnInit() {
    this.isSaving = false;
    this.activatedRoute.data.subscribe((contact:IContact) => {
        this.contact = contact;
        if (contact.languageId) {
            this.selectedLanguage = new Language();
            this.selectedLanguage.id = contact.languageId;
            this.selectedLanguage.name = contact.languageName;
        }
    });
}

captureSelectedLanguage($event) {
    this.selectedLanguage = $event;
    this.contact.languageId = $event.id;
    this.contact.languageName = $event.name;
}

The other methods don’t change (such as searchLanguages).

Optimizing the Network Traffic with Jackson Views

One good thing of using DTOs is that you can be very specific about the data you want back and forth from the back-end to the front-end. If you check the network logs, you will notice that when you enter data in the auto-complete component, a full list of languages is returned in JSon. So if you enter en, the auto-complete component will invoke the back-end API at the URI http://localhost:9000/api/languages?name.contains=en and this will return:

 

{
  "id" : 19,
  "alpha3b" : "ben",
  "alpha2" : "bn",
  "name" : "Bengali",
  "flag32" : "",
  "flag128" : "",
  "activated" : false
}, {
  "id" : 39,
  "alpha3b" : "eng",
  "alpha2" : "en",
  "name" : "English",
  "flag32" : "GB-32.png",
  "flag128" : "GB-128.png",
  "activated" : true
}, {
  "id" : 46,
  "alpha3b" : "fre",
  "alpha2" : "fr",
  "name" : "French",
  "flag32" : "FR-32.png",
  "flag128" : "FR-128.png",
  "activated" : true
}, {
  ...
}

That’s because the LanguageDTO has all the attributes from the Language entity. It’s a shame because what we really need in the auto-complete component is just the language id and name. So instead, we would like to have the following JSon structure returned from the back-end:

{
  "id" : 19,
  "name" : "Bengali"
}, {
  "id" : 39,
  "name" : "English"
}, {
  "id" : 46,
  "name" : "French"
}, {
  ...
}

That’s because invoking api/languages?name.contains=en executes a service whose code looks like this:

public Page<LanguageDTO> findByCriteria(LanguageCriteria criteria, Pageable page) {
    final Specification<Language> specification = createSpecification(criteria);
    return languageRepository.findAll(specification, page)
        .map(languageMapper::toDto);
}

Notice how the mapping between the entities and DTOs is made The languageRepository.findAll returns a list of Language entities and there are mapped to list of LanguageDTO, thanks to MapStruct.

There are several ways to only return the language id and name (e.g. create new DTOs, customize the mapper, build the JSon yourself) but I like to use Jackson Views. The idea is to group certain attributes within a “view” and tell Jackson to only serialize a specific view. This way, you can have several “views” of the same DTO (minimal, normal, extended…). The code below defines a Minimal format:

public class Format {
    public static class Minimal {
    }
}

Then, we group the id and name under the Minimal format:

public class LanguageDTO implements Serializable {

    @JsonView(Format.Minimal.class)
    private Long id;

    @NotNull
    @JsonView(Format.Minimal.class)
    private String name;

    @NotNull
    @Size(max = 3)
    private String alpha3b;

    // ...

And then it’s just a matter of annotating the REST endpoint with the Minimal view and Jackson will only serialize the id and the name:

    @GetMapping("/languages")
    @JsonView(Format.Minimal.class)
    public ResponseEntity<List<LanguageDTO>> getAllLanguages(LanguageCriteria criteria, Pageable pageable) {
        Page<LanguageDTO> page = languageQueryService.findByCriteria(criteria, pageable);
        HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/languages");
        return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
    }

CSS Help Needed

BTW if one of you knows Boostrap, JHipster, PrimeNG and is a CSS guru, can you tell me why the vertical red bar in front of Language is not as big as the others ? If yes, just send me a PR ;o) #Thanks

Conclusion

There was lots of discussion on the JHipster GitHub issues about creating a home made auto-complete-like component. I understand that JHipster wants to be agnostic to any component library. As a former PrimeFaces user, I am very happy to find the same components, the same team, the same community around PrimeNG. I am a very happy PrimeNG user!

I hope this post helped you in using auto-complete component in your JHipster Angular application. And you should have a look at the other PrimeNG components, there are amazing.

References

Categories: angular, Java

Tagged as: , ,

3 Comments »

  1. .ng-valid[required],
    .ng-valid.required {
    border-left: 2px solid #42a948; /* green */
    }

    .ng-invalid:not(form) {
    border-left: 2px solid #a94442; /* red */
    }

    p-autocomplete.ng-invalid {
    border-left: 0 none;
    }

    p-autocomplete.ng-touched.ng-dirty.ng-invalid
    > .ui-autocomplete
    > .ui-inputtext,
    p-autocomplete.ng-invalid > .ui-autocomplete > .ui-inputtext[required] {
    border-left: 2px solid #a94442;

  2. Can’t make it work, it still looks the same for me. Could you send a PR so I could check it ?

    Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s