Get Interactive BehaviorSubject Stream


The title of this article could be easily converted to "Component Input and Outputs or Services and BehaviorSubject". However, as you can assume, we are going to take a pick on this debate and demo it!

Before we proceed with the case and explain why we chose this one over the other, we should verify this: "Everything exists for a reason! There are cases when one is more suitable than the other, and also personal preferences!"!

I have to admit, whenever I am in this dilemma, my first thought is always "Let's try to see if services and BehaviorSubject suit us!" with a very strong hope that it will in the end!".

Let's quickly see what are the options and when it is most likely suitable to use one over the other:

Component Inputs and Outputs:

Inputs and Outputs are a means to do 'parent-child' communication where a parent component is passing data down to its child components or receiving data/events from child components.

They are easy to use and provide a clear and simple model for component interaction, especially when components have a direct parent-child relationship.

However, inputs and outputs do require each component in the component tree to participate in forwarding the data or event, which can lead to more tightly coupled components and "prop drilling".

They are best for simpler data flow structures - typically top-down where a parent component is passing values to its child components.

Services and BehaviorSubject:

The use of services with a BehaviorSubject enables components to share data without being in a direct parent-child relationship. They are a way to do 'cross-component' communication.

With this method, there is no need for components to be directly connected through any lineage trail.

Components all over the application can share data between them without passing data through intermediate components.

This makes your components more loosely coupled and prevents the issue of "prop drilling" where data has to be passed through every level of the component tree. Moreover, this also centralizes the data and logic related to it, which can make it easier to manage.

This accommodating approach fits a larger or more complex application where various deeply nested components might need to share or manipulate the same data.

The choice between using a service or the input/output mechanism for component communication depends on the specific requirements of the project, the architecture of your app, and your personal preference. Both are valid strategies and have their strengths in different situations.


Now let's see a scenario in which we might want to use the services and BehavioSubject approach!

Imagine you have a small family-oriented diner and you want a simple system to just register the orders. You will have a menu, and then the waiter will place the orders through a mobile/tablet. Obviously, we will not build the entire system here, we will demo a very basic, stripped-down version, just enough to demo how services and BehaviorSubject can be used together so cross components can share a state/stream/data between them!


Let's start with our objects/interfaces:

export interface IOrder {
id: string;
dish: string;
price: number;
}

export interface IBill {
id: number;
orders: IOrder[];
total: number;
}


The IBill is a collection of orders that simply adds the price at the end! IOrder is just a simplified interface for the menu items!

Now let's implement our service!

@Injectable({
providedIn: 'root'
})
export class OrderService {
private billSource = new BehaviorSubject<IBill>({id: 123, orders: [], total: 0});
currentBill = this.billSource.asObservable();

// this is to pass around the menu
private menu: IOrder[] = [
{id: "1", dish: "Burger", price: 4.50},
{id: "2", dish: "Hot Wings", price: 4},
{id: "3", dish: "Lasagna", price: 5},
{id: "4", dish: "Pizza", price: 6}
];

addOrder(order: IOrder): void {
const currentBillValue = this.billSource.value;
currentBillValue.orders.push(order);
currentBillValue.total += order.price;
this.billSource.next(currentBillValue);
}

getMenu() {
return this.menu;
}

payBill(bill: IBill) {
// suppose we pay...
console.log('Paid bill: ' + bill.id);

// reset...
this.billSource.next({id: 123, orders: [], total: 0});
}
}


The idea/implementation is very simple: Create a BehaviorSubject and then create an Observable based on that BehaviorSubject so we can expose it to the service's consumers! The clients (service consumers) subscribe to the Observable and due to the nature of the object (BehaviorSubject) will always be notified of the last updated value when changes!

The OrderService initializes the billSource, which is a BehaviorSubject. As always we need to assign a default value. We make this.

Now let's create some components that will share the data, in this case, the IBill between them, whenever it is changing. 

We will call the parent component "FamilyComponent" because the "parent" is too obvious, and also to avoid confusion or bugs with other libraries! We then will call each of the child components a member of the family, mother, daughter, son, and father! 

Each of the child components will get a "copy" of the menu and the bill. Each time the user adds a dish to the order from one component, all the other child components and the parent component will be updated!

Let's start with the parent component of the family:

export class FamilyComponent implements OnInit {
currentBill: IBill = {id: 0, orders: [], total: 0};

constructor(private orderService: OrderService) {
}

ngOnInit(): void {
this.orderService.currentBill.subscribe(bill => this.currentBill = bill);
}

payBill(bill: IBill) {
this.orderService.payBill(bill);
}

}

