Reuse and extend Angular component logic and template in child component - javascript

I have a form that is used to choose customer details. It is used in more than one places in my app but with extended logic and UI.
I would like to be able to reuse my basic component in other components but also to extend(or override) it's template and logic. To don't have to copy paste the same UI & logic in each component and have one common basic place for changes.
base component with template & logic:
#Component({
selector: 'app-base',
template: `<h1> Base component </h1>
<label>getLabel() </label>
<ng-content> </ng-content> `
})
export class BaseComponent implements OnInit {
constructor() {
// base init
}
ngOnInit(): void {
// base ngOnInit
}
getLabel(): string {
return "label";
}
}
Parent component that is suppose to reuse template, being able to extend methods and lifehooks and extend the UI as well.
#Component({
selector: 'app-parent',
template: `<app-base> <h1> extended </h1> </app-base>`,
})
export class ParentComponent extends BaseComponent {
constructor() {
super();
}
ngOnInit(): void {
super.ngOnInit();
// extended fancy stuff
}
getLabel(): string {
let a = super.getLabel();
return a + "a"; // extended logic
}
}
It sort of works using this approach but one problem that I noticed is that the ngOnInit is called twice. Once from the base component since I'm using its template in parent component and second time because of parentComponent ngOnInit being triggered.
Question:
Is there a way to reuse the template like using <app-base></app-base> with all shared logic in one place but without actually executing the lifehooks twice from both components?
Solution(?):
The only thing that I came up is to wrap the basic component logic with a service. So there will be something like this:
#Component({
selector: 'app-base',
template: `<h1> Base component </h1>
<label>getLabel() </label>
<ng-content> </ng-content> `})
export class BaseComponent implements OnInit {
constructor(IBasicService basicService) { // <-- injected service
// base init
}
ngOnInit(): void {
this.basicService.onInit(); // <-- using ngOnInit from service
// base ngOnInit
}
getLabel(): string {
return this.basicService.getLabel(); <-- using getLabel from service
}
}
With this approach I'll be able to simply reuse my base component but with different injected services that will implement IBasicService interface and also simply extend the UI using or but is this correct way? Isn't there any better solution?
Thanks in advance!

Related

Angular service call a child method in constructor

Is there a way to call a child class method from an abstract service? When I do this, the if statement doesn't execute because onInit doesn't exist. I am not sure why it doesn't exist though. Maybe there is an "angular" way of doing this instead of triggering this on the init. Or maybe I just need to call the onInit manually in the component instead. Basically what I am doing is trying to get the initial application data from the server.
#Injectable({providedIn:'root'})
export class RootService {
public constructor(httpClient: HttpClient) {
if(typeof this.onInit === 'function') {
this.onInit()
}
}
}
#Injectable({providedIn:'root'})
export class MyService extends RootService {
public onInit() {
// Do stuff
}
}
#Component()
export MyComponent {
public constructor(myService: MyService) {}
}
Services do not have lifecycle events. However, components have lifecycle hooks such as:
ngOnChanges()
ngOnInit()
ngDoCheck()
...
So you can load data when your component is initialized. If yes, then just use ngOnInit:
import { Component, OnInit } from '#angular/core';
#Component()
export MyComponent implements OnInit {
yourData;
public constructor(myService: MyService) {}
ngOnInit(){
myService.loadData()
.subscribe(s=> yourData = s);
}
}
According to Angular docs:
OnInit is a lifecycle hook that is called after Angular has
initialized all data-bound properties of a directive.
If you add the service to the providers of your component, it will be in your components scope then you can call onInit in your service as well.
But the downside of this is you can no longer share the same instance of the service among your components.
This will be valid if your service only serves one component.

How can I reproduce the Angular JS .broadcast() / .on() behaviour in Angular?

