Is there a more efficient approach to implementing Angular component inheritance while also inheriting the template?

I am currently working on an Angular project that involves multiple forms. While these forms share a similar structure and functionality, there are some distinct differences between them in terms of both functionality and template design.

I am looking for a more efficient way to handle component inheritance, where I can define a base component with its functionality and template, but also have the flexibility to customize certain elements within the template in child components.

After some research, I came across a method involving extending the base component and dynamically modifying the parent element within the child component's template. I have provided a code example on SandBox (https://codesandbox.io/s/github/dsavke/inheratance-test) as a demonstration of this concept.

Here is the code snippet for the BaseFormComponent:

@Component({
    selector: 'base-form',
    templateUrl: './base-form.component.html'
})
export class BaseFormComponent {
    private counter: number = 0;

    @ContentChild('content') content!: TemplateRef<any>;

    public editUrl: string = '';

    public onEdit(): void {
        //some logic
    }

    public onSave(): string {
        if (!this.canSave())
            return 'Not saved';

        return 'Saved';
    }

    public canSave(): boolean {
        return true || true;
    }

    private updateCounter(): void {
        this.counter++;
    }
}

As well as the template code for the BaseFormComponent:

<h2>Base component</h2>
<p>Can save form? {{canSave()}}</p>

<ng-container
    style="border: 1px solid red;"
    *ngIf="content"
    [ngTemplateOutlet]="content"
></ng-container>

For the UserFormComponent:

@Component({
    selector: 'user-form',
    templateUrl: './user-form.component.html'
})
export class UserFormComponent extends BaseFormComponent {
    public override canSave(): boolean {
        return false;
    }
}

UserFormComponent template code:

<base-form>

    <ng-template #content>

        <h5>This is from USER FORM!</h5>

    </ng-template>

</base-form>

While this method of component inheritance works in Angular, it does come with some challenges:

  1. Duplicating instances of the BaseFormComponent occurs, one for the extension in UserFormComponent and another one for displaying the UI by adding the BaseFormComponent element in the template;
  2. Overriding methods in UserFormComponent may appear futile as the instance of the parent component (the extended component) remains hidden due to decorators. There is a need for two templates - one for the base component display and one for customizations in the child component using ng-template;

Is there an alternative approach to achieve inheritance, where both the template and functionality from the parent component are retained while allowing customization in the child component? This would represent true inheritance - is such a scenario feasible in Angular?

EDIT: The form in question is utilized in over 1000 locations, making it a significant part of the project. It consists of numerous child components and allows for UI customization through parameters. While the form inputs require manual adjustments, generic features are consistently used. The challenge lies in efficiently managing template modifications within this complex structure.

Answer №1

While this may not be an exact match for what you're seeking, it could point you in the right direction.

Explained

I once tackled a similar challenge with a shared-form-wizard component. My approach involved creating a shared-wizard.component that standardized a form dialog while still allowing me to customize the tabs displayed within that Shared Wizard. This setup enabled me to reuse the base component across different Wizards in my application.

In my scenario, I opted against using traditional inheritance by segregating my logic to avoid potential complications, but incorporating it shouldn't pose significant challenges.

Illustrative Code Snippets

To enable custom content projection, generate a Custom ContentProjectorTemplateRef directive and pipe. These constructs facilitate projecting customized content. In my case, I utilized them to iterate tab content, hence inclusion of index in the pipe (you can omit if unnecessary).

@Directive({
  selector: "[contentProjectorTemplateRef]",
  standalone: true,
})
export class ContentProjectorTemplateRefDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

...

@Pipe({ name: "contentProjectorTemplateRef", standalone: true })
export class ContentProjectorTemplateRefPipe implements PipeTransform {
  public transform(content: QueryList<ContentProjectorTemplateRefDirective>, index: number): TemplateRef<unknown> {
    return content.get(index)?.templateRef;
  }
}

In your core component, define a query list for projected content.

@ContentChildren(ContentProjectorTemplateRefDirective) content!: QueryList<ContentProjectorTemplateRefDirective>;

Add ngTemplateOutlet to the core-component interface:

<!-- Project the templateRef into the templateOutlet -->
<ng-container *ngTemplateOutlet="content | contentProjectorTemplateRef : i">
</ng-container>

Enclose these elements in appropriate tags, then transmit to the core-component alongside any necessary configuration inputs. The final structure should resemble something like this:

