What is the Correct Way to Send Functions to Custom Directives in Angular 2 Using TypeScript?

I am relatively new to Angular 2. I am currently in the process of upgrading my application from AngularJS and focusing on completing the UI/UX development. There is one final issue that I am seeking help with, and I appreciate any assistance provided.

Current Strategy

  1. I have created a custom directive called TspFieldWidgetDirective, which accepts multiple inputs, including a function
    @Input('onRefresh') public refresh: Function;
    .
  2. The TspFieldWidgetDirective element can be utilized within various components to transform standard form fields.
  3. I have integrated the current directive as a select box within the template of the TspHeaderComponent.
  4. Upon changing the value of the select box, the OnRefresh function is invoked to refresh the application for updating the view based on the selection of a new company.

Issues Encountered

  1. Every time the TspHeaderComponent template loads in the browser, it triggers an endless loop of calls to the OnRefresh function, specifically onCompanyChange, instead of being executed only once when the select box values change.

  2. When modifying the value of the select box from the browser (within the TspFieldWidgetDirective template -

    (change)="refresh({'id': fieldData[fieldKey]})"
    ), it results in the following error.
    ERROR TypeError: _co.refresh is not a function

Note

  1. The functionality has never functioned correctly in Angular 5; however, it worked perfectly in AngularJS.
  2. All features work except passing a function as input to the directive.

Code Snippets Provided Below:

tsp-header.component.ts

