Step 4: Props, Events & The Context Bus
In this step, you will implement decoupled, bidirectional communication between the Angular host shell and the React billing remote without coupling their module bundles or relying on global state.
1. Passing Reactive Props from Host to Fragment
In apps/shell/src/app/app.component.ts, bind reactive props to the <braid-fragment>:
ts
import { Component, signal } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
import { BraidFragmentComponent } from '@braidlabs/angular';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink, BraidFragmentComponent],
template: `
<header style="background: #18181b; color: white; padding: 1rem; display: flex; justify-content: space-between;">
<h1 style="font-size: 1.25rem; margin: 0;">Acme Enterprise Portal</h1>
<div>
<span>Account: <strong>{{ currentAccount() }}</strong></span>
<button (click)="switchAccount()" style="margin-left: 8px;">Switch Account</button>
</div>
</header>
<main style="padding: 1.5rem;">
<braid-fragment
name="billing"
[props]="{ accountId: currentAccount(), currency: 'USD' }"
(fragmentEvent)="onFragmentEvent($event)"
/>
@if (lastNotification()) {
<div style="margin-top: 1rem; padding: 1rem; background: #ecfdf5; border: 1px solid #10b981; border-radius: 6px;">
{{ lastNotification() }}
</div>
}
</main>
`,
})
export class AppComponent {
currentAccount = signal('ACC-99201');
lastNotification = signal<string | null>(null);
switchAccount() {
this.currentAccount.set(
this.currentAccount() === 'ACC-99201' ? 'ACC-44012' : 'ACC-99201'
);
}
onFragmentEvent(event: { type: string; detail: any }) {
if (event.type === 'invoice:paid') {
this.lastNotification.set(
`Received payment confirmation for invoice ${event.detail.invoiceId} (${event.detail.amount})`
);
}
}
}2. Emitting Custom Events from the React Fragment
Update apps/billing/src/app/app.tsx to read props and emit custom DOM events back through the slot:
tsx
import React, { useState } from 'react';
export function App() {
const [invoices, setInvoices] = useState([
{ id: 'INV-101', amount: '$450.00', status: 'Pending' },
{ id: 'INV-102', amount: '$1,200.00', status: 'Pending' },
]);
const payInvoice = (id: string, amount: string) => {
// 1. Update local React state
setInvoices((prev) =>
prev.map((inv) => (inv.id === id ? { ...inv, status: 'Paid' } : inv))
);
// 2. Dispatch a CustomEvent through the DOM facade
// The Braid runtime captures this and bubbles it as (fragmentEvent) on <braid-fragment>
window.dispatchEvent(
new CustomEvent('invoice:paid', {
detail: { invoiceId: id, amount, timestamp: Date.now() },
bubbles: true,
})
);
};
return (
<div style={{ padding: '1.5rem', fontFamily: 'system-ui, sans-serif' }}>
<h2>Billing & Invoices (React Remote)</h2>
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
<thead>
<tr style={{ background: '#f4f4f5', textAlign: 'left' }}>
<th style={{ padding: '8px' }}>Invoice ID</th>
<th style={{ padding: '8px' }}>Amount</th>
<th style={{ padding: '8px' }}>Status</th>
<th style={{ padding: '8px' }}>Action</th>
</tr>
</thead>
<tbody>
{invoices.map((inv) => (
<tr key={inv.id} style={{ borderBottom: '1px solid #e4e4e7' }}>
<td style={{ padding: '8px' }}>{inv.id}</td>
<td style={{ padding: '8px' }}>{inv.amount}</td>
<td style={{ padding: '8px' }}>
<span
style={{
padding: '2px 8px',
borderRadius: '4px',
background: inv.status === 'Paid' ? '#dcfce7' : '#fef9c3',
color: inv.status === 'Paid' ? '#166534' : '#854d0e',
}}
>
{inv.status}
</span>
</td>
<td style={{ padding: '8px' }}>
{inv.status === 'Pending' && (
<button
onClick={() => payInvoice(inv.id, inv.amount)}
style={{
background: '#2563eb',
color: 'white',
border: 'none',
padding: '4px 10px',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Pay Now
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default App;3. Test Bidirectional Communication
- Open
http://localhost:3000/billing. - Click Pay Now on invoice
INV-101in the React table. - Observe:
- React state immediately updates
PendingtoPaid. - The React app dispatches
invoice:paid. - The Angular host intercepts
(fragmentEvent)and renders the green confirmation banner at the top of the shell.
- React state immediately updates
- Click Switch Account on the Angular header:
- Notice the updated
[props]pass seamlessly into the React fragment via structured clone.
- Notice the updated
Next Step
In Step 5: Versioned Data & Offline Storage, we will persist billing state in IndexedDB using @braidlabs/data and @braidlabs/skew with optimistic writes that work offline.