I'm working on rewriting an AngularJS application in Angular8. I have read of the different ways to communicate between components but can't seem to find the proper way to achieve this given my current requirements.
I currently have 2 sibling components which both use a common service that handles basic crud functionality. The form component calls a create method in the service and the list component calls a fetch method.
<page-component>
<form component></form component>
<list-component></list-component>
</page-component>
In AngularJS this would have been achieved by using a $scope.broadcast() and $scope.on() which is the effect that I'm looking to reproduce.
What I'm trying to figure out is the best way for the form component to emit an event to which the list component would (i assume) subscribe in order to tell it to refresh itself.
To be clear, I don't want to pass the updated values to the list component. I would like to simply communicate to it that its dataset has been updated and that it needs to re-fetch its records.
I have tried using an #Output in my form component:
export class FormComponent implements OnInit {
#Output() valueChange = new EventEmitter();
onUpdate() {
this.valueChange.emit(true);
}
...
}
But I'm confused on how to implement the event listener as all the documentation I have read seems to point to events passed between parents and children and not to siblings as it were. Also the examples I have seen all seem to be focused on passing the actual data to the component instead of simply having it watch for an event and do the work on its own, as I require.
I have also seen methods that use #Input which require parameters to be passed into them on declaration. Somehow I feel that these components should be able to work on their own and not depend on this.
I believe there are multiple ways to achieve this.
In case of unrelated components, like the sibling components, one method would be to use a Subject Observable of RxJS module in the common service to which Form component will push a new value that can be subscribed to by the List component.
Service:
import { Subject } from 'rxjs/Subject';
#Injectable()
export class SampleService {
public triggerSource = new Subject<boolean>();
public trigger$ = this.triggerSource.asObservable();
}
Form Component:
import { SampleService } from '../services/sample.service';
#Component({
selector: 'form-component',
templateUrl: './form.component.html',
styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {
constructor(private _sampleService: SampleService) {
}
private setTriggerState(state: boolean) {
this._sampleService.triggerSource.next(state);
}
}
List Component:
import { SampleService } from '../services/sample.service';
#Component({
selector: 'list-component',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css'],
})
export class ListComponent implements OnInit {
constructor(private _sampleService: SampleService) {
this._sampleService.trigger$.subscribe(value => {
if (value) {
// value is true (refresh?)
} else {
// value is false (don't refresh?)
}
});
}
}
And since we are returning an Observable, there are numerous operators to refine the outflow of data. For eg., in your case operator distinctUntilChanged() can be used to avoid pushing redundant information. Then the declaration of the observable would be
public trigger$ = this.triggerSource.asObservable().distinctUntilChanged();
Answer to this question could be a topic for a nice holywar, you know )
My opinion based on experience is in following:
Let's assume user typing something and than performs refresh page. Ask ourself what should happen?
If User's changes should recover to "before editing" state - than the parent component is enough to share such events.
If User's changes should be as it was before refresh page action, so if you need to store it localStorage or database or elsewhere, than it's better to involve services for it.
Create a #Input property in your ListComponent and pass all the items that list component will render from PageComponent. Whenever create component emit a value fetch the new list in PageComponent and update passed property.
Implement the OnChange lifecycle hook in ListComponent and capture the property change in ngOnChange(changes) method.
If you don't like to pass the list from PageComponent then there are few options that can be used to solve the issue.
Option 1:
instead of passing a list:Array<any> pass a list:Observable<Array<any>> and subscribe the observable in your ListComponent so technically you http call will happen inside list component. But you have to reassign the list with new Observable<Array<any>> everytime FormComponent emit a value so ngOnChange will notified.
Option 2 :
You can pass a Subject to ListComponent as a propery and Subscribe to subject in ListComponent. Then you can fetch the product in ListComponent. When ever your FormComponent emit value call next() method in subject you passed. Every time you call next method your subscribles will notify and you can fetch the details from API.
reference links
https://angular.io/api/core/OnChanges
https://angular.io/api/core/Input
#Component({
//Component Metadata
})
export class ListComponent implement OnChange{
#Input() listOfAny:Array<any>=[];
ngOnChanges(changes: SimpleChanges) {
this.listOfAny =changes.listOfAny.currentValue
}
}
#Component({
//Component Metadata
})
export class FormComponent {
#Output() valueChange = new EventEmitter();
onUpdate() {
this.valueChange.emit(true);
}
}
#Component({
//Component Metadata
template:`
<page-component>
<form component (valueChange)="valueChanges($event)" ></form component>
<list-component [listOfAny]="list"></list-component>
</page-component>`
})
export class PageComponent {
list=[]
valueChanges(newValue){
// you can fetch items from API if needed.
this.list.push(newValue)
}
}
Have a look at the Rxjs Subjects. Subjects makes the inter-components communication easier.
Since Forms and List component use a common CRUD service in your case, you can have a subject (say, newRecordSubject) and push any new records created into that subject. Now the List component should subscribe to this subject to be notified of any new records whenever it is created by the Forms component.
Have a look at the sample CRUD service below. I beleieve everything is very much self explainatory from the code snippets below.
crud.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
import { Record } from './record.model';
#Injectable({providedIn: 'root'})
export class CrudService {
private newRecordSubject = new Subject<Record>();
get newRecordListener() {
return this.newRecordSubject.asObservable();
}
public insertRecord(record: Record): void {
// logic to pass the record to the backend
this.newRecordSubject.next(record);
}
public fetch(): Record[] {
// logic to fetch the inital records from the backend
return [];
}
}
Now in your List component, inject this CRUD service and subscribe to the newRecordsSubject as below.
list.component.ts
import { OnInit, Component, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs';
import { CrudService } from './crud.service';
import { Record } from './record.model';
#Component({
selector: 'list-component',
template: ''
})
export class ListComponent implements OnInit, OnDestroy {
records: Record[];
private sub: Subscription;
constructor(private service: CrudService) {}
ngOnInit() {
this.records = this.service.fetch();
this.sub = this.service.newRecordListener.subscribe(record => this.records.push(record));
}
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe();
}
}
}
Note:
I'm converting Subject to Observable so that the subject is available as read-only to methods outside Crud Service.
It is always a best practice to unsubscribe to the Subscriptions of Subjects and Observables, to avoid memory leaks.
Hope this helps! Cheers and Happy Coding.
You can create a function in parent that sets the value in the child component that you need.
In the component
<page-component>
<form component (value change)="formValueFunction($event)" emitedformValue="formEmitiedValue"></form component>
<list-component (list change)="listValueFunction($event)" emitedListValue="listEmitedValue"></list-component>
</page-component>
In the component you can set the value to the other components like this
formEmitiedValue:any;
listEmitedValue:any;
formValueFunction(event){
this.formEmitiedValue= event;
}
listValueFunction(event)
{this.listEmitedValue=event;
}
check the event value using logs and if there are changes in form input use OnChange event hook in the components
When I was migrating my app from AngularJS to Angular 2+. I choose to use RxJS, super easy to use, but there are learning curve.
You create service and RxJS BehaviorSubject. Any component can now subscribe for changes if needed, and any component can send updated data using .next() method.
This is your service:
import {Injectable} from "#angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
#Injectable()
export default class yourService {
valueChange = new BehaviorSubject<object>({});
//add service functions if needed
...
}
This is your component:
import { Component, OnInit, OnDestroy } from "#angular/core";
import YourService from "../path to your service";
#Component({
selector: "name of component",
template: require("your.html")
})
export class nameOfComponent implements OnInit, OnDestroy {
private sub;
privat val = "Your value";
constructor( private YourService: YourService) {
}
ngOnInit() {
this.sub = this.YourService.valueChange.subscribe( (value) => {
this.YourService.valueChange.next(this.val); // If you need to pass some value to the service.
this.val = value;
})
}
ngOnDestroy() {
this.sub.unsubscribe();
}

Cannot invoke an expression whose type lacks a call signature. Type 'EventEmitter<Number>' has no compatible call signatures

I am trying to write a service in which some events are triggered via respective methods in it so that any component in my application can listen to it.
So, I created a custom event in a service as follows
export class EventsService {
#Output() expandNav: EventEmitter<Number> = new EventEmitter();
trigExpandNav() {
this.expandNav.emit(1);
}
constructor() { }
}
Then I included this service as a provider in my component in which I wanted to emit this event via the trigExpandNav method as follows :
import {
Component,
OnInit,
Output,
EventEmitter
} from '#angular/core';
import {
EventsService
} from '../../services/events.service';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css'],
providers: [EventsService]
})
export class HeaderComponent implements OnInit {
eventsService: EventsService;
fun() {
this.eventsService.trigExpandNav();
}
constructor() {}
ngOnInit() {}
}
But I am getting this error
ERROR in src/app/layout/header/header.component.ts(21,7): error TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'EventEmitter<Number>' has no compatible call signatures.
I don't know where I am doing it wrong, so please help and Thanks in advance.
IMPORTANT
What I am trying to achieve is :
Suppose I have 2 components. I want to trigger an event from 1st component when a button or link is clicked and listen that event in 2nd component from the html file like this sda
For that I made a service in which I am emitting the event through a method. The thing is it's working as expected.
You shouldn't be using event emitters for Service's this is a better place for a RxJS BehaviorSubject or Subject. depending on your need.
#Ouput()s and EventEmitters are for Components only
So your service should become something along the lines of:
export class EventsService {
private expandNavSubject = new BehaviorSubject<number>(null);
trigExpandNav() {
this.expandNavSubject.next(1);
}
get expandNav$(){
this.expandNavSubject.asObservable();
}
constructor() { }
}
}
then in your header component inject the service via the constructor with:
constructor(private eventsService: EventsService){}
and call the trigger expand nav function with this.eventsService.trigExpandNav(); in your fun() function

