如何在 Angular2/Typescript 中正确地将函数传递给自定义指令?
How Does One Properly Pass Functions to Custom Directives in Angular2/Typescript?
我是 Angular 2 的新手。我正在从 AngularJS 升级我的应用程序,我现在正处于 UI/UX 开发的最后阶段。我还有最后一个问题需要帮助。提前谢谢你。
当前计划
- 我有一个自定义指令 TspFieldWidgetDirective 接受多个输入,一个是函数
@Input('onRefresh') public refresh: Function;
.
- TspFieldWidgetDirective 元素可用于各种组件内部以重新设计标准表单字段。
- 我将当前指令用作 TspHeaderComponent 模板中的 select 框。
- 当用户更改 select 框的值时,会调用
OnRefresh
函数来刷新应用程序以更新新公司的视图。
什么不起作用
每次在浏览器中加载 TspHeaderComponent 模板时,都会产生对 OnRefresh
函数的无限循环调用,onCompanyChange,只应调用一次,当 select 框更改值时。
当我从浏览器更改 select 框的值时(在 TspFieldWidgetDirective 模板 - (change)="refresh({'id': fieldData[fieldKey]})"
内,以下产生错误。
ERROR TypeError: _co.refresh is not a function
请注意
- 该功能在 Angular 5 中从未起作用,它在 AngularJS 中运行得非常好。
- 除将函数作为输入传递给指令外,其他一切都有效。
下面的代码片段:
tsp-header.component.ts
/*
Method to change company of the company selector
@param: id - string
@return: none
*/
public onCompanyChange(id: string): void {
if ((__env.debug && __env.verbose)) {
console.log('Setting current_company_id to ' + id);
}
if (id !== undefined && id !== null)
{
// @TODO: Update user preference on server IF they have permission to change their default company
this.cookies.set('preferences.current_company_id', id);
this.core.app.current_user.preferences.current_company_id = id;
this.core.initApp();
this.router.navigate(['/app/dashboard']);
}
}
tsp-header.html
<!-- company changer -->
<li>
<tsp-field-widget
type="company-select"
[onRefresh]="onCompanyChange(id)"
[showAvatar]="true"
[fieldData]="core.app.current_user.preferences"
fieldKey="current_company_id"
[hideLabel]="true"
[optionData]="core.app.session.base_companies">
</tsp-field-widget>
</li>
tsp-field-widget.component.ts
// Component class
@Component({
selector: 'tsp-field-widget',
templateUrl: './templates/tsp-field-widget.html'
})
export class TspFieldWidgetComponent implements OnInit {
public lang:any; // for i18n
@Input() public type: string; // the field type
@Input() public isRequired: boolean; // is the field required
@Input() public isReadonly: boolean;
@Input() public index: any; // index of ng-repeat
@Input() public filterBy: any; // used in conjunction with ng-repeat
@Input() public orderBy: any; // used in conjunction with ng-repeat
@Input() public fieldData: any; // the record of ng-repeat
@Input() public fieldKey: string; // the index of record - record[fieldKey]
@Input() public placeVal: string; // the placeholder value of the field, usually used for selects and other dropdowns
@Input() public pattern: string; // used for ng-pattern
@Input() public prefix: string; // Text to display before title listings
@Input() public recordId: any; // the ID of the record
@Input() public label: string; // the label for the field for <label> tag
@Input() public suffix: string; // sub label, usually placed below some label or title
@Input() public optionData: any[]; // array of data used to populate selects or to store data values
@Input() public showAvatar: boolean; // show the fields avatar
@Input() public hideLabel: boolean; // show the fields <label>
@Input() public showAdd: boolean; // show the add button (to add new record)
@Input() public showTitle: boolean; // avatar type: show the name of the user
@Input() public showDesc: boolean; // avatar type: show the name of the user
// @Input() public isHighlighted:boolean; // default type: highlight text
@Input() public numRows: string; // textarea: the number of rows of the text area
@Input() public idKey: string; // select: the default ID key for option data - defaults to _id
@Input() public titleKey: string; // select: the default title key for option data - defaults to title
@Input() public descKey: string; // select: the default description key for option data - defaults to description
@Input() public sliderType: string; // percent, amount or default, slider type
@Input() public sliderMinValue: string; // slider type
@Input() public sliderMaxValue: string; // slider type
@Input() public sliderStepValue: string; // slider type
@Input() public sliderOrientation: string; // slider type
@Input() public dzOptions: any; // dropzone options
@Input() public dzCallbacks: any; // dropzone callbacks
@Input() public dzMethods: any; // dropzone methods
@Input() public decimalPlaces: string; // tspFormat type
@Input() public locale: string; // for dates and currency
@Input() public format: string; // for dates
@Input() public formatHours: string; // for dates
@Input() public intervalMins: number; // for dates
@Input() public owner: string; // used for module windows to determine the type of record to add
@Input('onAdd') public add: Function;
@Input('onEdit') public edit: Function;
@Input('onToggle') public toggle: Function;
@Input('onDelete') public delete: Function;
@Input('onRefresh') public refresh: Function;
constructor(private el: ElementRef,
private cookies: TspCookiesService,
public core: TspCoreService,
private object: TspObjectService,
public date: TspDateService) {
this.lang = core.lang;
this.date = date;
}
}
tsp-field-widget.html
<div *ngIf="type=='company-select'">
<select class="company-select"
class="form-control"
[(ngModel)]="fieldData[fieldKey]"
(change)="refresh({'id': fieldData[fieldKey]})"
data-parsley-trigger="change">
<option [selected]="x[idKey] === fieldData[fieldKey]" *ngFor="let x of optionData" [ngValue]="x[idKey]">
{{x[titleKey]}}
</option>
</select>
</div>
Angular 的更改检测已在视图初始化期间触发。但是此时refresh()可能还是null/undefined。
我建议不要直接调用 'refresh()',而是可以检查是否已传入 refresh() 的包装方法。
你的 tsp-field-widget.html 中有这样的内容:
(change)="doRefresh({'id': fieldData[fieldKey]})"
这在你的 tsp-field-widget.component.ts
private doRefresh(object: any): void {
if(refresh) {
refresh(object);
}
}
这样您就可以确保仅在刷新已存在时才调用它。
完成修复
tsp-field-widget.component.ts
- 将 Function
类型的所有 @Inputs
更改为 @Ouput
并初始化为 EventEmitter。将所有 on
前缀替换为 evt
,因为 on
不允许作为 @Output
的前缀。添加了四个新方法,一旦事件被触发,这些方法就会被调用。每个新方法都需要有一个接口分配给 args
参数以保持一致性
// Component class
@Component({
selector: 'tsp-field-widget',
templateUrl: './templates/tsp-field-widget.html'
})
export class TspFieldWidgetComponent implements OnInit {
public lang:any; // for i18n
@Input() public type: string; // the field type
@Input() public isRequired: boolean; // is the field required
@Input() public isReadonly: boolean;
@Input() public index: any; // index of ng-repeat
@Input() public filterBy: any; // used in conjunction with ng-repeat
@Input() public orderBy: any; // used in conjunction with ng-repeat
@Input() public fieldData: any; // the record of ng-repeat
@Input() public fieldKey: string; // the index of record - record[fieldKey]
@Input() public placeVal: string; // the placeholder value of the field, usually used for selects and other dropdowns
@Input() public pattern: string; // used for ng-pattern
@Input() public prefix: string; // Text to display before title listings
@Input() public recordId: any; // the ID of the record
@Input() public label: string; // the label for the field for <label> tag
@Input() public suffix: string; // sub label, usually placed below some label or title
@Input() public optionData: any[]; // array of data used to populate selects or to store data values
@Input() public showAvatar: boolean; // show the fields avatar
@Input() public hideLabel: boolean; // show the fields <label>
@Input() public showAdd: boolean; // show the add button (to add new record)
@Input() public showTitle: boolean; // avatar type: show the name of the user
@Input() public showDesc: boolean; // avatar type: show the name of the user
// @Input() public isHighlighted:boolean; // default type: highlight text
@Input() public numRows: string; // textarea: the number of rows of the text area
@Input() public idKey: string; // select: the default ID key for option data - defaults to _id
@Input() public titleKey: string; // select: the default title key for option data - defaults to title
@Input() public descKey: string; // select: the default description key for option data - defaults to description
@Input() public sliderType: string; // percent, amount or default, slider type
@Input() public sliderMinValue: string; // slider type
@Input() public sliderMaxValue: string; // slider type
@Input() public sliderStepValue: string; // slider type
@Input() public sliderOrientation: string; // slider type
@Input() public dzOptions: any; // dropzone options
@Input() public dzCallbacks: any; // dropzone callbacks
@Input() public dzMethods: any; // dropzone methods
@Input() public decimalPlaces: string; // tspFormat type
@Input() public locale: string; // for dates and currency
@Input() public format: string; // for dates
@Input() public formatHours: string; // for dates
@Input() public intervalMins: number; // for dates
@Input() public owner: string; // used for module windows to determine the type of record to add
@Output() public evtAdd = new EventEmitter();
@Output() public evtEdit = new EventEmitter();
@Output() public evtToggle = new EventEmitter();
@Output() public evtDelete = new EventEmitter();
@Output() public evtRefresh = new EventEmitter();
constructor(private el: ElementRef,
private cookies: TspCookiesService,
public core: TspCoreService,
private object: TspObjectService,
public date: TspDateService) {
this.lang = core.lang;
this.date = date;
}
add(args: IAdd){
this.evtAdd.emit(args);
}
edit(args: IEdit){
this.evtEdit.emit(args);
}
toggle(args: IToggle){
this.evtToggle.emit(args);
}
delete(args: IDelete){
this.evtDelete.emit(args);
}
refresh(args: IRefresh){
this.evtRefresh.emit(args);
}
}
tsp-field-widget.html
- 无需更改
tsp-header.component.ts
- 更新为传入包含函数所需值的对象。
public onCompanyChange(args: IRefresh):void {
if (args !== undefined){
if (args.id !== undefined && args.id !== null)
{
if ((__env.debug && __env.verbose)) {
console.log('Setting current_company_id to ' + args.id);
}
// @TODO: Update user preference on server IF they have permission to change their default company
this.cookies.set('preferences.current_company_id', args.id);
this.core.app.current_user.preferences.current_company_id = args.id;
this.core.initApp();
this.router.navigate(['/app/dashboard']);
}
}
}
tsp-header.html
- 将 onRefresh
属性重命名为 evtRefresh
。为了防止无限循环,我用圆括号而不是括号将 evtRefresh
属性包装起来,以表示该属性是一个事件而不是一个对象。此外,函数参数始终必须是 $event
.
<!-- company changer -->
<li>
<tsp-field-widget
type="company-select"
(evtRefresh)="onCompanyChange($event)"
[showAvatar]="true"
[fieldData]="core.app.current_user.preferences"
fieldKey="current_company_id"
[hideLabel]="true"
[optionData]="core.app.session.base_companies">
</tsp-field-widget>
</li>
我是 Angular 2 的新手。我正在从 AngularJS 升级我的应用程序,我现在正处于 UI/UX 开发的最后阶段。我还有最后一个问题需要帮助。提前谢谢你。
当前计划
- 我有一个自定义指令 TspFieldWidgetDirective 接受多个输入,一个是函数
@Input('onRefresh') public refresh: Function;
. - TspFieldWidgetDirective 元素可用于各种组件内部以重新设计标准表单字段。
- 我将当前指令用作 TspHeaderComponent 模板中的 select 框。
- 当用户更改 select 框的值时,会调用
OnRefresh
函数来刷新应用程序以更新新公司的视图。
什么不起作用
每次在浏览器中加载 TspHeaderComponent 模板时,都会产生对
OnRefresh
函数的无限循环调用,onCompanyChange,只应调用一次,当 select 框更改值时。当我从浏览器更改 select 框的值时(在 TspFieldWidgetDirective 模板 -
(change)="refresh({'id': fieldData[fieldKey]})"
内,以下产生错误。ERROR TypeError: _co.refresh is not a function
请注意
- 该功能在 Angular 5 中从未起作用,它在 AngularJS 中运行得非常好。
- 除将函数作为输入传递给指令外,其他一切都有效。
下面的代码片段:
tsp-header.component.ts
/*
Method to change company of the company selector
@param: id - string
@return: none
*/
public onCompanyChange(id: string): void {
if ((__env.debug && __env.verbose)) {
console.log('Setting current_company_id to ' + id);
}
if (id !== undefined && id !== null)
{
// @TODO: Update user preference on server IF they have permission to change their default company
this.cookies.set('preferences.current_company_id', id);
this.core.app.current_user.preferences.current_company_id = id;
this.core.initApp();
this.router.navigate(['/app/dashboard']);
}
}
tsp-header.html
<!-- company changer -->
<li>
<tsp-field-widget
type="company-select"
[onRefresh]="onCompanyChange(id)"
[showAvatar]="true"
[fieldData]="core.app.current_user.preferences"
fieldKey="current_company_id"
[hideLabel]="true"
[optionData]="core.app.session.base_companies">
</tsp-field-widget>
</li>
tsp-field-widget.component.ts
// Component class
@Component({
selector: 'tsp-field-widget',
templateUrl: './templates/tsp-field-widget.html'
})
export class TspFieldWidgetComponent implements OnInit {
public lang:any; // for i18n
@Input() public type: string; // the field type
@Input() public isRequired: boolean; // is the field required
@Input() public isReadonly: boolean;
@Input() public index: any; // index of ng-repeat
@Input() public filterBy: any; // used in conjunction with ng-repeat
@Input() public orderBy: any; // used in conjunction with ng-repeat
@Input() public fieldData: any; // the record of ng-repeat
@Input() public fieldKey: string; // the index of record - record[fieldKey]
@Input() public placeVal: string; // the placeholder value of the field, usually used for selects and other dropdowns
@Input() public pattern: string; // used for ng-pattern
@Input() public prefix: string; // Text to display before title listings
@Input() public recordId: any; // the ID of the record
@Input() public label: string; // the label for the field for <label> tag
@Input() public suffix: string; // sub label, usually placed below some label or title
@Input() public optionData: any[]; // array of data used to populate selects or to store data values
@Input() public showAvatar: boolean; // show the fields avatar
@Input() public hideLabel: boolean; // show the fields <label>
@Input() public showAdd: boolean; // show the add button (to add new record)
@Input() public showTitle: boolean; // avatar type: show the name of the user
@Input() public showDesc: boolean; // avatar type: show the name of the user
// @Input() public isHighlighted:boolean; // default type: highlight text
@Input() public numRows: string; // textarea: the number of rows of the text area
@Input() public idKey: string; // select: the default ID key for option data - defaults to _id
@Input() public titleKey: string; // select: the default title key for option data - defaults to title
@Input() public descKey: string; // select: the default description key for option data - defaults to description
@Input() public sliderType: string; // percent, amount or default, slider type
@Input() public sliderMinValue: string; // slider type
@Input() public sliderMaxValue: string; // slider type
@Input() public sliderStepValue: string; // slider type
@Input() public sliderOrientation: string; // slider type
@Input() public dzOptions: any; // dropzone options
@Input() public dzCallbacks: any; // dropzone callbacks
@Input() public dzMethods: any; // dropzone methods
@Input() public decimalPlaces: string; // tspFormat type
@Input() public locale: string; // for dates and currency
@Input() public format: string; // for dates
@Input() public formatHours: string; // for dates
@Input() public intervalMins: number; // for dates
@Input() public owner: string; // used for module windows to determine the type of record to add
@Input('onAdd') public add: Function;
@Input('onEdit') public edit: Function;
@Input('onToggle') public toggle: Function;
@Input('onDelete') public delete: Function;
@Input('onRefresh') public refresh: Function;
constructor(private el: ElementRef,
private cookies: TspCookiesService,
public core: TspCoreService,
private object: TspObjectService,
public date: TspDateService) {
this.lang = core.lang;
this.date = date;
}
}
tsp-field-widget.html
<div *ngIf="type=='company-select'">
<select class="company-select"
class="form-control"
[(ngModel)]="fieldData[fieldKey]"
(change)="refresh({'id': fieldData[fieldKey]})"
data-parsley-trigger="change">
<option [selected]="x[idKey] === fieldData[fieldKey]" *ngFor="let x of optionData" [ngValue]="x[idKey]">
{{x[titleKey]}}
</option>
</select>
</div>
Angular 的更改检测已在视图初始化期间触发。但是此时refresh()可能还是null/undefined。
我建议不要直接调用 'refresh()',而是可以检查是否已传入 refresh() 的包装方法。
你的 tsp-field-widget.html 中有这样的内容:
(change)="doRefresh({'id': fieldData[fieldKey]})"
这在你的 tsp-field-widget.component.ts
private doRefresh(object: any): void {
if(refresh) {
refresh(object);
}
}
这样您就可以确保仅在刷新已存在时才调用它。
完成修复
tsp-field-widget.component.ts
- 将 Function
类型的所有 @Inputs
更改为 @Ouput
并初始化为 EventEmitter。将所有 on
前缀替换为 evt
,因为 on
不允许作为 @Output
的前缀。添加了四个新方法,一旦事件被触发,这些方法就会被调用。每个新方法都需要有一个接口分配给 args
参数以保持一致性
// Component class
@Component({
selector: 'tsp-field-widget',
templateUrl: './templates/tsp-field-widget.html'
})
export class TspFieldWidgetComponent implements OnInit {
public lang:any; // for i18n
@Input() public type: string; // the field type
@Input() public isRequired: boolean; // is the field required
@Input() public isReadonly: boolean;
@Input() public index: any; // index of ng-repeat
@Input() public filterBy: any; // used in conjunction with ng-repeat
@Input() public orderBy: any; // used in conjunction with ng-repeat
@Input() public fieldData: any; // the record of ng-repeat
@Input() public fieldKey: string; // the index of record - record[fieldKey]
@Input() public placeVal: string; // the placeholder value of the field, usually used for selects and other dropdowns
@Input() public pattern: string; // used for ng-pattern
@Input() public prefix: string; // Text to display before title listings
@Input() public recordId: any; // the ID of the record
@Input() public label: string; // the label for the field for <label> tag
@Input() public suffix: string; // sub label, usually placed below some label or title
@Input() public optionData: any[]; // array of data used to populate selects or to store data values
@Input() public showAvatar: boolean; // show the fields avatar
@Input() public hideLabel: boolean; // show the fields <label>
@Input() public showAdd: boolean; // show the add button (to add new record)
@Input() public showTitle: boolean; // avatar type: show the name of the user
@Input() public showDesc: boolean; // avatar type: show the name of the user
// @Input() public isHighlighted:boolean; // default type: highlight text
@Input() public numRows: string; // textarea: the number of rows of the text area
@Input() public idKey: string; // select: the default ID key for option data - defaults to _id
@Input() public titleKey: string; // select: the default title key for option data - defaults to title
@Input() public descKey: string; // select: the default description key for option data - defaults to description
@Input() public sliderType: string; // percent, amount or default, slider type
@Input() public sliderMinValue: string; // slider type
@Input() public sliderMaxValue: string; // slider type
@Input() public sliderStepValue: string; // slider type
@Input() public sliderOrientation: string; // slider type
@Input() public dzOptions: any; // dropzone options
@Input() public dzCallbacks: any; // dropzone callbacks
@Input() public dzMethods: any; // dropzone methods
@Input() public decimalPlaces: string; // tspFormat type
@Input() public locale: string; // for dates and currency
@Input() public format: string; // for dates
@Input() public formatHours: string; // for dates
@Input() public intervalMins: number; // for dates
@Input() public owner: string; // used for module windows to determine the type of record to add
@Output() public evtAdd = new EventEmitter();
@Output() public evtEdit = new EventEmitter();
@Output() public evtToggle = new EventEmitter();
@Output() public evtDelete = new EventEmitter();
@Output() public evtRefresh = new EventEmitter();
constructor(private el: ElementRef,
private cookies: TspCookiesService,
public core: TspCoreService,
private object: TspObjectService,
public date: TspDateService) {
this.lang = core.lang;
this.date = date;
}
add(args: IAdd){
this.evtAdd.emit(args);
}
edit(args: IEdit){
this.evtEdit.emit(args);
}
toggle(args: IToggle){
this.evtToggle.emit(args);
}
delete(args: IDelete){
this.evtDelete.emit(args);
}
refresh(args: IRefresh){
this.evtRefresh.emit(args);
}
}
tsp-field-widget.html
- 无需更改
tsp-header.component.ts
- 更新为传入包含函数所需值的对象。
public onCompanyChange(args: IRefresh):void {
if (args !== undefined){
if (args.id !== undefined && args.id !== null)
{
if ((__env.debug && __env.verbose)) {
console.log('Setting current_company_id to ' + args.id);
}
// @TODO: Update user preference on server IF they have permission to change their default company
this.cookies.set('preferences.current_company_id', args.id);
this.core.app.current_user.preferences.current_company_id = args.id;
this.core.initApp();
this.router.navigate(['/app/dashboard']);
}
}
}
tsp-header.html
- 将 onRefresh
属性重命名为 evtRefresh
。为了防止无限循环,我用圆括号而不是括号将 evtRefresh
属性包装起来,以表示该属性是一个事件而不是一个对象。此外,函数参数始终必须是 $event
.
<!-- company changer -->
<li>
<tsp-field-widget
type="company-select"
(evtRefresh)="onCompanyChange($event)"
[showAvatar]="true"
[fieldData]="core.app.current_user.preferences"
fieldKey="current_company_id"
[hideLabel]="true"
[optionData]="core.app.session.base_companies">
</tsp-field-widget>
</li>