/*
    Method for changing the 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
  
  ... Additional Input Properties ...

  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>

Answer №1

During the initialization of the view in Angular, the change detection is already activated. However, it is possible that at this point, 'refresh()' is still undefined or null.

Instead of directly calling 'refresh()', I suggest creating a wrapper method that can verify if 'refresh()' has been passed in.

You can implement something like the following in your tsp-field-widget.html:

(change)="doRefresh({'id': fieldData[fieldKey]})"

And in your tsp-field-widget.component.ts:

private doRefresh(object: any): void {

    if(refresh) {
       refresh(object);
    }

}

This approach ensures that 'refresh' will only be executed if it exists.

Answer №2

Complete Solution

tsp-field-widget.component.ts - Modified all @Inputs of type Function to @Output and initialized them as EventEmitters. Replaced all prefixes of on with evt since on is not allowed as a prefix for @Output. Introduced four new methods that will be executed once events are triggered. Each of these new methods must have an interface assigned to the args argument for consistency.

// 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; // whether the field is required
  @Input() public isReadonly: boolean;

  // Other @Inputs removed for brevity...

  @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;
  }
  
  // Methods for emitting events based on actions
  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 - No changes needed

tsp-header.component.ts - Updated to pass an object containing required values to the function.

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);
            }

            // Implementation logic...
        }
    }
}

tsp-header.html - Renamed onRefresh attribute to evtRefresh. To prevent infinite loops, changed from brackets to parenthesis around (evtRefresh) indicating it's an event, not an object. Also updated the function argument to always be $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>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Automatically update the cart count in the header component in Angular 6 when a product is added to the cart, without the need to

I am working on an E-Commerce application using Angular 6. I am facing an issue with updating the shopping cart count in the header component whenever a product is added or removed from the cart. The cart count only updates when I refresh the page. I have ...

Implement the click event binding using classes in Angular 2

If I have the template below, how can I use TypeScript to bind a click event by class? My goal is to retrieve attributes of the clicked element. <ul> <li id="1" class="selectModal">First</li> <li id="2" class="selectModal">Seco ...

Access to Angular CORS request has been blocked

I'm currently working on establishing a connection between my Angular application and a basic REST server using express. The server responds to requests with JSON data exclusively. To enable CORS support, I've integrated the cors module from npm ...

Stringified HTML code showcased

When working with Angular, I have encountered an issue where I am calling a function inside an .html file that returns a string containing a table element. My goal is to convert this string into HTML code. I attempted to use [innerHtml]: <p [innerHtml ...

What could be the reason for the Angular2 Component property not appearing on my webpage?

Here is the code snippet I am working with: import {Component} from "@angular/core"; @Component({ selector: 'my-app', template: ` <h1>{{title}}</h1> <h2>{{secondTitle}}</h2> <main-page></ma ...

Show a modal component from another component in Angular 2

As a newcomer to Angular, I'm working on a modal component that changes from hiding to showing when a button with (click) is clicked. The goal is to integrate this modal into my main component, allowing me to display the modal on top of the main conte ...

Adjust the size of the mat-expansion indicator to your desired height and width

Trying to modify the width and height of the mat indicator has been a bit challenging. Despite following suggestions from other similar questions, such as adjusting the border width and padding, I am still unable to see the changes reflect in my CSS file ...

The triggering of Angular Change Detection does not occur when using nested ngFor loops

Currently, I'm deeply engrossed in a substantial Angular project that utilizes NgRx Store. One interesting feature of the app is an infinite scrolling list that displays skeleton items at the end, which are later replaced by real items once the reques ...

Using TypeScript to automatically deduce the output type of a function by analyzing the recursive input type

I am currently working on developing an ORM for a graph database using TypeScript. Specifically, I am focusing on enhancing the "find" method to retrieve a list of a specific entity. The goal is to allow the function to accept a structure detailing the joi ...

Adjust the tally of search results and modify the selection depending on the frequency of the user's searches within an array of objects

Seeking assistance with adding a new function that allows users to navigate to the next searched result. Big thanks to @ggorlen for aiding in the recursive search. https://i.stack.imgur.com/OsZOh.png I have a recursive search method that marks the first ...

Sharing markdown content between two Vue.js components

I have a markdown editor in View A which is displaying the result in the current View. My goal is to share this result with another page, View B. In View A, there is a button that allows the user to share the markdown result with View B. I am using a texta ...

Tips for setting values to the child component property in Angular 4

When I was using ngif, I encountered an issue with getting the element reference of the child component. After extensive searching, I discovered that in order to access the element, I needed to use view children instead of view child. While I am able to ge ...

When the page is first loaded, the select options dropdown using NgFor and NgValue does not display the initial object from the list

I am facing an issue with a select options dropdown that contains a list of objects. I have used ngValue to set the value of the dropdown as an object. However, upon page load, the dropdown does not display the first object from the list; it only shows obj ...

What are the steps to configure Auth0 for an Angular application?

I'm having trouble implementing Auth0 into my angular app. After configuring it on [https://manage.auth0.com/dashboard/], clicking the save changes button results in this error: Error!Payload validation error: 'Object didn't pass validatio ...

Utilize TypeScript enum keys to generate a new enum efficiently

I am in need of two Typescript enums as shown below: export enum OrientationAsNumber { PORTRAIT, SQUARE, LANDSCAPE } export enum OrientationAsString { PORTRAIT = 'portrait', SQUARE = 'square', LANDSCAPE = 'landscape&ap ...

Exploring the File Selection Dialog in Node.js with TypeScript

Is it possible to display a file dialog in a Node.js TypeScript project without involving a browser or HTML? In my setup, I run the project through CMD and would like to show a box similar to this image: https://i.stack.imgur.com/nJt3h.png Any suggestio ...

Utilizing Angular to reference a computed variable in a different function while iterating through a loop

I recently started learning Angular 5 and coming from a background in PHP, I am finding variables a bit confusing. Currently, I am working on a loop where I want to use a calculated variable in another function within that same loop. Here is the code snip ...

What is the process of accessing the changelog.md file within a component in Angular?

My challenge is to showcase the content from my changelog.md file, which is situated at the same level as the package.json file. I created a service for this purpose using the following code, function getData() { return http.get('../c ...

Issue with React filter function where data is not being displayed when search query is left

I am facing an issue where the data does not show up when the search term is empty. Previously, I used to have this line in my code if (!searchTerm) return data for my old JSON data, and it worked fine. However, now that I'm using Sanity CDN, this lo ...

Discovering a specific property of an object within an array using Typescript

My task involves retrieving an employer's ID based on their name from a list of employers. The function below is used to fetch the list of employers from another API. getEmployers(): void { this.employersService.getEmployers().subscribe((employer ...