【问题标题】:How Does One Properly Pass Functions to Custom Directives in Angular2/Typescript?如何正确地将函数传递给 Angular2/Typescript 中的自定义指令?
【发布时间】:2018-10-23 14:18:12
【问题描述】:

我对 Angular 2 还很陌生。我正在从 AngularJS 升级我的应用程序,现在我正处于 UI/UX 开发的最后阶段。我有最后一个问题需要帮助。提前谢谢你。

当前计划

  1. 我有一个自定义指令 TspFieldWidgetDirective,它接受多个输入,其中一个是函数 @Input('onRefresh') public refresh: Function;
  2. TspFieldWidgetDirective 元素可用于各种组件内部,以重新设计标准表单字段。
  3. 我将当前指令用作 TspHeaderComponent 模板内的选择框。
  4. 当用户更改选择框的值时,会调用OnRefresh 函数来刷新应用程序以更新新公司的视图。

什么不起作用

  1. 每次在浏览器中加载 TspHeaderComponent 模板时,它都会产生对 OnRefresh 函数 onCompanyChange 的无限循环调用。仅在选择框更改值时调用一次。

  2. 当我从浏览器更改选择框的值时(在 TspFieldWidgetDirective 模板内 - (change)="refresh({'id': fieldData[fieldKey]})",会生成以下错误。 ERROR TypeError: _co.refresh is not a function

请注意

  1. 该功能在 Angular 5 中从未运行过,但在 AngularJS 中运行良好。
  2. 除了将函数作为输入传递给指令外,其他所有方法都有效。

代码片段如下:

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>

【问题讨论】:

  • 一个 onRefresh @Input 是倒退的。您应该为此使用@Output
  • @IngoBürk 不幸的是,这没有用。我是 TypeScript 的新手,但 TspFieldWidgerComponent 接受一个函数作为输入,它没有发出任何事件,这不是 \@Ouput 的用途吗?
  • 好吧,你带我去喝水,但我不得不喝,哈哈,我不得不做很多技巧,但最终想通了@IngoBürk

标签: angular typescript angular2-template angular2-directives angular2-components


【解决方案1】:

Angular 的变更检测在视图初始化期间已经被触发。但此时 refresh() 可能仍为空/未定义。

我建议不要直接调用'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);
    }

}

这样您可以确保仅在刷新已存在时才调用它。

【讨论】:

  • 似乎在渲染 TspHeaderComponent 时调用了该函数,并在那里产生了无限循环。只要标题组件在视图中,就会不幸调用该函数。
【解决方案2】:

完成修复

tsp-field-widget.component.ts - 将类型为 Function 的所有 @Inputs 更改为 @Ouput 并初始化为 EventEmitters。将所有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>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-16
    • 2019-11-05
    • 2016-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多