mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
feat(angular): a template's event bindings call the component's methods (#2114)
An Angular handler's only caller is its template: `(click)="toggleFavorite()"`,
`(ngSubmit)="submitForm()"`. With no edge, the method showed no callers,
impact stopped at it, and the Steps picture of a screen reached its
handlers only through class containment, with nothing to say what fires
them.
The template pass now reads each `(event)="…"` binding — not
`[(ngModel)]`'s two-way half — and links the component to every method of
its own the statement calls (`save()`, `this.toggle(x)`; not
`closed.emit()` or `form.reset()`), as a `calls` edge
(`synthesizedBy: 'angular-event'`) carrying the binding as
`metadata.trigger`: `{ kind: 'prop', name: '(click)', of: 'button' }`.
The binding is in the template, so it cannot be read back from the source
at the edge's line; Steps now takes a trigger an edge carries before
reading one at the site.
angular-realworld: 21 binding sites → 17 edges (3 repeats of a method and
event, 1 `delete.emit(true)`); Steps for /article/:slug draws
deleteArticle on (click) · <button> → DELETE and back to /, addComment on
(ngSubmit) · <form>, the favorite button's toggle → POST/DELETE or
/register. Ghostfolio 225 edges of 302 sites, ngx-admin 75 of 100 — the
rest are `$event.stopPropagation()`, output emits, assignments and
repeats. Controls (koel, proshop) byte-identical.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
761611c18e
commit
c3efbbf4e7
@@ -15,6 +15,7 @@ import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
import { commandsHref, localizeDefault, parseAngularRoutes, staticString } from '../src/resolution/frameworks/angular-router';
|
||||
import { buildScreens } from '../src/ui-server/api/screens';
|
||||
import { buildSteps } from '../src/ui-server/api/steps';
|
||||
|
||||
describe('parseAngularRoutes', () => {
|
||||
const file = (body: string) => `import { Routes } from '@angular/router';\n${body}`;
|
||||
@@ -189,7 +190,13 @@ export class AdminRoutingModule {}
|
||||
<a [routerLink]="['/account', 'access', 7]">Access</a>
|
||||
<a routerLink="relative">Relative</a>
|
||||
`,
|
||||
'src/app/shared/card.component.ts': component('CardComponent', 'app-card', `<button (click)="save()">Save</button>`, ` save() { this.router.navigate(internalRoutes.account.subRoutes.access.routerLink('me')); }`).replace(
|
||||
'src/app/shared/card.component.ts': component(
|
||||
'CardComponent',
|
||||
'app-card',
|
||||
`<button (click)="save()">Save</button><input [(ngModel)]="name" /><a (click)="closed.emit(true)">x</a>`,
|
||||
` name = '';
|
||||
save() { this.router.navigate(internalRoutes.account.subRoutes.access.routerLink('me')); }`
|
||||
).replace(
|
||||
"import { Router } from '@angular/router';",
|
||||
"import { Router } from '@angular/router';\nimport { internalRoutes } from '../routes.constants';"
|
||||
),
|
||||
@@ -270,6 +277,27 @@ export class AdminRoutingModule {}
|
||||
expect(renders).toEqual(['HomeComponent -> CardComponent <app-card>']);
|
||||
});
|
||||
|
||||
it("links a template's event bindings to the component's own methods, the binding as the trigger", async () => {
|
||||
const bindings = cg
|
||||
.getNodesByKind('class')
|
||||
.flatMap((c) =>
|
||||
cg
|
||||
.getOutgoingEdgesFrom([c.id], ['calls'])
|
||||
.filter((e) => (e.metadata as Record<string, unknown> | undefined)?.synthesizedBy === 'angular-event')
|
||||
.map((e) => {
|
||||
const meta = e.metadata as Record<string, unknown>;
|
||||
return `${c.name} -> ${cg.getNode(e.target)?.name} ${JSON.stringify(meta.trigger)}`;
|
||||
})
|
||||
);
|
||||
// `[(ngModel)]` is a two-way binding and `closed.emit()` raises the component's own output: neither calls a method.
|
||||
expect(bindings).toEqual(['CardComponent -> save {"kind":"prop","name":"(click)","of":"button"}']);
|
||||
|
||||
const home = cg.getNodesByKind('route').find((r) => r.name === '/home')!;
|
||||
const payload = await buildSteps(cg, root, new URLSearchParams({ anchor: home.id }));
|
||||
const save = payload.steps.find((st) => st.label === 'save');
|
||||
expect(save?.trigger).toMatchObject({ kind: 'prop', name: '(click)', of: 'button', in: 'CardComponent' });
|
||||
});
|
||||
|
||||
it("draws the Screens picture: a child component's navigation on its screen, a layout's on each screen inside it", async () => {
|
||||
const payload = await buildScreens(cg, root);
|
||||
expect(payload.routed).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user