angular 4. remove a component in the constructor based on an if

I want to add protection to components.
If a user doesn't have permission to see this component it wont be rendered.
I have tried putting my if in the constructor and return false but it still renders.
I also added the if to the template itself but then I didn't see the view but the component is still alive and it adds complexity to the code as I need to maintain several places of the same if.
Is there a way to tell the component to not render at all ?
constructor( private userService: UserService) {
if (this.userService.isAllowed("see_trade_groups") === false) {
return;
}
}
For that purpose you can see CanActivate. Put it on the component route and it will do the job you want.
In that you can write a logic, based on which the route will be navigated or not.
Component compilation lifecycle is handled by Angular compiler, so a component is unable to control its own lifecycle, and it should be controlled from the outside.
A common way to handle this is to use router. The lifecycle in route components differs from regular components because it's handled by router; they are attached to <router-outlet> component. It's possible to prevent compilation in route components but not in regular components.
Otherwise this should be handled with a directive. ngIf is built-in way to prevent the compilation of regular components.
So it becomes
<foo *ngIf="userService.isAllowed('see_trade_groups')"></foo>
Since this requires to inject userService to parent component every time it's needed, this will result in a lot of boilerplate code. An appropriate solution is to create a directive that behaves similarly to ngIf - or extend it to provide desired functionality:
import {Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '#angular/core';
import {NgIf, NgIfContext} from '#angular/common';
...
#Directive({
selector: '[role]'
})
class Role extends NgIf {
#Input() role: string;
constructor(
viewContainer: ViewContainerRef,
templateRef: TemplateRef<NgIfContext>
public userService: User
) {
super(viewContainer, templateRef);
}
ngOnChanges({ role }: SimpleChanges) {
this.ngIf = this.userService.isAllowed(role);
// can also subscribe to some observable to add/remove a component any time
}
}
Which is used like:
<foo *role="'see_trade_groups'"></foo>
Notice that Role is * structural directive. This allows it to control the compilation of an element it was specified on, similarly to how ngIf and ngFor do.

