{"id":50713998,"url":"https://github.com/mauriziocescon/ng-next","last_synced_at":"2026-06-09T17:30:48.170Z","repository":{"id":292428683,"uuid":"973553358","full_name":"mauriziocescon/ng-next","owner":"mauriziocescon","description":"Just personal thoughts on the future of ng","archived":false,"fork":false,"pushed_at":"2026-06-08T09:56:31.000Z","size":894,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-08T11:26:59.861Z","etag":null,"topics":["angular","components","signals"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mauriziocescon.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-27T08:33:02.000Z","updated_at":"2026-06-08T09:56:36.000Z","dependencies_parsed_at":"2026-02-02T13:22:06.622Z","dependency_job_id":null,"html_url":"https://github.com/mauriziocescon/ng-next","commit_stats":null,"previous_names":["mauriziocescon/ng-playground","mauriziocescon/ng-outlook","mauriziocescon/ng-next"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mauriziocescon/ng-next","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauriziocescon%2Fng-next","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauriziocescon%2Fng-next/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauriziocescon%2Fng-next/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauriziocescon%2Fng-next/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mauriziocescon","download_url":"https://codeload.github.com/mauriziocescon/ng-next/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mauriziocescon%2Fng-next/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34118750,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","components","signals"],"created_at":"2026-06-09T17:30:47.330Z","updated_at":"2026-06-09T17:30:48.164Z","avatar_url":"https://github.com/mauriziocescon.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Anatomy of Signal Components\n\n**⚠️ Note ⚠️: personal thoughts from a developer's perspective on [the future of Angular](https://myconf.dev/videos/2024-keynote-session-the-future-of-angular) at the template level.**\n\nHighlights:\n\n1. Building blocks as functions:\n  - `*.ng` files with template DSL (see [appendix](#appendix-co-located-templates-in-angular-via-ng-files)),\n  - `component`: a `setup` with scoped logic that returns a `template` or `{ template, expose }`,\n  - `directive`: a `setup` that can change the appearance or behavior of DOM elements,\n  - `derivation`: a factory for template-scoped computed values that requires DI,\n  - `fragment`: a way to capture some markup in the form of a function,\n2. TS expressions with `{}`: bindings + text interpolation\n3. Extra bindings for DOM elements: `bind:`, `on:`, `model:`, `class:`, `style:`, `animate:`, `use:`,\n4. Hostless components + TS lexical scoping for templates,\n5. Component inputs: lifted up + immediately available in setup and providers,\n6. Composition with Fragments, Directives, and Forward Syntax,\n7. Expose and Template Refs,\n8. Dependency Injection Enhancements,\n9. Final considerations (`!important`) + [`types`](https://github.com/mauriziocescon/ng-next/blob/main/types/ng-types.ts).\n\n**Template syntax note**: the template syntax in the examples below resembles TSX syntactically but is Angular DSL, not JSX. It supports Angular control flow, directives, custom bindings, and an Angular-owned `IntrinsicElements` map for native tag typing.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eTable of contents\u003c/strong\u003e\u003c/summary\u003e\n\n- [Component structure and bindings](#component-structure-and-bindings)\n- [Element directives](#element-directives)\n- [Template-Scoped Derivations (`@derive`)](#template-scoped-derivations-derive)\n- [Binding syntax helpers](#binding-syntax-helpers)\n- [One-time bindings (`once:`)](#one-time-bindings-once)\n- [Input-driven providers](#input-driven-providers)\n- [Composition with Fragments, Directives, and Forward Syntax](#composition-with-fragments-directives-and-forward-syntax)\n- [Expose and Template Refs](#expose-and-template-refs)\n- [Dependency Injection Enhancements](#dependency-injection-enhancements)\n- [Final considerations](#final-considerations)\n- [Appendix: Co-located templates in Angular via `.ng` files](#appendix-co-located-templates-in-angular-via-ng-files)\n\n\u003c/details\u003e\n\n## Component structure and bindings\n\n`setup` runs once in an injection context. All bindings are wired and available immediately; destructuring is optional:\n\n```ts\nimport { component, signal, linkedSignal, input, output } from '@angular/core';\n\nexport const TextSearch = component({\n  bindings: {\n    value: input.required\u003cstring\u003e(),\n    valueChange: output\u003cstring\u003e(),\n  },\n  setup: ({ value, valueChange }) =\u003e {\n    const text = linkedSignal(() =\u003e value());\n    const isDanger = signal(false);\n\n    function textChange() {\n      valueChange.emit(text());\n    }\n\n    /**\n     * Native elements are resolved through IntrinsicElements.\n     * This gives the compiler the element type plus valid DOM\n     * attributes, properties, and events.\n     *\n     * - 1way: bind:property={var} (bind: can be omitted)\n     * - 2way: model:property={var} (input / select / textarea)\n     * - events: on:event_name={handler}\n     *\n     * Invalid native bindings are compile-time errors:\n     * ‼️ \u003cinput typ=\"text\" /\u003e // unknown attribute ‼️\n     * ‼️ \u003cspan class=\"...\" class={...}\u003e // duplicate static/bound ‼️\n     * ‼️ \u003cspan on:click={...} on:click={...}\u003e // duplicate event ‼️\n     *\n     * Can use multiple class: and style:\n     * ✅ \u003cspan class=\"...\" class:some-class={...} class:some-other-class={...}\u003e ✅\n     */\n    return (\n      \u003clabel class:danger={isDanger()}\u003eText:\u003c/label\u003e\n      \u003cinput type=\"text\" model:value={text} on:input={textChange} /\u003e\n\n      \u003cbutton disabled={text().length === 0} on:click={() =\u003e text.set('')}\u003e\n        {'Reset ' + text()}\n      \u003c/button\u003e\n    );\n  },\n  style: `\n    .danger {\n      color: red;\n    }\n  `,\n});\n```\n\nAny component can be used in the template; `bind:`, `model:`, and `on:` behave the same as for native elements:\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { UserDetail, User } from './user-detail.ng';\n\nexport const UserDetailConsumer = component({\n  setup: () =\u003e {\n    const user = signal\u003cUser\u003e(/** ... **/);\n    const email = signal\u003cstring\u003e(/** ... **/);\n\n    function makeAdmin() {/** ... **/}\n\n    /**\n     * ⚠️ Must provide all required inputs / models ⚠️\n     *\n     * Invalid component bindings are compile-time errors:\n     * ‼️ \u003cUserDetail role=\"admin\" /\u003e // unknown binding ‼️\n     * ‼️ \u003cUserDetail user={...} user={...} model:user={...} /\u003e // duplicate binding ‼️\n     * ‼️ \u003cUserDetail on:makeAdmin={...} on:makeAdmin={...} /\u003e  // duplicate binding ‼️\n     *\n     * Shouldn't use 'on' prefix with input / model / output\n     * ⚠️ \u003cUserDetail onInput={...} model:onModel={...} on:onEvent={...} /\u003e ⚠️\n     */\n    return (\n      \u003cUserDetail\n        user={user()}\n        model:email={email}\n        on:makeAdmin={makeAdmin} /\u003e\n    );\n  },\n});\n\n// -- UserDetail -----------------------------------\nimport { component, input, model, output } from '@angular/core';\n\nexport interface User {/** ... **/}\n\nexport const UserDetail = component({\n  bindings: {\n    user: input.required\u003cUser\u003e(),\n    email: model\u003cstring\u003e(),\n    makeAdmin: output\u003cvoid\u003e(),\n  },\n  setup: (bindings) =\u003e (\n    // bindings.user, bindings.email, bindings.makeAdmin\n  ),\n});\n```\n\nLexical scoping resolves in this order: template → setup → functions, constants, enums, and interfaces imported in the file → global.\n\n```ts\nimport { component } from '@angular/core';\n\nenum Type {\n  Counter = 'counter',\n  Other = 'other',\n}\n\nconst type = Type.Counter;\n\nconst counter = (value: number) =\u003e `Let's count till ${value}`;\n\nexport const Counter = component({\n  setup: () =\u003e (\n    @if (type === Type.Counter) {\n      \u003cp\u003e{counter(5)}\u003c/p\u003e\n    } @else {\n      \u003cspan\u003eEmpty\u003c/span\u003e\n    }\n  ),\n});\n```\n\n## Element directives\n\nChange the appearance or behavior of DOM elements:\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { tooltip } from '@mylib/tooltip';\n\nexport const TextSearch = component({\n  setup: () =\u003e {\n    const text = signal('');\n    const message = signal('Message');\n\n    function valueChange() {/** ... **/}\n    function doSomething() {/** ... **/}\n\n    /**\n     * Encapsulation of directive data: use:directive(...)\n     * Any directive can be used directly in the template\n     */\n    return (\n      \u003cinput\n        type=\"text\"\n        model:value={text}\n        on:input={valueChange}\n        use:tooltip(message={message()} on:dismiss={doSomething}) /\u003e\n\n      \u003cp\u003eValue: {text()}\u003c/p\u003e\n    );\n  },\n});\n\n// -- tooltip in @mylib/tooltip --------------------\nimport { directive, ref, input, output, inject, DestroyRef, Renderer2, afterRenderEffect } from '@angular/core';\n\nexport const tooltip = directive({\n  /**\n   * Host element constraint. When this directive is used on a\n   * native tag, the tag's IntrinsicElements entry supplies the\n   * concrete host type checked against this ref type.\n   */\n  host: ref\u003cHTMLElement\u003e(),\n  bindings: {\n    message: input.required\u003cstring\u003e(),\n    dismiss: output\u003cvoid\u003e(),\n  },\n  setup: ({ message, dismiss }, { host }) =\u003e {\n    const destroyRef = inject(DestroyRef);\n    const renderer = inject(Renderer2);\n\n    afterRenderEffect(() =\u003e {\n      const hostEl: HTMLElement | undefined = host();\n      // something with hostEl\n    });\n\n    destroyRef.onDestroy(() =\u003e {\n      // cleanup logic\n    });\n  },\n});\n```\n\n## Template-Scoped Derivations (`@derive`)\n\n`@derive` creates a template-scoped reactive computation, establishing an injection context before calling the derivation's `setup`. It follows the lifecycle of the enclosing view. Bindings are passed as named pairs `key={expr}`, not as a JS object literal.\n\n```ts\nimport { component, derivation, computed, inject, input } from '@angular/core';\nimport { Item, PriceManager } from '@mylib/item';\n\nconst simulation = derivation({\n  bindings: {\n    /**\n     * Only inputs are allowed: a derivation has no DOM host,\n     * so there is no surface to emit outputs or sync models against\n     */\n    item: input.required\u003cItem\u003e(),\n    qty: input.required\u003cnumber\u003e(),\n  },\n  /**\n   * setup always returns Signal\u003cT\u003e (e.g. computed)\n   */\n  setup: ({ item, qty }) =\u003e {\n    const priceManager = inject(PriceManager);\n\n    return computed(() =\u003e priceManager.computePrice(item(), qty()));\n  },\n});\n\nexport const PriceSimulator = component({\n  bindings: {\n    items: input.required\u003cItem[]\u003e(),\n  },\n  setup: ({ items }) =\u003e {\n    /**\n     * Any derivation can be used directly in the template via @derive\n     *\n     * price shares the @for embedded view scope and is created once,\n     * following its lifecycle. Same scope as @let, same lifetime as\n     * a pure pipe. Each row owns an independent instance. Not accessible\n     * outside its block.\n     */\n    return (\n      @for (item of items(); track item.id) {\n        @derive price = simulation(item={item} qty={1});\n\n        \u003ch5\u003e{item.desc}\u003c/h5\u003e\n        \u003cdiv\u003ePrice: {price()}\u003c/div\u003e\n      }\n    );\n  },\n});\n```\n\n## Binding syntax helpers\n\n- Literal form equivalence for inputs: literal attributes and literal expressions are equivalent for inputs: `prop=\"value\"` and `prop={'value'}` produce the same input value.\n- `:when`: conditionally applies a `use:` binding; sits outside the directive's inputs and cannot clash with them.\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { tooltip } from '@mylib/tooltip';\n\nexport const SearchBox = component({\n  setup: () =\u003e {\n    const text = signal('');\n    const showTip = signal(true);\n    const tip = signal('Type to search');\n\n    return (\n      // Literal equivalence: placeholder=\"Search\" and placeholder={'Search'} are identical\n      \u003cinput\n        type=\"text\"\n        placeholder=\"Search\"\n        model:value={text}\n        use:tooltip(message={tip()}):when={showTip()} /\u003e\n    );\n  },\n});\n```\n\n## One-time bindings (`once:`)\n\n`once:` lets the consumer freeze an input at creation time. The value is seeded once and never updated, even if the source signal changes later. Rules:\n\n- `once:` applies only to inputs.\n- `once:model:*` and `once:on:*` are compile-time errors.\n- `once:prop` and `prop` together on the same element are a duplicate binding error.\n- Literal input expressions are effectively one-time: `prop={'10'}` is semantically equivalent to `once:prop={v()}` where `v()` is a signal returning `'10'`.\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { UserDetail, User } from './user-detail.ng';\n\nexport const UserDetailConsumer = component({\n  setup: () =\u003e {\n    const user = signal\u003cUser\u003e(/** ... **/);\n    const email = signal\u003cstring\u003e(/** ... **/);\n\n    function makeAdmin() {/** ... **/}\n\n    return (\n      \u003cUserDetail\n        once:user={user()}\n        model:email={email}\n        on:makeAdmin={makeAdmin} /\u003e\n    );\n  },\n});\n```\n\n## Input-driven providers\n\nInputs hoisted to the component level for use in provider initialization (`providers` receives only inputs — not models or outputs):\n\n```ts\nimport { component, linkedSignal, input, WritableSignal, provide, inject } from '@angular/core';\n\nclass CounterStore {\n  private readonly counter: WritableSignal\u003cnumber\u003e;\n  readonly value = this.counter.asReadonly();\n\n  constructor(c = () =\u003e 0) {\n    this.counter = linkedSignal(() =\u003e c());\n  }\n\n  decrease() {/** ... **/}\n  increase() {/** ... **/}\n}\n\nexport const Counter = component({\n  bindings: {\n    c: input.required\u003cnumber\u003e(),\n  },\n  setup: () =\u003e {\n    const store = inject(CounterStore);\n\n    return (\n      \u003ch1\u003eCounter\u003c/h1\u003e\n      \u003cdiv\u003eValue: {store.value()}\u003c/div\u003e\n      \u003cbutton on:click={() =\u003e store.decrease()}\u003e-\u003c/button\u003e\n      \u003cbutton on:click={() =\u003e store.increase()}\u003e+\u003c/button\u003e\n    );\n  },\n  /**\n   * Only inputs are provided\n   */\n  providers: ({ c }) =\u003e [\n    provide(CounterStore, () =\u003e new CounterStore(c)),\n  ],\n});\n```\n\n## Composition with Fragments, Directives, and Forward Syntax\n\nFragments are similar to [Svelte snippets](https://svelte.dev/docs/svelte/snippet): functions that return HTML markup. The returned markup is opaque — it cannot be manipulated like [React Children (legacy)](https://react.dev/reference/react/Children) or [Solid children](https://www.solidjs.com/tutorial/props_children). \n\n`@forward()` designates where forwarded directives and bindings land. In `component.withDirectiveForwarding\u003cS\u003e(...)`, it targets an element for directive passthrough. In `component.wrap(Target, ...)`, it forwards remaining bindings and directives to the wrapped component.\n\nImplicit children fragment — placement, lifecycle, and binding context:\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { Menu, MenuItem } from '@mylib/menu';\n\nexport const MenuConsumer = component({\n  setup: () =\u003e {\n    const first = signal('First');\n    const second = signal('Second');\n\n    /**\n     * Markup inside a component tag implicitly becomes a children fragment\n     */\n    return (\n      \u003cMenu\u003e\n        \u003cMenuItem\u003e{first()}\u003c/MenuItem\u003e\n        \u003cMenuItem\u003e{second()}\u003c/MenuItem\u003e\n      \u003c/Menu\u003e\n    );\n  },\n});\n\n// -- Menu in @mylib/menu --------------------------\nimport { component, fragment } from '@angular/core';\n\nexport const Menu = component({\n  bindings: {\n    /**\n     * Provided by Angular from nested content (not bindable directly).\n     * This name is reserved by Angular.\n     */\n    children: fragment\u003cvoid\u003e(),\n  },\n  setup: ({ children }) =\u003e {\n    /** ... **/\n\n    /**\n     * No ng-container needed; full form: @render(fragment(), { injector })\n     */\n    return (\n      @if (children) {\n        @render(children())\n      } @else {\n        \u003cspan\u003eEmpty\u003c/span\u003e\n      }\n    );\n  },\n});\n\nexport const MenuItem = component({\n  bindings: {\n    children: fragment.required\u003cvoid\u003e(),\n  },\n  setup: ({ children }) =\u003e (\n    @render(children())\n  ),\n});\n```\n\nCustomizing components:\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { Menu } from '@mylib/menu';\nimport { MyMenuItem } from './my-menu-item.ng';\n\nexport interface Item {\n  id: string;\n  desc: string;\n}\n\nexport const MenuConsumer = component({\n  setup: () =\u003e {\n    const items = signal\u003cItem[]\u003e(/** ... **/);\n\n    /**\n     * Inline @fragment is auto-passed as the matching fragment input.\n     * Equivalent explicit form: declare @fragment outside, pass as menuItem={menuItem}.\n     */\n    return (\n      \u003cMenu items={items()}\u003e\n        @fragment menuItem(item: Item) {\n          \u003cdiv class=\"my-menu-item\"\u003e\n            \u003cMyMenuItem\u003e{item.desc}\u003c/MyMenuItem\u003e\n          \u003c/div\u003e\n        }\n      \u003c/Menu\u003e\n    );\n  },\n  styleUrl: './menu-consumer.css',\n});\n\n// -- Menu in @mylib/menu --------------------------\nimport { component, input, fragment } from '@angular/core';\n\nexport const Menu = component({\n  bindings: {\n    items: input.required\u003c{ id: string, desc: string }[]\u003e(),\n    menuItem: fragment.required\u003c[{ id: string, desc: string }]\u003e(),\n  },\n  setup: ({ items, menuItem }) =\u003e (\n    \u003ch1\u003e Total items: {items().length} \u003c/h1\u003e\n\n    @for (item of items(); track item.id) {\n      @render(menuItem(item))\n    }\n  ),\n});\n```\n\nDirectives passed through a component and applied to an element:\n\n```ts\nimport { component, signal } from '@angular/core';\nimport { Button } from '@mylib/button';\nimport { ripple } from '@mylib/ripple';\nimport { tooltip } from '@mylib/tooltip';\n\nexport const ButtonConsumer = component({\n  setup: () =\u003e {\n    const tooltipMsg = signal('');\n    const valid = signal(false);\n\n    function doSomething() {/** ... **/}\n\n    /**\n     * Type safety: Button forwards to HTMLButtonElement, so only\n     * directives whose host is assignable from HTMLButtonElement\n     * are accepted (e.g. host: ref\u003cHTMLInputElement\u003e() → compile error).\n     * \n     * The same directive cannot be applied more than once\n     * to the same component / element.\n     */\n    return (\n      \u003cButton\n        type=\"button\"\n        style=\"background-color: cyan\"\n        class={valid() ? 'global-css-valid' : ''}\n        use:ripple()\n        use:tooltip(message={tooltipMsg()})\n        disabled={!valid()}\n        on:click={doSomething}\u003e\n          Click / Hover me\n      \u003c/Button\u003e\n    );\n  },\n});\n\n// -- button in @mylib/button --------------------\nimport { component, input, output, computed, fragment } from '@angular/core';\n\nexport const Button = component.withDirectiveForwarding\u003cHTMLButtonElement\u003e({\n  bindings: {\n    type: input\u003c'button' | 'submit' | 'reset'\u003e('button'),\n    class: input\u003cstring\u003e(''),\n    style: input\u003cstring\u003e(''),\n    disabled: input\u003cboolean\u003e(false),\n    click: output\u003cvoid\u003e(),\n    children: fragment.required\u003cvoid\u003e(),\n  },\n  setup: ({ type, class: className, style, disabled, click, children }) =\u003e {\n    const innerStyle = computed(() =\u003e `${style()}; color: red;`);\n\n    /**\n     * Directive forwarding: directives applied to \u003cButton /\u003e are propagated to\n     * and instantiated on the internal \u003cbutton\u003e (HTMLButtonElement) element.\n     */\n    return (\n      \u003cbutton\n        @forward()\n        type={type()}\n        class={className()}\n        style={innerStyle()}\n        disabled={disabled()}\n        on:click={() =\u003e click.emit()}\u003e\n        @render(children())\n      \u003c/button\u003e\n    );\n  },\n});\n```\n\nWrapping components and forwarding inputs, outputs, models, fragments, and directives:\n\n```ts\nimport { component, signal, input, computed } from '@angular/core';\nimport { tooltip } from '@mylib/tooltip';\nimport { UserDetail, User } from './user-detail.ng';\n\nexport const UserDetailConsumer = component({\n  setup: () =\u003e {\n    const user = signal\u003cUser\u003e(/** ... **/);\n    const email = signal\u003cstring\u003e(/** ... **/);\n\n    function makeAdmin() {/** ... **/}\n\n    return (\n      \u003cUserDetailWrapper\n        user={user()}\n        model:email={email}\n        on:makeAdmin={makeAdmin} /\u003e\n    );\n  },\n});\n\n/**\n * Wrapper: selected bindings go to setup, remainder forwarded via @forward()\n */\nexport const UserDetailWrapper = component.wrap(UserDetail, {\n  bindings: {\n    user: input.required\u003cUser\u003e(),\n  },\n  setup: ({ user }) =\u003e {\n    const other = computed(() =\u003e /** something depending on user() or a default value **/);\n\n    return (\n      \u003cUserDetail \n        @forward() \n        use:tooltip(message={'Tooltip message'}) \n        user={other()} /\u003e\n    );\n  },\n});\n\n// -- UserDetail -----------------------------------\nimport { component, input, model, output, fragment } from '@angular/core';\n\nexport interface User {\n  name: string;\n  role: string;\n}\n\nexport const UserDetail = component.withDirectiveForwarding\u003cHTMLElement\u003e({\n  bindings: {\n    user: input.required\u003cUser\u003e(),\n    email: model.required\u003cstring\u003e(),\n    makeAdmin: output\u003cvoid\u003e(),\n    children: fragment\u003cvoid\u003e(),\n  },\n  setup: ({ user, email, makeAdmin, children }) =\u003e (\n    \u003cdiv @forward()\u003e\n      \u003ch3\u003e{user().name}\u003c/h3\u003e\n      \u003cp\u003eRole: {user().role}\u003c/p\u003e\n\n      \u003clabel\u003eEmail:\u003c/label\u003e\n      \u003cinput type=\"email\" model:value={email} /\u003e\n\n      \u003cbutton on:click={() =\u003e makeAdmin.emit()}\u003eMake Admin\u003c/button\u003e\n\n      @render(children?.())\n    \u003c/div\u003e\n  ),\n});\n```\n\n## Expose and Template Refs\n\n`expose` is the public interface of `setup()` for refs. Components return it along with `template`; directives return it from `setup`.\n\n`ref(Type)` → `Signal\u003cexpose | undefined\u003e`, `refMany(Type)` → `Signal\u003cexpose[]\u003e`; without `expose`, they resolve to `Signal\u003cundefined\u003e` and `Signal\u003cundefined[]\u003e`. Elements and components are bound with `ref={...}`, or with `use:...:ref={...}` for directives, and can be read after `afterNextRender`.\n\n```ts\nimport { component, ref, refMany, signal, input, afterNextRender, Signal } from '@angular/core';\nimport { ripple } from '@mylib/ripple';\nimport { tooltip } from '@mylib/tooltip';\n\nconst Child = component({\n  setup: () =\u003e {\n    const text = signal('');\n    const _internal = signal(0); // not exposed\n\n    return {\n      template: (...),\n      // expose: component's public interface — only these are accessible via ref\n      expose: {\n        text: text.asReadonly(),\n      },\n    };\n  },\n});\n\nconst Sibling = component({\n  bindings: {\n    childRef: input.required\u003c{ text: Signal\u003cstring\u003e } | undefined\u003e(),\n  },\n  setup: ({ childRef }) =\u003e (\n    \u003cbutton on:click={() =\u003e childRef()?.text()}\u003eShow text\u003c/button\u003e\n  ),\n});\n\nexport const Parent = component({\n  setup: () =\u003e {\n    // Native element: type explicit -\u003e Signal\u003cHTMLDivElement | undefined\u003e.\n    // The template compiler checks ref={el} against the tag type from\n    // IntrinsicElements, so \u003cdiv ref={el}\u003e is valid but \u003cinput ref={el}\u003e\n    // is not.\n    const el = ref\u003cHTMLDivElement\u003e();\n    // Component: type inferred from expose → Signal\u003c{ text: Signal\u003cstring\u003e } | undefined\u003e\n    const child = ref(Child);\n    // Directive: type inferred from setup() return → Signal\u003c{ toggle: () =\u003e void } | undefined\u003e\n    const tlp = ref(tooltip);\n    // Multiple instances (e.g. inside @for) → Signal\u003c{ text: Signal\u003cstring\u003e }[]\u003e\n    const many = refMany(Child);\n\n    afterNextRender(() =\u003e {\n      // refs resolve here\n    });\n\n    return (\n      \u003cdiv\n        ref={el}\n        use:ripple()\n        use:tooltip(message={'something'}):ref={tlp}\u003e\n          Something\n      \u003c/div\u003e\n\n      \u003cChild ref={child} /\u003e\n      \u003cSibling childRef={child()} /\u003e\n\n      \u003cChild ref={many} /\u003e\n      \u003cChild ref={many} /\u003e\n\n      \u003cbutton on:click={() =\u003e tlp()?.toggle()}\u003eToggle tlp\u003c/button\u003e\n    );\n  },\n});\n```\n\n## Dependency Injection Enhancements\n\nImproved ergonomics for types and tokens:\n\n```ts\nimport { component, inject, provide, injectionToken, input, signal } from '@angular/core';\n\n/**\n * Not provided in root by default: throws if not provided\n * in the injector tree.\n *\n * factory = default factory used by the provide(compToken)\n * shorthand — not a fallback\n */\nconst compToken = injectionToken({\n  debugName: 'compToken',\n  factory: () =\u003e {\n    const counter = signal(0);\n\n    return {\n      value: counter.asReadonly(),\n      decrease: () =\u003e {\n        counter.update(v =\u003e v - 1);\n      },\n      increase: () =\u003e {\n        counter.update(v =\u003e v + 1);\n      },\n    };\n  },\n});\n\n/**\n * Auto-provided: factory invoked once at root scope —\n * no need to provide it explicitly\n */\nconst rootToken = injectionToken({\n  debugName: 'rootToken',\n  autoProvided: true,\n  factory: () =\u003e {\n    const counter = signal(0);\n\n    return {\n      value: counter.asReadonly(),\n      decrease: () =\u003e {\n        counter.update(v =\u003e v - 1);\n      },\n      increase: () =\u003e {\n        counter.update(v =\u003e v + 1);\n      },\n    };\n  },\n});\n\n/**\n * Token without factory: must use provide(token, factory)\n * with an explicit factory. provide(otherCompToken) shorthand\n * is a compile-time error.\n */\nconst otherCompToken = injectionToken\u003cstring\u003e({ debugName: 'otherCompToken' });\n\n/**\n * Multi token with factory: provide(multiToken) shorthand uses\n * this factory — not a root default entry.\n */\nconst multiToken = injectionToken.multi({\n  debugName: 'multiToken',\n  factory: () =\u003e Math.random(),\n});\n\nclass Store {}\n\nexport const Counter = component({\n  bindings: {\n    initialValue: input\u003cnumber\u003e(),\n  },\n  setup: () =\u003e {\n    const rootCounter = inject(rootToken);\n    const compCounter = inject(compToken);\n    const multi = inject(multiToken); // array of numbers\n    const store = inject(Store);\n    /** ... **/\n    return (...);\n  },\n  providers: ({ initialValue }) =\u003e [\n    // provide compToken using the default factory (shorthand)\n    provide(compToken),\n\n    // multi with factory: shorthand works\n    provide(multiToken),\n    provide(multiToken),\n    provide(multiToken, () =\u003e 10),\n    provide(multiToken, () =\u003e initialValue()),\n\n    // token without factory: must use explicit factory form\n    provide(otherCompToken, () =\u003e ''),\n    \n    // class\n    provide(Store, () =\u003e new Store()),\n  ],\n});\n```\n\n## Final considerations\n\n### Concepts Impacted by These Changes\n\n- `ng-content`: replaced by `fragments`,\n- `ng-template` (`let-*` shorthands + `ngTemplateGuard_*`): likely replaced by `fragments`,\n- structural directives: likely replaced by `fragments`,\n- `pipes`: replaced by derivations — derivations cover the same transform use case and also support DI,\n- `event delegation`: not explicitly considered, but it could fit as \"special attributes\" (`onClick`, ...) similarly to [Solid events](https://docs.solidjs.com/concepts/components/event-handlers),\n- `@let`: unchanged,\n- `bindings aliasing` at the setup level (ts destructuring),\n- `directives` attached to the host (components): no longer possible, but directives can be passed in and attached to elements,\n- `directive` types: since `host` is declared as a typed `ref` at the directive config level, static type checking is built in. For native tags, the target element type comes from `IntrinsicElements`, so directives can only be applied to compatible elements,\n- `template reference variables`: likely replaced by `ref`,\n- `queries`: likely replaced by `ref`; `ref` should be extended to cover programmatic component creation, but must not allow arbitrary `read` of providers from the injector tree (see [`viewChild abuses`](https://stackblitz.com/edit/stackblitz-starters-wkkqtd9j)),\n- `component and directive injection`: the preferred interaction model is an explicit `ref` passed as an `input`. Nevertheless, with `ref`/`expose` in place, component and directive injection are safer by design — directive-to-directive and child-to-parent injection are established patterns worth keeping (see [`ngModel hijacking`](https://stackblitz.com/edit/stackblitz-starters-ezryrmmy) for the kind of abuse `expose` helps prevent). The trade-off is that some Angular-reserved names are necessary (`children`);\n- `interface conformance`: opt-in via `satisfies` on `bindings` and `expose` — the same structural check that `implements` provides for classes.\n\n### Notes\n\n- other decorator properties: in this proposal, components and directives expose only `providers` and `setup` entries. However, `@Component` and `@Directive` have many more properties, some of which (like `preserveWhitespaces`) should probably remain. They are not covered here to avoid scope creep;\n- `providers` defined at `directive` level: the added value is unclear, but the confusion they generate is well-documented; it is uncertain whether this concept remains meaningful;\n- inputs and outputs can be reassigned inside the setup:\n  - `https://github.com/microsoft/TypeScript/issues/18497`,\n  - [`no-param-reassign`](https://eslint.org/docs/latest/rules/no-param-reassign).\n\n### Pros and cons\n\nPros:\n\n- familiar enough,\n- not subject to typical single-file component (SFC) limitations,\n- enforces a strict structure,\n- AI agent-friendly,\n- no `splitProps` drama 😅.\n\nCons:\n\n- noticeable repetition in how bindings are declared and consumed,\n- not plain TypeScript.\n\n---\n\n## Appendix: Co-located templates in Angular via `.ng` files\n\n`tsx` does not support Angular control flow/directives today, so co-located templates likely require an Angular DSL in `*.ng` files plus dedicated tooling/parser support.\n\nThis is not only syntax preference: if co-location becomes default, losing `templateUrl` would be a regression for some teams. The intent is co-location without weakening Angular's structural model.\n\nKey goals:\n- template and setup live in the same lexical scope,\n- tooling and agents get stable structural markers (`component`, `directive`, `derivation`, `fragment`),\n- bindings remain explicit and statically typed,\n- provider declarations remain separate from setup/template logic,\n- providers can depend on inputs, but not on setup-local variables,\n- component internals stay private — only what `expose` returns is reachable through `ref`.\n\nThis keeps the explicit contract model:\n- `bindings` remain the canonical public API surface,\n- Angular performs synchronization/wiring,\n- strict checks happen at build time,\n- `setup` runs once at component creation.\n\nInterface conformance for `bindings` and `expose` stays opt-in via `satisfies`.\n\n### Boilerplate tax — a known trade-off\n\nDeclaring a binding and then destructuring it in `setup` feels redundant for small components. This is a known tax of the format.\n\n```ts\n// Tiny — the tax is visible: ~5 lines of bindings for ~3 lines of logic\nexport const Badge = component({\n  bindings: {\n    label: input.required\u003cstring\u003e(),\n    variant: input\u003c'info' | 'warn'\u003e('info'),\n  },\n  setup: ({ label, variant }) =\u003e (\n    \u003cspan class={variant()}\u003e{label()}\u003c/span\u003e\n  ),\n});\n\n// Medium — the same tax is a small fraction of the overall code\nexport const DataTable = component({\n  bindings: {\n    rows: input.required\u003cRow[]\u003e(),\n    selected: model\u003cRow | null\u003e(),\n    sort: output\u003cSortEvent\u003e(),\n    rowTemplate: fragment\u003c[Row]\u003e(),\n  },\n  setup: ({ rows, selected, sort, rowTemplate }) =\u003e {\n    const sorted = linkedSignal(() =\u003e defaultSort(rows()));\n    const filter = signal('');\n    const filtered = computed(() =\u003e applyFilter(sorted(), filter()));\n    // ... 30+ lines of logic, handlers, derived state\n    return (...);\n  },\n});\n```\n\nFor medium and large components the binding declaration is a small fraction of the code, and the explicit contract pays for itself in readability, refactorability, and tooling support.\n\nThree additional points:\n- **Fairer comparison with other frameworks.** In React or Solid with TypeScript you typically write a separate `Props` interface that mirrors the component's accepted inputs — pure type-level boilerplate. Here, `bindings` serves double duty as both the type declaration *and* the runtime wiring. Counting the `Props` interface other frameworks require makes the math considerably more even.\n- **Multi-component co-location.** Traditional SFCs (Vue, Svelte, etc.) map one component to one file. Splitting a growing component means creating a new file, moving markup, wiring imports, and updating the module graph — even for small, tightly coupled pieces. `.ng` files let you define helper components, fragments, and directives in the same file and extract them only when they earn their own module boundary.\n- **Why not `defineBindings(...)` inside `setup`?** It would reduce repetition, but `providers` needs input access *before* `setup` runs — so it would require compiler hoisting magic or giving up input access in providers. It also introduces a second authoring style (à la Vue Options vs. Composition API) that tooling, docs, and developers all have to support.\n\nOne authoring format, explicit bindings, keeps the mental model simple — for humans and AI agents alike.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmauriziocescon%2Fng-next","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmauriziocescon%2Fng-next","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmauriziocescon%2Fng-next/lists"}