Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ jobs:
name: Lint, Test & Build
runs-on: ubuntu-latest

env:
# The type-aware ESLint pass (and tsc/vitest) exceed the runner's
# default Node heap on this codebase; raise it to avoid OOM (exit 134).
NODE_OPTIONS: --max-old-space-size=8192

steps:
- name: Checkout code
uses: actions/checkout@v6
Expand Down
22 changes: 21 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions context/diagrams/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ watcher or expression never crashes the loop.
| [DOM compiler ($compile)](./compile.md) | `$compile(element)` walk → directive collect/sort → compile → three-phase link (pre/child/post); the controller seam, transclusion, isolate bindings, text/attr interpolation, `templateUrl`, errors via `$exceptionHandler('$compile')` |
| [Built-in directives](./built-in-directives.md) | The shared directive mechanism (restrict / priority / compile / link / scope kinds) plus per-category sub-sections: structural, visibility & binding, class & style, attribute helpers, events, pluralization, CSP/template-cache/element overrides |
| [Routing ($location + ngRoute)](./routing.md) | `$location` digest sync + cancelable `$locationChange*` events, the `$route` navigation pipeline (match → redirect → Start → template/resolve → commit), `$routeParams`, `ngView` render |
| [Animations ($animate + ngAnimate)](./animate.md) | The `$animate` façade → `$$animateQueue` engine seam (instant core vs the opt-in `ngAnimate` full engine), the JS / CSS drivers, the pre-handled runner promise, the digest-end flush, `enabled` / `classNameFilter` / `on` / `off`, stagger + `ng-animate-children` |

## Maintenance

Expand Down
164 changes: 164 additions & 0 deletions context/diagrams/animate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Animations ($animate + ngAnimate)

## Purpose

`$animate` is the animation façade every built-in structural directive and class
toggler routes its DOM-visible changes through: `enter` / `leave` / `move` for
insertion / removal / reposition, `addClass` / `removeClass` / `setClass` for
class changes. It is ALWAYS available on core `ng`, wired by default to the
synchronous **instant engine** (`$$animateQueue` = `createCoreAnimateQueue`) —
every operation applies its end state immediately and resolves at once. Adding
the opt-in **`ngAnimate`** module re-registers `$$animateQueue` with the full
animation engine (DI last-wins — the override seam), and the same operations
become animated: the JS driver runs registered `.animation` callbacks, the CSS
driver runs the `ng-enter` / `-active` class choreography, a runner carries the
completion promise, and `on` / `off` listeners observe start / close.

## Collaborators & call order

```text
directive call site (ng-if / ng-repeat / ng-switch / ng-include / ng-view;
ng-show / ng-hide / ng-class family; forms state-classes.ts)
│ $animate.enter/leave/move · addClass/removeClass/setClass
▼
┌──────────────────────────────────────────────────────────────────┐
│ $animate façade (createAnimate) — normalize Element|Node[] → │
│ readonly Node[], delegate to $$animateQueue.push(nodes, event, …) │
└──────────────────────────────┬───────────────────────────────────┘
▼
┌────────────── which $$animateQueue did DI resolve? ──────────────┐
│ │
▼ core ng (instant) ▼ ngAnimate (full engine)
┌───────────────────────────┐ ┌──────────────────────────────────────────┐
│ createCoreAnimateQueue │ │ createAnimateQueue │
│ enter/move → insertNodes │ │ enter/move insert SYNC (layout correct) │
│ leave → removeNodes │ │ shouldAnimate(el)? gates: │
│ add/remove/set → classList│ │ startup-grace · enabled() · subtree · │
│ q.resolve(undefined) │ │ ng-animate-children · classNameFilter │
│ on/off = documented NO-OP │ │ coalescer: pending-batch matrix │
└───────────────────────────┘ │ (class ops → 1 setClass; net-empty DROP;│
│ structural BEATS class; later SUPERSEDES)│
│ cancel in-flight active record (WeakMap) │
│ OR-match the two drivers ─────────────┐ │
└────────────────────────────────────────┼───┘
│
┌──────────────────────────────┬──────────────────────────────┘
▼ ▼
┌───────────────────┐ ┌──────────────────────────────────────┐
│ js-driver.ts │ │ css-driver.ts │
│ match el.classList │ │ match: probe prep+active, read timing │
│ vs .animation regs │ │ (zero duration → null / skip) │
│ start(done): run │ │ start: +ng-animate +prep → reflow → │
│ per-op callbacks │ │ raf → +active → wait max(delay+dur) │
│ throw → '$animate' │ │ transitionend / fallback timer; │
│ cancel() fns │ │ stagger offset = idx × step │
└─────────┬──────────┘ └───────────────────┬──────────────────┘
└─────────── combineDriverAnimations ──────┘
│ (both matched → joint close)
▼
digest-end flush: $$postDigest(fn) → raf(fn) — ONE per batch
│ assign stagger indices per same-event group
▼
startAnimation(item): addElementCleanup(el, runner.end)
│ fireStart → events.notify('start') (Slice 9)
│ animation.start(closeAnimation, staggerIndex)
▼
closeAnimation: applyEndState (leave removes / classes apply) →
│ structurallyAnimating.delete → runner.complete()
▼
┌──────────────────────────────────────────────────────────────────┐
│ animate-runner.ts — $q-backed handle │
│ promise (PRE-HANDLED: no-op reject follow-up at construction) │
│ complete() / end() → resolve · cancel() → reject 'cancelled'│
└──────────────────────────────────────────────────────────────────┘
│ resolve/reject schedules a digest via $q → $evalAsync
▼
caller's .then / .catch runs on the scheduled digest turn
```

