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

The dynamic concatenation of Tailwind classes is failing to have any effect, even though the full class name is being

I'm currently using Tailwind CSS within my Next.js project and I have a common method that dynamically returns the desired background color. However, despite adding the full class name, the background color is not displaying as expected. After reading ...

What is the best way to accurately parse a Date object within a TypeScript class when the HttpClient mapping is not working correctly?

Task.ts: export class Task { name: string; dueDate: Date; } tasks.service.ts: @Injectable() export class TasksService { constructor(private http: HttpClient) { } getTasks(): Observable<Task[]> { return this.http.get<Ta ...

Generate a new content hash in ngsw after compiling or while deploying

Our Angular application utilizes the angular service worker to enhance performance. The service worker compares content hashes of cached files with those in the ngsw.json file. We have implemented continuous integration and delivery (with Azure DevOps) w ...

Tips for using conditional rendering with React and TypeScript

Issue with Conditional Rendering in TypeScript It seems like I might have encountered a problem with the way I declare my components. Take a look at this TypeScript snippet: import React, { FunctionComponent } from 'react'; export const Chapte ...

Is it possible to execute TypeScript class methods in asynchronous mode without causing the main thread to be blocked?

Creating an app that retrieves attachments from specific messages in my Outlook mail and stores the data in MongoDB. The challenge lies in the time-consuming process of receiving these attachments. To address this, I aim to execute the task in a separate t ...

Combining two sets of data into one powerful tool: ngx-charts for Angular 2

After successfully creating a component chart using ngx-charts in angular 2 and pulling data from data.ts, I am now looking to reuse the same component to display a second chart with a different data set (data2.ts). Is this even possible? Can someone guide ...

Creating a conditional statement within an array.map loop in Next.js

User Interface after Processing After retrieving this dataset const array = [1,2,3,4,5,6,7,8] I need to determine if the index of the array is a multiple of 5. If the loop is on index 0, 5, 10 and so on, it should display this HTML <div class="s ...

Creating a table and filling it with data from any cell is a simple process

I have a question about creating an HTML table similar to the image shown here: image I want to populate the table with data from a JSON response received from a web service: { "ErrorCode": 0, "ErrorMessage": "ok", "Data": [ { ...

When utilizing webpack in Angular 5, the :host selector does not get converted into a component entity

Initially, I set up Angular with webpack following the configuration in this guide, including configuring webpack sass-loader. Everything was working smoothly until I encountered an issue today: app.component.ts @Component({ selector: 'ng-app&ap ...

What is the best way to pass input values to directives?

Is there a way to dynamically pass input field values to directives and then send them to a server via post request? I am looking for a solution on how to achieve this functionality. This is my HTML code: <input type="text" [appHighlight]="appHighligh ...

Toggle visibility of an Angular 4 component based on the current route

Hello there, I'm facing an issue and not sure if it's possible to resolve. Essentially, I am looking to display a component only when the route matches a certain condition, and hide another component when the route matches a different condition. ...

Using TypeScript to import a Vue 2 component into a Vue 3 application

Recently, I embarked on a new project with Vue CLI and Vite, utilizing Vue version 3.3.4 alongside TypeScript. In the process, I attempted to incorporate the vue-concise-slider into one of my components. You can find it here: https://github.com/warpcgd/vu ...

Creating Typescript libraries with bidirectional peer dependencies: A complete guide

One of my libraries is responsible for handling requests, while the other takes care of logging. Both libraries need configuration input from the client, and they are always used together. The request library makes calls to the logging library in various ...

AngularFirestoreCollection can be thought of as a reference to a collection within Firestore, which

Hey there, I need some help with the following code snippet. Data link service file private dbUser = '/users'; constructor(private firestore: AngularFirestore) { this.userCollection = firestore.collection(this.dbUser); } Now in my component fi ...

An error occurred in the ngrx store with Angular during production build: TypeError - Unable to access property 'release' of undefined

After deploying my application and running it, I encountered an issue that seems to be happening only during production build at runtime. At this point, I am uncertain whether this is a bug or if there is a mistake in my code. The error "TypeError: Cannot ...

Not verifying the argument type in Typescript makes the function generic in nature

I was initially under the impression that TypeScript would throw an error due to passing the incorrect number of elements in EntryPoints, but surprisingly, no error occurred. function createContext<T>(defaultValue: T): T[] { return [defaultValue] ...

Error code 2532 occurs when trying to access an object using square brackets in TypeScript

Encountered the ts error: Object is possibly 'undefined'.(2532) issue while trying to access the value of a field within an object, where the object key corresponds to a value in an Enum. Below is a concise example to showcase this problem: en ...

Solving the issue of refreshing HTML Canvas drawings in Vue3 using the Composition API

In my typescript code base, I have successfully created a Sudoku board by directly manipulating the DOM and utilizing an HTML Canvas element with its API. Now, I am looking to elevate my project to a full website and integrate what I have into a Vue3 proj ...

Steps for mandating the specification of a type parameter for a generic React component

When setting up a new instance of a generic React component, I noticed that the TypeScript type checker automatically defaults to unknown without requiring me to specify the type argument: Ideally, I would prefer if TypeScript prompted for the explicit ty ...

Encountering a compilation error while compiling using Angular Ivy

Encountering a compile time error in my Angular 8 project when enabling angular Ivy. Upgrading to version 8.1.0 did not solve the issue, and I continue to receive the following error: D:\Users\Backup>ng build shared Building Angular Package B ...