Like every craft has to have a cool name, a flashy etiquette and needs to be poured into the right glass to please the hipster drinking it, the same applies to serving them a JHipster application: it’s all about presentation!
So let’s dig right into it and shave of some of ’em rough edges of our JHipster app. We’re building on the application we generated in the previous blog post. Code can be found here.
Presenting the relationships
Alright. One of the most annoying things that can happen when you’re in the middle of ordering a great craft on a warm Summer day, is some big dude demanding your ID right then and there and you discovering that you left the darn thing at home. So let us get rid of those IDs! Like the ones on the Beer page for example:
Those brewery ids don’t mean squat to your average refined beer drinker, so let’s tackle them first. We wanna swap the displayed ids with the corresponding brewery names. But before diving into the code let us take a look at the generated components to see what we’re dealing with.
For every entity JHipster generates a folder into the webapp/app/entities folder. For the Beer entity, for example, we’ve got a beer subfolder. Within we find the beer.component.html that serves as our overview page. The beer-detail.component.html is what is displayed when you press View, the beer-dialog.component.html when you press Create or Edit and the beer-delete-dialog.component.html when you press Delete. They all have their corresponding TypeScript classes.
The beer.model.ts handles the model classes, the beer.service.ts and beer-popup.service.ts classes handle the REST calls to the lower Spring Boot layer, beer.route.ts tackles all routes regarding the Beer entity (think of menu items, bookmark urls, foreign key hyperlinks and the Create, View, Edit and Delete links). Everything is packed in a separate Angular module, i.e. beer.module.ts and index.ts just exports all typescript classes in the Beer entity folder upwards in the Angular hierarchy.
Alright. So for changing the overview page displaying our beers with brewery ids, the beer.component.html page is the guy we need. Since the relationship between Beer and Brewery is represented by the entire Brewery class (so not only by the Brewery id) in the Beer class, we have the name for the taking.
Let’s first change the BaseEntity interface (all entities in a relationship are derived from this one) and add an (optional) name to it. The BaseEntity interface is available in the src/main/webapp/shared/model folder.
export interface BaseEntity { // using type any to avoid methods complaining of invalid type id?: any; name?: any; }
This change is mainly so the IDE won’t complain when we try to use the name property of a relationship somewhere in our html pages.
Now change the beer.component.html so it uses the name of the Brewery instead of the id. First change the table header (for sorting):
<th jhiSortBy="brewery.name"> <span jhiTranslate="helloBeerApp.beer.brewery">Brewery</span> <span class="fa fa-sort"></span> </th>
Now change the table body (for display):
<td> <div *ngIf="beer.brewery"> <a [routerLink]="['../brewery', beer.brewery?.id ]" >{{beer.brewery?.name}}</a> </div> </td>
Note that we didn’t change the link as it would break the navigation from Brewery to Brewery detail (those bold links are clickable) . That’s it. Refresh the Beer page and revel in the magic! The view page (beer-detail.component.html) is even simpler, just replace the one line that is displaying the brewery:
<dd> <div *ngIf="beer.brewery"> <a [routerLink]="['/brewery', beer.brewery?.id]">{{beer.brewery?.name}}</a> </div> </dd>
And voilà the detail page is displaying brewery names now instead of useless ids:
Creating and editing the relationships
The functionality for creating and editing entities are shared on the same html page (beer-dialog.component.html). We want to change the select item linking the beer to the brewery, so that it displays brewery names instead of ids: This one couldn’t have been easier. Just head over to the div displaying the select item, keep the code that handles displaying/selecting the right relationship based on the brewery id and only change the displayed brewery id into the brewery name:
<div class="form-group"> <label class="form-control-label" jhiTranslate="helloBeerApp.beer.brewery" for="field_brewery">Brewery</label> <select class="form-control" id="field_brewery" name="brewery" [(ngModel)]="beer.brewery" > <option [ngValue]="null"></option> <option [ngValue]="breweryOption.id === beer.brewery?.id ? beer.brewery : breweryOption" *ngFor="let breweryOption of breweries; trackBy: trackBreweryById">{{breweryOption.name}}</option> </select> </div>
Check out the Edit page now: See how the brewery name is being displayed instead of the id. How cool is that?!
Autosuggesting
Let’s take this one step further. The inventory item page is still displaying the id for the Beers: We could change this, like we did in the previous steps and display a list of Beer names. But the list of Beer names could become huge, certainly bigger than the list of breweries. So what if we replaced this guy with an auto-complete item? Sounds great, doesn’t it?! But how do we do that? Enter PrimeNG. PrimeNG is a set of UI components for Angular applications.
Installation
First add the PrimeNG lib to your JHipster application
npm install primeng --save
Next, add the auto-complete component to the module where we’re gonna use it, i.e. inventory-item.module.ts:
... import { AutoCompleteModule } from 'primeng/autocomplete'; ... @NgModule({ imports: [ AutoCompleteModule, ... ], ... })
Typescript code
For this blog post, we’re just gonna filter the complete Beer list already retrieved by the REST call in the NgOnInit() method – another option would be to omit this initial retrieval and add a REST method that can handle a filter. Then, every time you make a change in the auto-complete item, an instant REST call is made retrieving a list based on the then present filter.
These are the changes needed for the inventory-item-dialog-component.ts:
export class InventoryItemDialogComponent implements OnInit { ... beers: Beer[]; beerOptions: any[]; ... search(event) { this.beerOptions = this.beers.filter((beer) => beer.name.startsWith(event.query)); } ... }
So basically we just add a method filtering the beers starting with the string matching our query. This method will be called every time the input in the auto-complete item changes and will update the selectable options accordingly.
HTML page
Now lets add the auto-complete item on the inventory-item-dialog.component.html page (overwrite the select item):
<p-autoComplete id="field_beer" name="beer" [(ngModel)]="inventoryItem.beer" [suggestions]="beerOptions" (completeMethod)="search($event)" field="name" placeholder="Beer"></p-autoComplete>
Check the PrimeNG manuals for more information. The most important piece is adding the field attribute so the auto-complete item can work with a Beer object.
Styling
When you test the JHipster app at this point, you’ll notice the auto-complete functionality actually working already, albeit that the styling looks horrible. Luckily you can get PrimeNG to play nicely along with JHipster’s styling – which is based on the popular Bootstrap CSS library. Just add a few lines to the vendor.css file:
@import '~bootstrap/dist/css/bootstrap.min.css'; @import '~font-awesome/css/font-awesome.css'; @import '~primeng/resources/primeng.css'; @import '~primeng/resources/themes/bootstrap/theme.css';
This is a major improvement. One last optimization is to expand the auto-complete item to a width of 100% just like all the other items on the dialog pages. Add these line to the global.css file:
.ui-autocomplete { width: 100%; } .ui-autocomplete-input { width: 100%; }
Now, testing the inventory item Edit page you’ll see a nicely integrated auto-complete item: In the overview and detail pages of the Inventory Item entity we’ll just make the same changes we made for the Beer pages, i.e. exchanging the displayed ids for names:
Calendar
Alright this beer’s on the house! As a small extra, we’ll add in a calendar item to beautify the item stock level page (and we’ll also change that ugly id). As you can see the Stock Date field could use a good calendar to select the date time, and now we’re on it that ugly xml date presentation we could use without as well.
As we did for the auto-complete item, we’ll be using PrimeNG here again. PrimeNG supports a calendar item. It’s dependent on Angular’s animations module. So let’s first install that guy into our project:
npm install @angular/animations --save
And add the necessary imports to the item-stock-level.module.ts (the module where we’re gonna add the calendar item to).
... import {CalendarModule} from 'primeng/calendar'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; ... @NgModule({ imports: [ CalendarModule, BrowserAnimationsModule, ... ], ... })
Next add the PrimeNG calendar item itself (replace the input item representing the Stock Date) to the item-stock-level-dialog.component.html page:
<p-calendar id="field_stockDate" type="datetime-local" [showIcon]="true" name="stockDate" [(ngModel)]="itemStockLevel.stockDate" showTime="true" hourFormat="24" dateFormat="yy-mm-dd"></p-calendar>
Now, the date format used by JHipster isn’t compatible with this calendar item date format. So let’s change that. Format the stock date returned by the REST service in the item-stock-level-popup.service.ts to a format that the calender item understands, i.e. not the XML date format:
itemStockLevel.stockDate = this.datePipe .transform(itemStockLevel.stockDate, 'yyyy-MM-dd HH:mm');
And of course we also need to change the formatting of the stock date when we send it back to the back-end. Alter the item-stock-level.service.ts for this. Just comment out the formatting line (since where sending a plain javascript Date back):
/** * Convert a ItemStockLevel to a JSON which can be sent to the server. */ private convert(itemStockLevel: ItemStockLevel): ItemStockLevel { const copy: ItemStockLevel = Object.assign({}, itemStockLevel); // copy.stockDate = this.dateUtils.toDate(itemStockLevel.stockDate); return copy; }
That’s it! Now look at the dialog page when editing an item stock level row. Looks pretty neat (I’ve also changed that id reference into an description reference (not visible in the picture), I’ll not explain it, you can look it up in the code):
Summary
In this fairly long blog post, we’ve tweaked the front-end of a generated JHipster application. We made quite a few changes to make the application a bit more presentable. We performed the following changes:
- Changing relationships, replacing ids with meaningful strings;
- Adding an auto-complete item;
- Adding a calendar item.
For the last two step we used some PrimeNG components.
In the next blog post we’ll take a closer look at the server side of a JHipster application. So grab yourself a fine craft beer and stay tuned!