Collaborators: **`$q`** (the runner deferred; every method's returned promise),
**`$rootScope`** (`$$postDigest` for the flush, `$$phase`-guarded `$apply` /
`$evalAsync` for listener dispatch, startup grace), **`$injector`** (resolving
registered `.animation` factories once, lazily, via the `$$animateRegistry`
bridge), **`$exceptionHandler`** (JS callback throws, factory-resolution
failures, listener throws — all cause `'$animate'`, the 14th token), the
**`raf`** / **`computeStyle`** / **`setTimer`** globals (seam-bound by
`ng-animate-module.ts`), and the compiler's **`addElementCleanup`** contract
(destroy-mid-animation finalization). A cancelled runner nobody observes is
pre-handled, so it NEVER reaches `$q`'s unhandled-rejection channel;
`EXCEPTION_HANDLER_CAUSES` grows to 14.

## Using it the primary way

The ESM-first API: `createAnimate` / `createCoreAnimateQueue` /
`createAnimateQueue` / the two drivers / `createAnimateRunner` are pure
factories with injected seams — usable (and testable) without an injector or a
real browser.

```typescript
import { createAnimate, createCoreAnimateQueue } from 'my-own-angularjs/animate';

// The instant engine — every op applies synchronously, resolves at once.
const $animate = createAnimate({ queue: createCoreAnimateQueue({ q }) });

void $animate.enter(clone, parentEl, placeholder).then(() => {
// runs after the enter completes (immediately under the instant engine)
});
$animate.addClass(el, 'active'); // classList.add now; promise settles next digest
```

```typescript
import { createAnimateQueue, createJsDriver, createCssDriver } from 'my-own-angularjs/animate';

// The full engine with stubbed seams — unit-testable in jsdom.
const queue = createAnimateQueue({
q,
exceptionHandler,
postDigest: (fn) => fn(),
raf: (fn) => fn(),
jsDriver: createJsDriver({ getAnimations, exceptionHandler }),
cssDriver: createCssDriver({ raf, now, computeStyle, setTimer, clearTimer }),
classNameFilter: () => null,
});
```

## Using it the dependency-injection way

`$animate` is registered on the core `ng` module (wired to the instant engine).
`ngAnimate` is OPT-IN: compose it alongside the core and declare `'ngAnimate'`
in the app's deps chain — DI last-wins re-registers `$$animateQueue` with the
full engine, upgrading `$animate` automatically. No decorator, no lazy probe.

```typescript
import { createInjector, createModule } from 'my-own-angularjs';
import { ngModule } from 'my-own-angularjs/core';
import { ngAnimate } from 'my-own-angularjs/animate';

const app = createModule('app', ['ng', 'ngAnimate']).animation('.fade', [
() => ({
enter(element, done) {
element.classList.add('fading-in');
done();
},
}),
]);

const injector = createInjector([ngModule, ngAnimate, app]);
const $animate = injector.get('$animate');
const $rootScope = injector.get('$rootScope');

$rootScope.$apply(() => (scope.show = true)); // ng-if enters
// under ngAnimate the ceremony starts after this digest settles + one raf tick
```

`$animateProvider.classNameFilter(/animate-me/)` and
`$animateProvider.register('.slide', factory)` configure the service in a
`config()` block; `ng-animate-children` in compiled markup opts a container's
descendants back into animating under a structural parent.

## Related diagrams

- [Scopes & digest cycle](./scope-and-digest.md) — the `$$postDigest` flush and `$evalAsync` digest scheduling the runner promise leans on (`$q` backs the runner; its unhandled-rejection channel is what pre-handled promises bypass)
- [Built-in directives](./built-in-directives.md) — the structural / class-toggling directives that call `$animate`
- [DOM compiler ($compile)](./compile.md) — the `addElementCleanup` contract that finalizes a destroyed animation
- [Diagram index](./README.md)
10 changes: 5 additions & 5 deletions context/product/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ _Features that complete the full framework experience._
- [x] **ng-view:** Implement the view directive that renders route templates. _(spec 040 — `restrict: 'ECA'`, `transclude: 'element'`; renders `locals.$template` into a wrapper `<div>` against a fresh child scope with the per-route controller (resolve locals injectable by name, controller stashed for `require: '^ngController'`); emits `$viewContentLoaded`; render throws route via the existing `'$compile'` cause.)_
- [x] **Route Lifecycle:** Support `resolve`, route change events (`$routeChangeStart`, `$routeChangeSuccess`, `$routeChangeError`), and `$routeParams`. _(spec 040 — cancelable `$routeChangeStart`; `resolve` + template aggregated through one object-keyed `$q.all` with a staleness token (inline-template/no-resolve routes commit synchronously — a documented divergence); `$routeParams` repopulated in place; plus `redirectTo` (event-silent, `.replace()`, hardened with a 10-hop loop guard upstream lacks), `reloadOnSearch`/`reloadOnUrl` + `$routeUpdate`, `reload()`, and `updateParams()`.)_

- [ ] **Animations**
- [ ] **$animate Service:** Implement animation hooks for `enter`, `leave`, `move`, `addClass`, `removeClass`.
- [ ] **CSS Animations:** Support CSS transition and keyframe-based animations triggered by directive lifecycle.
- [ ] **JavaScript Animations:** Support programmatic animation definitions via `$animateProvider.register`.
- [ ] **Module DSL `.animation(name, fn)`:** Expose `.animation` on `createModule(...)` as a thin wrapper over `$animateProvider.register` — ng-module parity, shared registry, no duplicated state.
- [x] **Animations** _(spec 041 — shipped.)_
- [x] **$animate Service:** Implement animation hooks for `enter`, `leave`, `move`, `addClass`, `removeClass`. _(spec 041 — opt-in `ngAnimate` module upgrading an always-on core `$animate` façade + instant engine via the DI last-wins `$$animateQueue` seam; plus `enabled()`/`classNameFilter`/startup grace, completion promises, cancel/join, `on`/`off` listeners, staggering, `ng-animate-children`.)_
- [x] **CSS Animations:** Support CSS transition and keyframe-based animations triggered by directive lifecycle. _(spec 041 — parity `ng-enter`/`-active` + `<cls>-add`/`-active` class conventions, `ng-animate` marker, injectable `computeStyle`/`raf`/timer seams.)_
- [x] **JavaScript Animations:** Support programmatic animation definitions via `$animateProvider.register`. _(spec 041.)_
- [x] **Module DSL `.animation(name, fn)`:** Expose `.animation` on `createModule(...)` as a thin wrapper over `$animateProvider.register` — ng-module parity, shared registry, no duplicated state. _(spec 041 — config-block sugar forwarding to `$animateProvider.register`, the `.filter` precedent.)_

- [ ] **Package & Distribution**
- [ ] **npm Package:** Bundle and publish as an installable npm package with full TypeScript type declarations.
Expand Down
Loading