JHipster是一种引导你的应用程序的好方法。你的应用程序可以是一个单体,也可以分成几个微服务,使用JWT或OAuth2,用Docker打包,部署在云供应商上......JHipster可以处理沉重的技术复杂性。很好!但是......当涉及到从组合框中选择一个项目时,JHipster就不是那么好了。
在这篇文章中,我将告诉你如何改进生成的JHipster Angular代码,这样你就可以有一个(优化的)自动完成,而不仅仅是一个使用PrimeNG的普通组合框。
使用案例
让我们来看看一个简单的用例:一个联系人(在一个组织内可以联系的人)有一种偏好的交流语言。例如,"保罗喜欢说英语"和 "保罗喜欢说葡萄牙语"。由于JDL Studio的存在,我们可以对这种商业模式有一个可视化的表述:
对于那些了解JHipster及其JDL语言的人来说,以下是这种关系的JDL语法:
\[sourcecode language="shell"\]
entity Contact {
firstName String required,
lastName String required,
email String
}
实体 Language {
alpha3b String required maxlength(3),
alpha2 String required maxlength(2)
name String required,
flag32 String,
flag128 String,
activated Boolean
}
关系 ManyToOne {
Contact{language(name) required} to Language
}
\[/sourcecode\]
如果你想到处理这种要求的用户界面,你可以看到,当创建一个联系人时,你需要从一个组合框中选择一种语言。而这正是这篇博文的主题。
裸露的JHipster生成的应用程序
当我们使用JHipster生成一个简单的应用程序,在联系人和语言之间有多对一的关系时,我们得到了一个限制在20项的组合框。因为在现实生活中,大约有180种 "官方"语言,一个20项的组合框是不够的。所以,在这里,当我试图创建一个新的联系人时,JHipster只给了我20种第一语言(这在现实生活中是没有用的)。
使用默认的JHipster生成的代码,没有办法得到 "英语 "作为语言(在字母表中太远了)。
在代码方面,生成的Angular组件由两个文件组成:一个HTML和一个TypeScript文件。这就是它的模样。HTML文件与联系人的语言绑定([(ngModel)]="contact.language"]),并通过ngFor从后端获取语言列表。
\[sourcecode language="html" title="contact-update.component.html"\]
<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>
\[/sourcecode\]
\[sourcecode language="javascript" title="contact-update.component.ts"\]
ngOnInit() {
// ...
this.languageService.query()subscribe(
(res: HttpResponse<ILanguage\[\]>) => {
this.languages = res.body;
},
(res: HttpErrorResponse) => this.onError(res.message)
) ;
}
\[/sourcecode\]
带有实体的PrimeNG自动完成组件
这时你去PrimeNG,拿起一个更聪明的组件:自动完成组件。因此,不要使用默认的JHipster组合框,只需设置PrimeNG并修改几行代码,就可以得到一个组合框,在你输入时提示语言。最终的结果是这样的。
正如你所看到的,我们不再有一个愚蠢的组合框,而是一个自动完成的组件,在你每次输入几个字符时调用后端(例如,输入en会带回亚美尼亚语、孟加拉语、车臣语...)。为了让它工作,我们需要用以下NPM命令安装PrimeNG。
\[sourcecode language="shell"\]
$ npm install primeng -save
$ npm install primeicons -save
$ npm install @angular/animations -save
\[/sourcecode\]
然后,在JHipstervendor.scss文件中添加PrimeNG的CSS。一旦PrimeNG设置完毕,我们需要修改contact-update.component.html文件并添加p-autoComplete组件。
注意这个组件的属性。首先,直接在 contact.language 变量中进行绑定。这个字段属性很重要,因为它在自动完成组件中显示了language.name值(亚美尼亚语、孟加拉语、车臣语......)(否则你会得到[Object对象])。completeMethod调用searchLanguages方法,该方法负责调用后端并获得建议的语言列表(建议属性)。
\[sourcecode language="html" title="contact-update.component.html" \]
<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" \[sugges\]="suggedLanguages" (completeMethod)="searchLanguages($event)" required>
</p-autoComplete>
</div>
</div>
\[/sourcecode]
现在只需要对组件的Typescript部分进行编码。多亏了JHipster的过滤功能,我们没有什么可做的。事实上,我们重新使用生成的方法查询,并传递正确的参数'name.contains'和我们在自动完成组件中输入的值($event.query)。这就是contact-update.component.ts的样子。
\[sourcecode language="javascript" title=" contact-update.component.ts" \]
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)
);
}
\[/sourcecode\]
带有DTOs的PrimeNG自动完成组件
前面的例子直接使用了生成的实体(Contact和Language)。事实上,那是默认的。JHipster在其REST端点中直接使用域对象。但相反,最好要求JHipster生成一个DTO层(实体<->DTO的映射由MapStruct完成)。这是一个更好的做法,因为在后端和前端之间交换的JSON中必须精确(如果可能的话,不要有太多的数据)。因此,当我们用DTOs生成应用程序时,我们不使用Contact和Language之间的联系(我们不再有contact.language关系),而是ContactDTO有一个languageId和一个languageName属性。这看起来像这样。
这改变了我们在Angular中进行双向绑定的方式。在上面的例子中,直接使用实体,我们的自动完成组件被绑定到 contact.language([(ngModel)]="contact.language")。但是现在,我们需要将我们的组件绑定到ContactDTO上,而ContactDTO并没有与LanguageDTO的链接,而是一个languageId和languageName属性。
\[sourcecode language="html" title="Contact-update.component.html"\]
<p-autoComplete id="field\_language" name="language" \[(ngModel)\]="selectedLanguage" field="name" \[sugges\]="suggestLanguages" (completeMethod)="searchLanguages($event)" (onSelect)="captureSelectedLanguage($event) " required>
</p-autoComplete>
\[/sourcecode]
所以诀窍是在外部selectedLanguage属性上进行双向绑定,然后使用onSelect来捕获在自动完成组件中选择的languageId和languageName。这个手稿看起来像这样。
\[sourcecode language="javascript" title="Contact-update.component.ts"\]
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;
}
\[/sourcecode\]
其他方法不会改变(如searchLanguages)。
用杰克逊视图优化网络流量
使用DTOs的一个好处是,你可以对你想要从后端到前端来回的数据非常具体。如果你检查网络日志,你会注意到当你在自动完成组件中输入数据时,会在JSON中返回一个完整的语言列表。因此,如果你输入en,自动完成组件将在URIhttp://localhost:9000/api/languages?name.contains=en调用后端API,这将返回。
{
"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
}, {
...
}
这是因为LanguageDTO拥有Language实体的所有属性。这是一个遗憾,因为我们在自动完成组件中真正需要的只是语言的ID和名称。因此,我们希望从后端返回以下的JSon结构。
{
"id" : 19,
"name" : "Bengali"
}, {
"id" : 39,
"name" : "English"
}, {
"id" : 46,
"name" : "French"
}, {
...
}
这是因为调用api/languages?name.contains=en会执行一个服务,其代码看起来像这样。
\[sourcecode language="java" title="LanguageQueryService.java"\]
public Page<LanguageDTO> findByCriteria(LanguageCriteria criteria, Pageable page) {
final Specification<Language> specification = createSpecification(c criteria);
return languageRepository.findAll(specification, page)
.map(languageMapper::toDto);
}
\[/sourcecode]
注意实体和DTO之间的映射是如何实现的 LanguageRepository.findAll返回一个Language实体的列表,由于MapStruct,它们被映射到LanguageDTO的列表。
有几种方法可以只返回语言的ID和名称(例如,创建新的DTO,定制映射器,自己建立JSON),但我喜欢使用Jackson视图。这个想法是在一个 "视图 "中分组某些属性,并告诉Jackson只序列化一个特定的视图。这样,你可以对同一个DTO有几个 "视图"(最小、正常、扩展......)。下面的代码定义了一个Minimal格式。
\[sourcecode language="java" title="Format.java"\]
public class Format {
public static class Minimal {
}
}
\[/sourcecode\]
然后,我们在Minimal格式下对id和name进行分组:
\[sourcecode language="java" title="LanguageDTO.java"\]
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;
// ...
\[/sourcecode\]
然后就只需要用Minimal视图注释REST端点,Jackson就会只序列化id和name了:
\[sourcecode language="java" title="LanguageResource.java"\]
@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);
}
\[/sourcecode]
需要CSS帮助
BTW如果你们中有人知道Boostrap、JHipster、PrimeNG并且是CSS大师,你能告诉我为什么Language前面的垂直红条没有其他的那么大?如果可以,请给我发一个PR;o)#Thanks
总结
在JHipster的GitHub问题上有很多关于创建一个类似家庭自动完成的组件的讨论。我理解JHipster希望与任何组件库都不相干。作为一个曾经的PrimeFaces用户,我很高兴发现PrimeNG周围有同样的组件、同样的团队、同样的社区。我是一个非常快乐的PrimeNG用户!
我希望这篇文章能帮助你在你的JHipster Angular应用程序中使用自动完成组件。你也应该看看其他的PrimeNG组件,它们都非常棒。