The component initializes one object of the IBill and, on component initialization, subscribes to the bill from the Service, so it is notified whenever there is a change to the bill, aka another user placed an order or if the order is paid off!

Now let's create one child component:

export class MotherComponent implements OnInit {
menu: IOrder[] = [];
currentBill: IBill = {id: 0, orders: [], total: 0};

constructor(private orderService: OrderService) {
}

ngOnInit(): void {
this.menu = this.orderService.getMenu();
this.orderService.currentBill.subscribe(bill => this.currentBill = bill);
}

makeOrder(order: IOrder): void {
this.orderService.addOrder(order);
}
}

Similarly, we can create the FatherComponent, DaughterComponent, and SonComponent. All identical except the name!

Surprising this is it! Each child component, when initializations will get the latest "copy" of the menu and will subscribe to the bill! When a user makes the order (makeOrder), will use the service's method "addOrder" which performs our simple logic: add the order to the bill, update the total price, and then set the bill as the latest value (using .next(...) method). That will cause all the components that are subscribed to the bill, parent and child, to get notified and update their local copy of the bill Easy peasy!

How you display this logic is up to you, but if you want quickly to check it out you can grab the below HTML. I am using stand-alone with ngx-bootstrap which requires bootstrap and animations (demo below).

This is my app config file:

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes), provideAnimationsAsync(), BsDropdownModule, BsDropdownConfig]
};

This is the app.component.html:

<div class="container-fluid">
<app-nav-bar></app-nav-bar>
<div class="row">
<div class="col">
<router-outlet></router-outlet>
</div>
</div>
</div>

A very simple and ugly navigation bar just so we can navigate in and out of the parent and child components, as we want to verify that the state of the data stream remains the same:

<div>
<button [routerLink]="['/']" class="btn btn-primary" id="button-home"
type="button">
Home
</button>

<div class="btn-group" dropdown>
<button aria-controls="dropdown-basic" class="btn btn-primary dropdown-toggle" dropdownToggle id="button-basic"
type="button">
Nested BehavioSubject <span class="caret"></span>
</button>
<ul *dropdownMenu aria-labelledby="button-basic" class="dropdown-menu"
id="dropdown-basic" role="menu">
<li role="menuitem"><a [routerLink]="['/family']" class="dropdown-item">family</a></li>
<li role="menuitem"><a [routerLink]="['/mother']" class="dropdown-item">mother</a></li>
<li role="menuitem"><a [routerLink]="['/father']" class="dropdown-item">father</a></li>
<li role="menuitem"><a [routerLink]="['/son']" class="dropdown-item">son</a></li>
<li role="menuitem"><a [routerLink]="['/daughter']" class="dropdown-item">daughter</a></li>
<li class="divider dropdown-divider"></li>
<li role="menuitem"><a class="dropdown-item" [routerLink]="['/']">Home</a>
</li>
</ul>
</div>

</div>
<hr>

You need to add the HomeComponent yourselves, mine is literally empty!

Below is the family component html that serves as parent component:

<h1>Family orders!</h1>
<p>Below is the order summary (bill) from the family orders!</p>
<p>Tested like this:</p>
<ul>
<li>Add orders from this component and go to Home page and back here. Verify that state remains.</li>
<li>Add orders from this component and go child component to add more(from drop down). Verify that stream updates.
</li>
<li>Combine the two above to verify that the stream updates.</li>
</ul>

<h4>Bill ID: {{ currentBill.id }}</h4>
<hr>
<div *ngIf="currentBill">
<div *ngFor="let order of currentBill.orders">
<p>Dish: {{ order.dish }} - {{ order.price | currency: 'GBP' }}</p>
</div>
<h2>Total: {{ currentBill.total | currency: 'GBP' }}</h2>
<button (click)="payBill(currentBill)" class="btn btn-danger">Pay</button>
<hr>
<hr>
</div>


<app-mother></app-mother>
<app-father></app-father>
<app-son></app-son>
<app-daughter></app-daughter>

Below is the mother component that serves as the child (copy-paste over to make the other child components, only the name changes):

<p>Mother orders from menu: (Bill total: {{currentBill.total}})</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let order of menu">
<td>{{ order.dish }}</td>
<td>{{ order.price }}</td>
<td>
<button (click)="makeOrder(order)" class="btn btn-outline-success">Make Order</button>
</td>
</tr>
</tbody>
</table>
<hr>

Just added a small margin for the buttons, not that will make it look better, but...

.btn {
margin: 10px;
}

No files yet, migration hasn't completed yet!