Designing a versatile Angular component for inputting data (Mailing Address)

Currently, I am in the process of developing an Angular 11 application that requires input for three distinct mailing addresses. Initially, I thought I had a clear understanding of what needed to be done, only to encounter warnings about elements with non-unique IDs. Following my failed attempt, I delved into Single Component Angular Modules and decided to experiment with a SCAM component on my reference application.

The runtime warnings I am facing are:

[DOM] Found 2 elements with non-unique id #Address2:
[DOM] Found 2 elements with non-unique id #Address1:

Below is a snippet of the concept address component I have been working on:

import { Component, OnInit, Input, Output, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
//
@Component({
  selector: 'app-address',
  template: `
<label for='Address1'>{{addressLabel}}</label>
...
...

In my app.component.html, you will find the following sections pertaining to the addresses:

...
<app-address
  [disabled]='addressDisabled'
  [addressLabel]='addressLabel_1'
  [address1]='address1_1'
  [address2]='address2_1'>
</app-address>
...

As for my app.module setup, it looks like this:

import { NgModule } from '@angular/core';
...
...

At this juncture, I am seeking the most effective solution to streamline the data entry process for multiple addresses within my application.

Answer №1

If you're looking to create customized form controls, I recommend exploring reactive forms. This approach allows you to pass the form control itself to the child component, which can then handle any initial values and validations needed. By using custom form controls, you can reuse them for various form inputs, not limited to just addresses.

For a practical example tailored to your requirements, consider structuring the address as a FormGroup within a FormArray. In the provided sample code, I've also included a "name" form control to showcase how this customization can extend to other inputs.

To implement this in your Angular application, start by importing ReactiveFormsModule in your app module. Then, construct the form structure in the parent component:

import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'

// ....

export class AppComponent  {
  myForm: FormGroup;

  get addressArr() {
    return (this.myForm.get('addresses') as FormArray).controls;
  }

  constructor(private fb: FormBuilder) {
    this.myForm = this.fb.group({
      name: ['', [Validators.minLength(10)]],
      addresses: this.createAddresses()
    });
  }

  createAddresses() {
    let address = this.fb.array([]);
    for(let i = 0; i < 3; i++) {
      address.push(
        this.fb.group({
          address1: [{value: '', disabled: false}, [Validators.maxLength(10)]],
          address2: [{value: '', disabled: false}]
        })
      )
    }
    return address;
  }
}

In the above code snippet, I've set the 'disabled' property to false for demonstration purposes. Adjust it to true if initial disabling is required.

The child component, designed to receive form controls from the parent, will display the input field along with any corresponding validation errors:

import { Component, Input } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'hello',
  template: `
    <input [formControl]="ctrl" [placeholder]="placeholder"/>
    <small *ngIf="ctrl.hasError('maxlength')">Max 20 characters!</small>
    <small *ngIf="ctrl.hasError('minlength')">Min 10 characters!</small>
  `
})
export class CustomFormControl  {
  @Input() ctrl: FormControl;
  @Input() placeholder: string;

}

In the parent component, include the child components within the appropriate sections and pass the form controls accordingly. Remember to iterate over a FormArray when dealing with multiple groupings:

<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
  <custom-control [ctrl]="myForm.get('name')" placeholder="Name"></custom-control>
  <div *ngFor="let group of addressArr; let i = index">
    <div formArrayName="addresses">
      <div [formGroupName]="i">
        <custom-control [ctrl]="group.get('address1')" placeholder="Address 1"></custom-control>
        <custom-control [ctrl]="group.get('address2')" placeholder="Address 2"></custom-control>
      </div>
    </div>
  </div>
</form>

Feel free to refer to this Stackblitz link for a hands-on illustration.

Answer №2

I had to make several adjustments for this specific template-driven solution. Passing a class to the component proved to be more effective by leveraging the mutability of template-driven.

// ===========================================================================
// File: address.ts
export interface IAddress {
  Address1: string;
  Address2: string;
  City: string;
  State: string;
  Zip: string;
  Country: string;
  toString( ): string;
  getAddress( ): string;
}
//
export class Address implements IAddress {
  /*
  ** Create an empty/new instance.
  ** (Should occur first in class per lint)
  */
  public static empty( ): IAddress {
    return new Address( '', '', '', '', '', '' );
  }
  //
  constructor(
    public Address1: string,
    public Address2: string,
    public City: string,
    public State: string,
    public Zip: string,
    public Country: string,
  ) { }
  /*
  ** Combine numerous address fields.
  */
  getAddress(): string {
    const _addr: string = (this.Address2 !== '' ?
      `${this.Address1}, ${this.Address2}` :
      this.Address1 );
    return `${_addr}, ${this.City}, ${this.State} ${this.Zip} ${this.Country}`;
  }
  /*
  ** toString implementation for class address
  */
  public toString = (): string => {
    return JSON.stringify( this );
  }
  //
}
// ===========================================================================

The component code is as follows:

// ===========================================================================
// File: address.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
//
import { IAddress, Address } from '../../admin/classes/address';
//
@Component({
  selector: 'app-address',
  template: `
<div class='row form-group' [class.has-error]='Address1.invalid && Address1.touched && validation'>
  <div class='col-lg-3 col-md-4 col-sm-12 nsg-primary-color nsg-text-right'><label for='Address1{{uniquePostfix}}'>{{addressLabel}}</label></div>
  <div class='col-lg-8 col-md-7 col-sm-12'>
    <input type='text' id='Address1{{uniquePostfix}}' name='Address1{{uniquePostfix}}' required maxlength='50' class='nsg-input-wide'
      #Address1='ngModel' [(ngModel)]='model.Address1' (ngModelChange)='onDataChange( $event )'
      placeholder='Address 1...' [disabled]='disabled' />
  </div>
  <div class='col-lg-1 col-md-1 col-sm-12 nsg-alert-color' *ngIf='Address1.invalid && Address1.touched && validation'>Required</div>
</div>
<div class='row form-group'>
  <div class='col-lg-3 col-md-4 col-sm-12 nsg-primary-color nsg-text-right'><label for='Address2{{uniquePostfix}}'>&nbsp;</label></div>
  <div class='col-lg-8 col-md-7 col-sm-12'>
    <input type='text' id='Address2{{uniquePostfix}}' name='Address2{{uniquePostfix}}' required maxlength='50' class='nsg-input-wide'
      #Address2='ngModel' [(ngModel)]='model.Address2' (ngModelChange)='onDataChange( $event )'
      placeholder='Address 2...' [disabled]='disabled' />
  </div>
</div>
<div class='row form-group' [class.has-error]='(City.invalid && City.touched && validation) || (State.invalid && State.touched && validation) || (Zip.invalid && Zip.touched && validation) || (Country.invalid && Country.touched && validation)'>
  <div class='col-lg-3 col-md-4 col-sm-12 nsg-primary-color nsg-text-right'><label for='City{{uniquePostfix}}'>{{cityStateLabel}}</label></div>
  <div class='col-lg-auto col-md-auto col-sm-12'>
    <input type='text' id='City{{uniquePostfix}}' name='City{{uniquePostfix}}' required maxlength='50' size='20'
      #City='ngModel' [(ngModel)]='model.City' (ngModelChange)='onDataChange( $event )'
      placeholder='City...' [disabled]='disabled' />
    &nbsp;
    <input type='text' id='State{{uniquePostfix}}' name='State{{uniquePostfix}}' required maxlength='3' size='2'
      #State='ngModel' [(ngModel)]='model.State' (ngModelChange)='onDataChange( $event )'
      placeholder='State...' [disabled]='disabled' />
    &nbsp;
    <input type='text' id='Zip{{uniquePostfix}}' name='Zip{{uniquePostfix}}' required maxlength='12' size='11'
      #Zip='ngModel' [(ngModel)]='model.Zip' (ngModelChange)='onDataChange( $event )'
      placeholder='Zip...' [disabled]='disabled' />
    &nbsp;
    <input type='text' id='Country{{uniquePostfix}}' name='Country{{uniquePostfix}}' required maxlength='3' size='2'
      #Country='ngModel' [(ngModel)]='model.Country' (ngModelChange)='onDataChange( $event )'
      placeholder='Country...' [disabled]='disabled' />
  </div>
  <div class='col-lg-2 col-md-3 col-sm-12 nsg-alert-color' *ngIf='City.invalid || State.invalid || Zip.invalid || Country.invalid'>
    <div *ngIf='City.invalid && City.touched && validation'>City Req.</div>
    <div *ngIf='State.invalid && State.touched && validation'>State Req.</div>
    <div *ngIf='Zip.invalid && Zip.touched && validation'>Zip Req.</div>
    <div *ngIf='Country.invalid && Country.touched && validation'>Ctry Req.</div>
  </div>
</div>
`
})
export class AddressComponent implements OnInit {
  @Input() disabled: boolean = true;
  @Input() addressLabel: string = 'Address:';
  @Input() cityStateLabel: string = 'City/State/Zip:';
  @Input() uniquePostfix: string = '';
  @Input() validation: boolean = true;
  model: IAddress;
  @Input() set address( address: IAddress ) {
    this.model = address;
  }
  get address(): IAddress { return this.model; }
  @Output() onAddressChanged = new EventEmitter<boolean>();
  //
  constructor() {
    // static constructor method
    this.model = Address.empty( );
  }
  /*
  ** Component initialization, place all long running
  ** initializations here.
  */
  ngOnInit(): void {
  }
  /*
  ** Events
  ** * flag that the form is dirty.
  */
  onDataChange( event: any ): boolean {
    this.onAddressChanged.emit( true );
    return false;
  }
  //
}
// ===========================================================================

The uniquePostfix Input parameter is essential for DOM compatibility. Here is how you can use it:

<app-address
  [disabled]='!editable'
  [uniquePostfix]='companyUniquePostfix'
  [address]='model.CompanyAddress'
  (onAddressChanged)='onFormChanged( $event )'>
</app-address>
<app-address
  [disabled]='!editable'
  [addressLabel]='remitAddressLabel'
  [uniquePostfix]='remitUniquePostfix'
  [validation]='remitValidation'
  [address]='model.RemitAddress'
  (onAddressChanged)='onFormChanged( $event )'>
</app-address>

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

Issues with the functionality of Angular Firebase Authentication Service

I am currently working on setting up an authentication service in Angular that will integrate with Google Firebase for a Login form. However, I have encountered an issue where including the service in the constructor of my LoginComponent prevents me from a ...

What is the best way to send an enum from a parent component to a child component in

I'm trying to send an enum from a parent component to a child component. This is the enum in question: export enum Status { A = 'A', B = 'B', C = 'C' } Here's the child component code snippet: @Component({ ...

Angular material table

I'm having an issue with deleting a record from a table that I created using Angular Material- Even after successfully deleting a record, the view does not refresh. Here is the HTML code snippet - <ng-container matColumnDef="delete"> ...

Why is it that a static variable cannot be accessed using the `this` keyword in a static method when the static method is called in any route's controller in NODEJS?

Is it possible to access static variables in a static method using the 'this' keyword? The answer is yes, but there seems to be an issue when passing that static method in any route. The 'this' keyword refers to the class, yet its value ...

When attempting to send a fetch request in the most recent rendition of NextJS, it ends up with an error of 'failed fetch'

I am currently working on a nextjs (v.13.4.19) / strapi (v.4.12.5) application and facing issues when trying to make a request to the strapi endpoint using the fetch function. I have attempted several troubleshooting steps such as: changing localhost:1337 ...

The Gatsby + Typescript project is reporting that the module with the name "*.module.scss" does not have any exported members

I've recently gone through Gatsby's demo project in their documentation (which is long overdue for an update). I've carefully followed the instructions provided here: I've included an index.d.ts file in the /src directory of my project ...

Unlocking the Secrets of Passing ID Parameters Between Pages and Fetching Data from External APIs in Ionic 4

Before I get into it, apologies for the basic question, but I'm struggling to figure this out. Here's my issue: I have a list of categories that are being fetched from a nodeJS api. My goal is to fetch the subcategories based on the id from the d ...

The positioning of the legend in ngx-charts seems to be off

Greetings to all developers, I have been utilizing ngx-charts for my charting needs, and overall, everything has been working well. However, I am encountering an issue when attempting to change the position of the legend to below the chart. It seems to ex ...

What is the best way to display lengthy content using a pair of HTML tags?

I am facing an issue with my Angular4 app where I have a <md-card-content> tag on part of my page. Inside this tag, I am trying to display some content within a <p> tag which has a maximum height of 20cm. While this setup works fine for short ...

What is the most effective way to use a withLatestFrom within an effect when integrating a selector with props (MemoizedSelectorWithProps) sourced from the action?

I am struggling to utilize a selector with props (of type MemoizedSelectorWithProps) in an effect inside WithLatestFrom. The issue arises because the parameter for the selector (the props) is derived from the action payload, making it difficult for withLat ...

Substitute this.bindMethod for function component

I have a class component that is structured like this: interface MyProps { addingCoord: any resetCoords: any } interface MyState { x: any y: any } class DrawerOld extends React.Component<MyProps, MyState> { width: number height: number ...

Unusual Interactions between Angular and X3D Technologies

There is an unusual behavior in the x3d element inserted into an Angular (version 4) component that I have observed. The structure of my Angular project is as follows: x3d_and_angular/ app/ home/ home.component.css hom ...

Angular 2 variable reference

Within my appComponent, I have the line this.loggedIn = this.authenticationService.isLogged;. This means that appComponent is utilizing authenticationService to retrieve the isLogged data. I assume that this.loggedIn is referencing the data from the servi ...

TS2604: The JSX element '...' lacks any construct or call signatures and is unable to be processed

As part of our company's initiative to streamline development, I am working on creating a package that includes common components used across all projects. We primarily work with TypeScript, and I have successfully moved the code to a new project that ...

When using a Redux action type with an optional payload property, TypeScript may raise complaints within the reducer

In my react-ts project, I define the following redux action type: type DataItem = { id: string country: string population: number } type DataAction = { type: string, payload?: DataItem } I included an optional payload property because there are tim ...

Closing iframe in Angular 4 after redirection

I am currently working on an Angular 4 application that involves integrating a third-party payment system called Tranzila. Tranzila offers a convenient integration method using an iframe, allowing users to make payments on their domain without me having to ...

Developing Angular2 applications in Visual Studio Team Services (formerly known as Visual Studio Online)

Currently, I have an angular2 client integrated into a Visual Studio vNext (ASP.Net 5) project. During my attempt to create a build in Visual Studio Team Services, I encountered errors similar to this one during the build step: It appears that module &a ...

Ways to showcase product information (Using Angular and Firebase)

Information product.model.ts id?: string; name: string; price: number; sale_price: number; description: string; tech_sheet: string; imageUrls: string[]; category: string; createdAt: Date; } Initialize file product.service.ts The latest f ...

Problem arises when combining string manipulation and piping operations

I have an HTML code within an Angular 2.0 template that looks like this: <span> Total Employee {{employeeCount> 0 ? '(' + employeeCount+ ')' : ''}} ></span> My customFormatter function is able to take a val ...

Retrieve an item from the Ionic Firebase database

My current challenge is retrieving data from the Firebase database. user-service.ts getProfile(){ try {; return this.afDatabse.object(`profile/${this.afAuth.auth.currentUser.uid}`); } catch (e) { console.log(e); } } c ...