Angular 2 - testing a component with #input used in ngOnInit lifecycle hook

Currently I am trying to test a child component which is accepting an input from the host component, and used within the ngOnInit life cycle hook like the code below.
#Component({
selector: 'my-child-component',
template: '<div></div>'
})
class ChildComponent implements OnInit {
#Input() myValue: MyObject;
transformedValue: SomeOtherObject;
ngOnInit():void {
// Do some data transform requiring myValue
transformedValue = ...;
}
}
#Component({
template:`<my-child-component [myValue]="someValue"></my-child-component>`
})
class HostComponent {
someValue: MyObject = new MyObject(); // how it is initialized it is not important.
}
How should the ChildComponent be tested in this case where myValue needs the to be present upon creation while being able to have access to ChildComponent.transformedValue for assertion.
I tried creating the ChildComponent using the Angular TestBed class like this
componentFixture = testBed.createComponent(LoginFormComponent)
however the ngOnInit would have already been called up to the point where I call
fixture.componentInstance.myValue = someValue;
I also tried creating a fixture of the HostComponent, and while that works, I got stuck at getting access to the ChildComponent instance that was created, which i require to perform assertions on the ChildComponent.transformedValue field.
Help is greatly appreciated!
Thanks a lot!
Angular offers the ability to inject children components to their parent components using the #ViewChild() decorator. See https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
By updating the TestHostcomponent (that is written within the .spec.ts file) to the following
#Component({
template:`<my-child-component [myValue]="someValue"></my-child-component>`
})
class TestHostComponent {
#ViewChild(MyChildComponent)
childComponent: MyChildComponent;
}
it exposes its child component instance ( and its variables ), making the assertion of the 'transformedValue' possible, as per below.
componentFixture = testBed.createComponent(TestHostComponent)
expect(componentFixture.componentInstance.childComponent.transformedValue).toEqual(...someValue);

Categories