Skip to content

Commit

Permalink
feat(astro-angular): implement output forwarding on client-side hydra…
Browse files Browse the repository at this point in the history
…ted components (#641)
  • Loading branch information
rlmestre committed Sep 14, 2023
1 parent 4224f1d commit 3e836cb
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 6 deletions.
23 changes: 23 additions & 0 deletions apps/astro-app-e2e-playwright/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,28 @@ describe('AstroApp', () => {
componentLocator.locator('>> text=Angular (server side binding)')
).toContain(/Angular \(server side binding\)/i);
});

test('Then client side rendered CardComponent should emit an event on click', async () => {
const console = waitForConsole();
const componentLocator = page.locator(
'[data-analog-id=card-component-1]'
);
const elementLocator = componentLocator.locator('li');
await elementLocator.click();

await expect(await console).toBe(
'event received from card-component-1: clicked'
);
});
});
});

async function waitForConsole(): Promise<string> {
return new Promise(function (resolve) {
page.on('console', (msg) => {
if (msg.type() === 'log') {
resolve(msg.text());
}
});
});
}
9 changes: 8 additions & 1 deletion apps/astro-app/src/components/card.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
inject,
Input,
Output,
ViewEncapsulation,
} from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
Expand All @@ -12,7 +14,7 @@ import { HttpClient, provideHttpClient } from '@angular/common/http';
selector: 'astro-card',
standalone: true,
template: `
<li class="link-card">
<li class="link-card" (click)="onClick()">
<a [href]="href">
<h2>
{{ title }}
Expand Down Expand Up @@ -93,9 +95,14 @@ export class CardComponent {
@Input() href = '';
@Input() title = '';
@Input() body = '';
@Output() output = new EventEmitter<string>();

static renderProviders = [provideHttpClient()];
static clientProviders = [CardComponent.renderProviders, provideAnimations()];

private _http = inject(HttpClient);

onClick() {
this.output.emit('clicked');
}
}
10 changes: 8 additions & 2 deletions apps/astro-app/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const serverSideTitle = 'Angular (server side binding)';
/>
<CardComponent
client:visible
data-analog-id="card-component-1"
href="https://angular.io/"
title="Angular (Client Side)"
body="Build with Angular. ❤️"
Expand Down Expand Up @@ -111,5 +112,10 @@ const serverSideTitle = 'Angular (server side binding)';
</style>

<script>
console.log('Hello Astro');
</script>
import { addOutputListener } from '@analogjs/astro-angular/utils';
console.log('Hello Astro');

addOutputListener('card-component-1', 'output', (event) => {
console.log('event received from card-component-1:', event.detail);
});
</script>
36 changes: 33 additions & 3 deletions packages/astro-angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class HelloComponent {

Add the Angular component to the Astro component template. This only renders the HTML from the Angular component.

```ts
```tsx
---
import { HelloComponent } from '../components/hello.component';

Expand All @@ -188,7 +188,7 @@ const helpText = "Helping binding";

To hydrate the component on the client, use one of the Astro [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives):

```ts
```tsx
---
import { HelloComponent } from '../components/hello.component';
---
Expand All @@ -198,6 +198,37 @@ import { HelloComponent } from '../components/hello.component';

Find more information about [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) in the Astro documentation.

### Listening to Component Outputs

Outputs can be emitted by the Angular component are forwarded as HTML events to the Astro island.
To enable this feature, add a client directive and a unique `[data-analog-id]` property to each Angular component:

```tsx
---
import { HelloComponent } from '../components/hello.component';
---

<HelloComponent client:visible data-analog-id="hello-component-1" />
```

Then, listen to the event in the Astro component using the `addOutputListener` function:

```tsx
---
import { HelloComponent } from '../components/hello.component';
---

<HelloComponent client:visible data-analog-id="hello-component-1" />

<script>
import { addOutputListener } from '@analogjs/astro-angular/utils';

addOutputListener('hello-component-1', 'outputName', (event) => {
console.log(event.detail);
});
</script>
```

## Adding Component Providers

Additional providers can be added to a component for static rendering and client hydration.
Expand Down Expand Up @@ -295,4 +326,3 @@ import { HelloComponent } from "../../components/hello.component.ts";
## Current Limitations

- Only standalone Angular components in version v14.2+ are supported
- Component Outputs are not supported
1 change: 1 addition & 0 deletions packages/astro-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"author": "Brandon Roberts <robertsbt@gmail.com>",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils.js",
"./client.js": "./src/client.js",
"./server.js": "./src/server.js",
"./package.json": "./package.json"
Expand Down
33 changes: 33 additions & 0 deletions packages/astro-angular/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@angular/core';
import { ApplicationRef, NgZone, createComponent } from '@angular/core';
import { createApplication } from '@angular/platform-browser';
import { Observable, Subject, takeUntil } from 'rxjs';

export default (element: HTMLElement) => {
return (
Expand Down Expand Up @@ -40,6 +41,38 @@ export default (element: HTMLElement) => {
}
}

if (mirror?.outputs.length && props?.['data-analog-id']) {
const destroySubject = new Subject<void>();
element.setAttribute(
'data-analog-id',
props['data-analog-id'] as string
);

mirror.outputs.forEach(({ templateName, propName }) => {
const outputName = templateName || propName;
const component = componentRef.instance as Record<
string,
Observable<unknown>
>;
component[outputName]
.pipe(takeUntil(destroySubject))
.subscribe((detail) => {
const event = new CustomEvent(outputName, {
bubbles: true,
cancelable: true,
composed: true,
detail,
});
element.dispatchEvent(event);
});
});

appRef.onDestroy(() => {
destroySubject.next();
destroySubject.complete();
});
}

appRef.attachView(componentRef.hostView);
});
});
Expand Down
21 changes: 21 additions & 0 deletions packages/astro-angular/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function addOutputListener(
analogId: string,
outputName: string,
callback: (...args: any[]) => unknown,
eventListenerOptions: EventListenerOptions = {}
) {
const observer = new MutationObserver((mutations) => {
const foundTarget = mutations.find(
(mutation) =>
(mutation.target as HTMLElement).dataset?.['analogId'] === analogId
)?.target;

if (foundTarget) {
foundTarget.addEventListener(outputName, callback, eventListenerOptions);
observer.disconnect();
}
});
observer.observe(document.body, { attributes: true, subtree: true });

return () => observer.disconnect();
}

0 comments on commit 3e836cb

Please sign in to comment.