base.component.html

<form #sharedForm="ngForm">
  <h1>Shared Form</h1>
  ... // Other necessary components
</form>

base.component.ts

export class SharedWizardComponent implements AfterViewInit, OnDestroy {
  @ContentChildren(ContentProjectorTemplateRefDirective)
  content!: QueryList<ContentProjectorTemplateRefDirective>;

  @ViewChild("sharedForm", { static: false }) public sharedForm: NgForm;
  @Input() form: UntypedFormGroup;
...
  public ngAfterViewInit(): void {
          this.sharedForm = this.form as unknown as NgForm;
    }
  }

For the child component:

child-form-1.component.html

<form #childForm="ngForm">
  <shared-wizard>
    <ng-template contentProjectorTemplateRef> ...customContent1 </ng-template>
    <ng-template contentProjectorTemplateRef> ...customContent2 </ng-template>
    <ng-template contentProjectorTemplateRef> ...customContent3 </ng-template>
  </shared-wizard>
</form>

This setup puts you on the right track. Further adjustments related to forms are inevitable, but it empowers cleaner separation of logic without mandate for classical inheritance, offering the flexibility for child components to drive parent component changes.

Answer №2

Honestly, I'm not sure how many aspects all of your forms share, so it's uncertain if this response will be helpful. However, I recommend considering a different approach: content projection.

Picture having a BaseFormComponent

<fieldset class="container">
  <legend>{{title}}</legend>
  <ng-content></ng-content>
  <div>
    <button (click)="submit(form)">submit</button>
    <button type="button" (click)="cancel()">cancel</button>
  </div>
  <div>{{result$|async}}</div>
</fieldset>

You can enhance your baseForm component with additional code

 @Input('formGroup') form: FormGroup;
 @Input('title') title: string = '';
 @Input('dataService') dataService:any=null

 result$:any
 constructor(private router: Router, private activatedRoute: ActivatedRoute) {}
 cancel() {
   if (this.form.touched && window.confirm('Do you really want to leave?')) {
     this.router.navigate(['../'], { relativeTo: this.activatedRoute });
     this.form.reset();
   }
 }
 submit(form) {
   if (form.valid) {
     this.result$=this.dataService.save(form.value).pipe(
                     map((res:any)=>res.success?'Data save':'error'))
   } else form.markAllAsTouched();
 }

Then, your form components could look something like this

<base-form [formGroup]="form" title='Users' [dataService]="userService" >
 <label>User name :</label><input formControlName="userName">
 <label>Password :</label><input formControlName="password">
</base-form>

In this way, you can utilize the BaseComponent to override functions when necessary or assign values to variables that are not inputs

 @ViewChild(BaseFormComponent,{static:true}) base:BaseFormComponent
 constructor(public userService:UserService) {
 }
 form: FormGroup = new FormGroup({
   userName: new FormControl(''),
   password: new FormControl(''),
 });
 ngOnInit()
 {
   this.base.title='users'
   this.base.actionButton="create user"
 }

Check out this StackBlitz demo

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

What is the method to utilize attributes from one schema/class in a separate schema?

Consider the scenario presented below: Base.ts import Realm from 'realm'; export type BaseId = Realm.BSON.ObjectId; export interface BaseEntity { id?: BaseId; createdAt: string; updatedAt: string; syncDetails: SyncDetails; } e ...

Ways to utilize the ::after pseudo class with ElementRef within Angular

