Angular cross component communication

Tomáš Trojčák
5 min readJan 12, 2020

Typical use cases, with which has to developer deal with, when using nested components and nested controls:

  • how to communicate between two or more components? Typically child component/control on some level needs to expose that it is not valid and other components in the sub tree need to react to this state.
  • how to communicate between descendants or parent and child? Typically if we have a table, with row component and rows needs to share data each other.
    Solution for parent <-> child component communication is to use @Input @Output, or get the reference of child via @Viewchild.
    Solution for control <-> parent communication is to implement Control Value Accessor interface and optionaly Validator interface to expose validation status to parent. Example

Note: this article is about communication between objects, which are in the same sub tree on different levels. For communication between not related components, use NgRx store as usual.

The Subject service pattern

In this article solution to first problem is provided. We have multiple components/controls in the same sub tree with validation and some buttons need to be enabled/disabled in dependency of components/controls validation statuses.

To avoid complexity using @Input and @Output it is better to create Subject service, which will act like a local private store in the sub tree.
Pros for this service is, that it is easy to implemented, easy to extend and easy to use. Every time we need to add new object to share its validation state, everything we need to do is to put one property in the ValidationState model. Everything else is done automatically. Why we don’t use the Redux store pattern? Because we don’t need to share the validation state outside BOSS component in our case. If you check the diagram below, you will see, that every time, component updates its state, the data will flow through the whole diagram, just to return to the BOSS component itself and then to its descendants when using NgRx store pattern.

Little theory

There are more subject types:

  • new Subject() On subscription there is no value, it receives the next value emitted in the data stream.
  • new BehaviourSubject(‘’) On subscription will receive the last value in the data stream and will receive every value emitted from the stream after that.
  • new ReplaySubject(3) On subscription will receive the specified number of values (buffer is set to 3 in our case) and will receive every value emitted after that.
    In case of our validation state sharing is the best way to choose BehaviourSubject, as every subscriber will receive the latest state just as he subscribes to the service.

The service

As we need to share the same validation state for every component, we need to create singleton, so every component will inject the same instance of the service.

@Injectable({ providedIn: 'root' })
export class ValidationStateService {
private validationState = new ValidationState();
private subject = new BehaviorSubject<ValidationState>(this.validationState);
updateValidationState(newValidationState: Partial<ValidationState>) {
this.subject.next(
this.validationState.mergeValidationState(newValidationState)
);
}
getValidationState(): Observable<ValidationState> {
return this.subject.asObservable();
}
}

With the @Injectable({ providedIn: 'root' }) we tell angular, we want to use the same instance in every class, which asks for it during dependency injection.

The shared validation object

Next we want to share the validation state object. That object will hold validation states of particular validated objects.

export class ValidationState {
isComponentValid = true;
isTableValid = true;
isInputValid= true;
public mergeValidationState(newValidationState: Partial<ValidationState>) {
Object.assign(this, newValidationState);
return this;
}
isAllValid(): boolean {
const thisProperties = Object.getOwnPropertyNames(this);
for (let i = 0; i < thisProperties.length; i++) {
const propName = thisProperties[i];
if (!this[propName]) {
return false;
}
}
return true;
}
}

As we can see, there are booleans for each validated object, and these booleans are set, when the validation state of the object is changed. Their initial state can be as per needs. We want to share the same object, so we need to create a single instance of this model. This is done in the service

@Injectable({ providedIn: 'root' })
export class ValidationStateService {
private validationState = new ValidationState();

When implementing new boolean property for new validated object, all we need to do is extend this class ValidationState with the new property and set initial state. Everything else is done generic way.

Usage

Inject the service in any component, which is expecting to use it

constructor(private validationService: ValidationStateService) {}

Call the service updateValidationState method with anonymous object, which has the state property and value, we need to update.
this.validationService.updateValidationState({ isInputValid: false } as Partial<ValidationState>);
We dont need to pass the whole ValidationState model with all props as parameter, because it will be created by the service automatically:

updateValidationState(newValidationState: Partial<ValidationState>) {
this.subject.next(
this.validationState.mergeValidationState(newValidationState)
);
}

The mergeValidationState will take care about updating only given property in the whole ValidationState and returns whole ValidationState object.

public mergeValidationState(newValidationState: Partial<ValidationState>) {
Object.assign(this, newValidationState);
return this;
}

This new state is the emitted to all subscribers in the updateValidationState method
Our submit button should be disabled, when one of the emitters is invalid. So again, we inject the service in constructor of the component, where our submit button as subscriber is located:
constructor(private validationService: ValidationStateService){}
And we can now disable the button in template directly using the method isAllValid()

<button
type="submit"
color="accent"
[disabled]="!(validationService.getValidationState() | async).isAllValid()"
mat-raised-button
>

The isAllValid method in the class ValidationState looks on all properties of its own class and returns false, if even one property has false value.

isAllValid(): boolean {
const thisProperties = Object.getOwnPropertyNames(this);
for (let i = 0; i < thisProperties.length; i++) {
const propName = thisProperties[i];
if (!this[propName]) {
return false;
}
}
return true;
}

If we do

validationService.getValidationState().subscribe(state => console.log(state))

we will get object with properties and values:

--

--

Tomáš Trojčák

Fullstack developer since 2007, Angular enthusiast since 2018