Is it possible to manipulate the background-color of the ::after pseudo selector for the specified element using Angular? @ViewChild('infobubble', { read: ElementRef }) infoBubbleElement: ElementRef; ngAfterViewInit(): void { const el ...

Stop extra properties from being added to the return type of a callback function in TypeScript

Imagine having an interface called Foo and a function named bar that accepts a callback returning a Foo. interface Foo { foo: string; } function bar(callback: () => Foo): Foo { return callback(); } Upon calling this function, if additional pr ...

Make sure to clear the Auxiliary Route prior to switching to a new route. Exclusively for @angular-4

Currently, I am using an auxiliary route for popup and toast notifications that automatically clears itself after a few seconds upon successful navigation. During this time, the router.url is set to /home(aux:toast). If I try to navigate to another route d ...

Firebase and Angular 7 encountered a CORS policy block while trying to access an image from the origin

I am attempting to convert images that are stored in Firebase storage into base64 strings in order to use them in offline mode with my Angular 7/Ionic 4 application! (I am using AngularFire2) However, I encountered an error message stating: Access to i ...

Preserve Inference in Typescript Generics When Typing Objects

When utilizing a generic type with default arguments, an issue arises where the inference benefit is lost if the variable is declared with the generic type. Consider the following types: type Attributes = Record<string, any>; type Model<TAttribu ...

Dealing with Errors in Angular's HttpClient

I've been facing some challenges when it comes to running code after an error occurs in HttpClient. I need help with setting a flag that will stop a loading spinner when the HTTP call fails. My project uses RxJs 5.5.2 and Angular 5. private fetch() ...

Angular isn't generating a new project

I am having some trouble creating an Angular project successfully. F:\Demos>npm init F:\Demos>npm install @angular/cli The above two commands went smoothly. However, when I tried to execute the following command: F:\Demos>ng new m ...

Leverage push notifications and utilize multiple service workers with an Angular Ionic 5 progressive web app, designed for optimal performance on Web, Android

Currently facing an issue with my Ionic/Angular Capacitor PWA. In order to enable PUSH NOTIFICATIONS for Web, Android, and IOS platforms, I have focused on setting it up for Web first. The Angular app prompts users for permission to receive notifications ...

Using ts-jest for mocking internal modules

Within my .ts module, I have the following code: import client from './client'; export default class DefaultRequest implements IRequest { make(req: Request): Promise<Response> { return new Promise<Response>((resolve, reje ...

The navigation bar changes its functionality depending on the page

My Angular application features a main app component that includes a navbar linking to other components using the routerLink directive. The structure is simple: <nav> <button [routerLink]="['/foo']> Foo </button> ...

Exploring Angular 5: Utilizing ComponentFactoryResolver for Dynamic Component Properties

Trying to use ComponentFactoryResolver to render a component view and insert it into the DOM, but encountering issues with undefined properties upon rendering. What steps can be taken to resolve this? Specifically, the property in the component is defined ...

Access the API URL from a JSON file stored locally within the app.module file

I'm currently working on an Angular project that will be deployed in various addresses, each with a different API link. Instead of manually changing and configuring the apiUrl provider in app.module for every address, I want to store these links local ...

What is the best way to identify and process individual JSON objects separated by new lines in oboe's node-event within an Angular2 application?

I am working with an API that retrieves data in the following format: {"t":"point","id":817315,"tableid":141,"classid":142,"state":0,"loc":[6850735.34375,24501674.0039063]} {"t":"line","id":817314,"tableid":204,"classid":2102,"loc":[[6850335.8828125,24501 ...

Creating dynamic keys to insert objects

In my coding project, I am dealing with an empty array, a value, and an object. Since there are multiple objects involved, I want to organize them into categories. Here is an example of what I envision: ARRAY KEY OBJECT OBJECT KEY OBJECT ...

I'm looking to inject both default static values and dynamic values into React's useForm hook. But I'm running into a TypeScript type error

Below is the useForm code I am using: const { register, handleSubmit, formState: { errors, isSubmitting }, reset, getValues, } = useForm({ defaultValues: { celebUid, //props fanUid, // props price, // props ...

Tips for accurately inputting a Record key in typescript

Within my code, I have a function that filters the properties of an object based on a specified filtering function: function filterProps<K extends string, V>(object: Record<K, V>, fn: (key:K, value: V, object: Record<K, V>) => unknown) ...

How can I assign a type to an array object by utilizing both the 'Pick' and '&' keywords simultaneously in TypeScript?

As a beginner in TypeScript, I am looking to declare a type for an array object similar to the example below. const temp = [{ id: 0, // number follower_id: 0, // number followee_id: 0, // number created_at: '', // string delete_at: &ap ...

Ways to merge values across multiple arrays

There is a method to retrieve all unique properties from an array, demonstrated by the following code: var people = [{ "name": "John", "age": 30 }, { "name": "Anna", "job": true }, { "name": "Peter", "age": 35 }]; var result = []; people. ...

Cannot locate module in Jest configuration

I'm struggling to understand config files and encountering issues while attempting to run jest unit tests: Cannot locate module '@/app/utils/regex' from 'src/_components/DriverSearchForm.tsx' Here's my jest configuration: ...