angular

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars181
mentions26
0481216Oct 4Oct 18Nov 2
UTC days · repositories angular owns9 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-18

npm60M
015M30M45M60MOct 4Oct 18Nov 2
Oct 28npm: 1/11 packages observed

Across 11 known packages in repositories angular owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

angular’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside angular’s account where angular opened or reviewed pull requests in the past 365 days.

Who noticed angular

Everyone who starred or forked angular’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

27posts
Nov 31 post
  1. Inside Angular’s Change Detection Zones: How UI Updates Are Scheduled and Batched You’ve just fixed a bug where your Angular component’s UI didn’t update right after an HTTP request. You refresh, hoping the input reflects the latest data immediately. But nope, the UI lags, or requires a second click, or only updates after you interact again. What gives? I’ve been there. It feels like Angular’s change detection is on its own schedule, ignoring your updates. Tracking down the root cause led me straight into Angular’s NgZone and how it intercepts asynchronous operations to schedule UI updates. The Familiar Developer Frustration: UI Doesn’t Update When You Think It Should Imagine this: you call a service that fetches data with HttpClient. You subscribe, set a component property in the callback, and expect Angular to refresh the template. this.http.get('/api/data').subscribe(data => { this.user = data; // Expect UI to update immediately }); But sometimes the UI stays stale. Or, if you’re using third-party libraries or custom async code, change detection doesn’t run automatically. You might resort to triggering it manually with ChangeDetectorRef.detectChanges() or NgZone.run(). That’s a red flag something subtle is happening under the hood. What Is NgZone and Why Does Angular Use It? Angular wraps your app’s execution inside an NgZone, an Angular-specific zone based on the zone.js library. Zones monkey-patch async browser APIs like setTimeout, promises, events, and XHR. This lets Angular know when async work starts and finishes. Why does it matter? Because Angular’s change detection is not running constantly or on every microtask. Instead, Angular waits for these async activities to complete and then triggers change detection once per turn of the event loop. This batching avoids excessive UI work. In short, NgZone is Angular’s way to track async boundaries and schedule UI updates efficiently. How NgZone Intercepts Asynchronous Operations Behind the scenes, zone.js patches these APIs: - Timers: setTimeout, setInterval - Promises: .then() callbacks - DOM events: addEventListener - XHR and fetch Each async callback runs inside the NgZone. When an async task starts, NgZone increments an internal counter. When it completes, it decrements it. When the counter hits zero, NgZone knows the async turn is over and calls Angular’s change detection. This is why Angular’s change detection doesn’t run immediately at every microtask or callback, but batches updates until the async “turn” is done. What Happens When You Run Code Outside NgZone? Sometimes, to optimize performance, you or a library might run code outside Angular’s zone using NgZone.runOutsideAngular(). This means Angular doesn’t intercept those async callbacks. this.ngZone.runOutsideAngular(() => { setTimeout(() => { // Angular change detection won't run after this timer this.user = newUser; }, 1000); }); If you update component state inside such callbacks without re-entering Angular’s zone, your UI won’t update automatically. That’s a common source of confusion. The fix: wrap updates inside NgZone.run(), or trigger change detection manually. The Tradeoff: Performance vs Predictability NgZone’s interception is powerful but comes at a cost. Monkey-patching async APIs adds overhead, which can affect performance in large apps or complex async flows. Because Angular batches change detection after all async tasks finish, UI updates might feel delayed if you have chained or long-running async operations. Some experiments and libraries aim to avoid zones entirely (like Angular Signals or manual change detection strategies) for more control and performance, but zones remain the default for good developer ergonomics. Examples: When Change Detection Runs Example 1: Simple Promise Promise.resolve().then(() => { this.count++; // Angular will detect changes after this microtask finishes }); Angular patches promise .then(), so after the microtask queue empties, change detection runs. Example 2: setTimeout setTimeout(() => { this.count++; // Angular detects changes after timer callback }, 100); NgZone tracks timers similarly, triggering change detection once the callback completes. Example 3: Event Listener document.getElementById('btn').addEventListener('click', () => { this.count++; // UI updates because event listener runs inside NgZone }); Events patched by zone.js trigger change detection after handlers. Example 4: Outside Angular Zone this.ngZone.runOutsideAngular(() => { setTimeout(() => { this.count++; // UI stays stale because this runs outside Angular zone }, 100); }); No automatic change detection unless you re-enter Angular zone or detect changes manually. Debugging Unexpected UI Update Timings If your UI doesn’t update as expected: - Check whether your async code runs inside NgZone. - Use console.log(NgZone.isInAngularZone()) inside callbacks to verify. - Try forcing change detection temporarily with ChangeDetectorRef.detectChanges() to isolate the problem. - Beware of third-party libraries that operate outside Angular zones. When Change Detection Might Feel Too Aggressive Because NgZone triggers detection after every async turn, sometimes Angular runs more often than you want, especially with frequent timers or event-heavy components. You can optimize with: - ChangeDetectionStrategy.OnPush to reduce detection scope - Running complex code outside Angular zones if UI updates aren’t needed every time - Using signals or other reactive primitives for fine-grained updates Wrapping Up NgZone is Angular’s secret sauce for knowing when async work finishes so it can batch and schedule change detection. It patches your async browser APIs, tracks when tasks start and finish, and triggers UI updates accordingly. This makes Angular apps feel reactive without you having to call detectChanges() everywhere. But it can also surprise you when third-party async code runs outside Angular’s zone, or when UI updates lag behind your expectations. Understanding NgZone’s role helps you debug those subtle UI update glitches and write more predictable, performant Angular apps. --- Helpful learning resources - MDN Web Docs - web.dev performance guidance - OpenAI developer resources Originally published at Under The Hood. Get the next deep dive in your inbox: subscribe to Under The Hood.

  2. Angular 22+ projects are having an issue in Stackblitz currently, but while the larger issue is being fixed, try this workaround that Material added to their own starter: components#34030. Larger issue: angular-cli#34524. The workaround worked for my own projects.

    3 likes
  3. Angular Signal Forms with an Event-Driven NgRx Signal Store Angular 22 landed in June, and the headline in it for me is that Signal Forms are stable. They were experimental in v21, they are not any more, and they ship with Angular Material and Angular Aria support. They are worth your attention for 3 things: 1. The model is a plain signal you own, so the data and the form are never two copies of the same thing drifting apart. Field access is typed off that model: form.title replaces former form.get('title'), and a typo becomes a compile error instead of an undefined at runtime. 2. Validation lives in a schema, next to the shape it describes, with the error copy on the rule instead of endless template @if chains. 3. Every piece of form state is a signal, from value and errors to touched, dirty, pending and submitting, so nothing needs subscribing to and ordinary inputs need no ControlValueAccessor anymore. So it's time to try them out. The form we'll convert belongs to a small task board which features three columns and a form to add a task, backed by a mock API. The first article put the board's state in a method-based NgRx Signal Store, and the second moved that store to the events plugin, where a component dispatches an event and an effect answers with another one. The form itself was still built with our good old friend FormBuilder, and in this post we move it to Signal Forms. Most of that is straightforward: fb.group becomes form(), Validators become schema rules, formControlName becomes [formField]. Submitting is the part that isn't, because this form submits to an event-driven NgRx Signal Store, and that's where most of this post goes. Full code is in the repository linked at the end. The Model Is a Signal You Own Declaring the Form A Signal Form starts from data, not from controls. Our task form needs a title, a description and the column the task starts in, so that will be our model: a plain object in a signal, handed to form() together with the rules that define it. Only the title has rules, since it is required and needs three characters: const EMPTY_DRAFT: NewTask = { title: '', description: '', status: TaskStatus.TODO, }; readonly draft = signal({ ...EMPTY_DRAFT }); readonly taskForm = form(this.draft, path => { required(path.title, { message: 'Title is required' }); minLength(path.title, 3, { message: 'Title must be at least 3 characters' }); }); With FormBuilder the values used to live inside the FormGroup, and we were only able to reach them through the form. Now they live in a signal we declare ourselves, draft, and form() builds the fields on top of it without taking a copy. this.draft stays a normal WritableSignal that the rest of our component can read and write. Typing in the bound input writes straight back into it, and setting it updates the input, so there's no patchValue, no form.value, and no moment where the two disagree. > 💡 This is the part that changes how a form feels. The model is the source of truth and the form is just a view over it, rather than the form owning a copy that we have to keep syncing. In the repository the two rules live in one reusable schema() that the form applies with apply(path.title, taskTitleSchema), so a second form, like the board's edit form in the next post, can share it. The inline version above is the same thing, spelled out. Typed Field Access taskForm is a field tree shaped like our model. taskForm.title exists because title is on the model, and it's typed as a string field, so if we rename the property the template will stop compiling, which is the part get('title') could never do for us. Nested models nest too. A { address: { city: string } } model gives us taskForm.address.city, typed all the way down, and applyEach applies a schema to every item of an array field. Reading Field State Calling a field, as in taskForm.title(), gives us its state, and every member of that state is a signal: taskForm.title().value(); // 'Clean the kitchen' taskForm.title().errors(); // [{ kind: 'minLength', message: '…' }] taskForm.title().touched(); // false until the user has been near it taskForm.title().valid(); // and invalid(), dirty(), pending(), required() taskForm().submitting(); // on the root, while a submit is in flight value is a writable signal, so taskForm.title().value.set('…') works whenever we need to update a value directly. > 💡 Errors are computed whether or not the user has touched the field. An empty required field is invalid from the moment the form exists. touched() is what decides whether we show anything, which matters later when we reset. Validation in a Schema The second argument to form() is a schema function. It receives a path tree mirroring our model, and we attach rules to paths, so a rule sits next to the shape it describes rather than in an array on a control constructor. Built-in Validators The built-in validators cover the common use cases: - required, email, pattern - min and max for numbers, minDate and maxDate for dates - minLength and maxLength for strings and collections Each one takes an options object where message is the string our template renders, so the error copy lives with the rule instead of in an @if chain keyed on errors?.['minlength']. Custom and Async Rules For any use case the built-in validators don't cover, there is validate, returning an error object or null: validate(path.title, ({ value }) => value().trim().toLowerCase() === 'untitled' ? { kind: 'placeholder', message: 'Give it a real name' } : null ); Async rules get validateAsync and validateHttp, and the field's pending() signal returns true while they run, so a spinner on a username check is a single binding rather than component state we'd have to maintain by hand. Reusing a Zod Schema The one I'd use on a bigger form is validateStandardSchema. Signal Forms accept anything implementing Standard Schema, so a Zod or Valibot schema we already maintain for our API types can validate the form too, instead of being restated as form rules: form(this.draft, path => validateStandardSchema(path, TaskDraftSchema)); Our task board is small enough that the built-ins are the right call, so that stays a pointer rather than something this post demonstrates. Conditional Rules Validators aren't the only thing the schema function holds. The same path tree carries conditional logic: - disabled, readonly and hidden take a predicate over the model - applyWhen scopes a whole block of rules to a condition - debounce holds a field's input back from the model, and so from validation, until the user pauses or blurs So "required only when a checkbox is on" stops being a subscription in ngOnInit. Binding Fields in the Template The FormField Directive One single directive connects a field to a control. Here is the title input of our task form, its error block and the submit button: <input type="text" [formField]="taskForm.title" class="form-control" /> @if (taskForm.title().touched() && taskForm.title().invalid()) { <div class="error-message"> @for (error of taskForm.title().errors(); track error.kind) { <span>{{ error.message }}</span> } </div> } <button type="submit" [disabled]="taskForm().invalid() || taskForm().submitting()"> @if (taskForm().submitting()) { Adding... } @else { Add Task } </button> And here is how it works: 1. [formField]="taskForm.title" is the whole binding. No formControlName, and no ControlValueAccessor for a standard input. 2. The error block reads touched() and invalid() together, so nothing shows until the user has been in the field, and then every entry in errors() renders with the message from its rule. 3. The button's disabled state reads invalid() and submitting() off the root field, instead of a component boolean we'd have to keep in sync. Custom Controls Without a ControlValueAccessor [formField] binds a plain <input> with no extra code. A custom component of our own is a different story, and our form has one: the status in the model is picked with 3 buttons, one per column, since until now every new task had to start in To Do. With reactive forms that picker would implement ControlValueAccessor. With Signal Forms it implements FormValueControl<T> instead, and that contract is much smaller: the only required member is a model() called value. @Component({ selector: 'app-status-picker', /* … */ }) export class StatusPickerComponent implements FormValueControl<TaskStatus> { readonly value = model(TaskStatus.TODO); readonly disabled = input(false); readonly touch = output<void>(); readonly options = [ { status: TaskStatus.TODO, label: 'To Do' }, { status: TaskStatus.IN_PROGRESS, label: 'In Progress' }, { status: TaskStatus.DONE, label: 'Done' }, ]; } The template is a row of buttons that reads and writes value: <div role="group" aria-label="Starting column" (focusout)="touch.emit()"> @for (option of options; track option.status) { <button type="button" [attr.aria-pressed]="value() === option.status" [disabled]="disabled()" (click)="value.set(option.status)"> {{ option.label }} </button> } </div> And the task form binds it exactly like the input above: <app-status-picker [formField]="taskForm.status" /> What each member does: 1. value is two-way bound to taskForm.status. A click calls value.set(), which writes into draft, and resetting the form to the empty draft moves the pressed button back to To Do. 2. disabled is optional. Because we declared it, a disabled rule on the status field greys the buttons out with no extra binding. 3. touch is optional too. Emitting it marks the field touched, and ours emits when focus leaves the group, the same moment a native input would. Everything beyond value is optional. Anything we do declare is kept in sync with the field's state for us: errors, disabled, readonly, hidden, invalid, pending, touched, dirty, required, and the constraint values min, max, minLength, maxLength and pattern. Checkboxes implement FormCheckboxControl instead, which swaps value for checked. > 💡 This is the part to check thoroughly before migrating a design system. A component library only works with Signal Forms once it implements this contract, and a ControlValueAccessor written for reactive forms does not carry over. What Replaces ngSubmit One thing to know when we drop ReactiveFormsModule: (ngSubmit) goes with it, so we have 2 options. - Handle (submit) ourselves and call preventDefault() - Pass submission options into form() and use <form [formRoot]="taskForm">, which hosts novalidate and calls preventDefault() for us The examples below take the first route, since it keeps the submit logic in the component where it's easier to read. The Form This Replaced For contrast, here is what our task board had, and what the 2 previous articles have shown: taskForm: FormGroup = this.fb.group({ title: ['', [Validators.required, Validators.minLength(3)]], description: [''], }); createTask() { if (this.taskForm.invalid) return; this.dispatch.taskCreated({ title: this.taskForm.get('title')?.value, description: this.taskForm.get('description')?.value, status: TaskStatus.TODO, }); this.taskForm.reset(); } It works, but it has no idea whether the task was created. dispatch returns nothing, so the method ends, the form clears, and a failed API call reaches the user as a line in the console. Signal Forms have an answer for that, and it's where the conversion stopped being a straight swap. Submitting, and What It Expects of You The submit() Contract submit() takes the form and an action it can await. Errors the action returns get routed onto the fields we name: await submit(this.taskForm, async f => { const result = await saveTask(f().value()); if (result.ok) return; return { kind: 'taken', message: 'That title is taken', fieldTree: f.title }; }); It does 3 more things for us: - Marks the whole tree touched, so every outstanding error becomes visible at once - Refuses a second submit while one is in flight - Drives submitting() throughout, which is what the button binding above reads For a component that calls an HTTP service directly, that's the whole story, and it's a real improvement on wiring those states by hand. Where an Event-Driven Store Breaks the Assumption submit() assumes submitting is a function that eventually resolves. In my previous article our task board moved to the NgRx Signal Store events plugin, where a component never calls a store method. It dispatches an event, a reducer updates state, and an effect performs the side effect and dispatches another event with the result. That's where submit() gets stuck. dispatch.taskCreated(...) returns void, and the outcome turns up later, elsewhere, as taskCreatedSuccess or taskCreatedFailure. There's nothing to await. When Binding Is Enough Not every form needs to await, though. If the template binds to the store's loading and error state, the button can disable itself and an alert can show the failure, and the component never awaits anything. For a lot of forms that is enough, and submit() isn't needed. Our form wants more than that. It wants submitting() to drive the button, and it wants a duplicate title to land under the title input instead of in an alert at the top of the board. Both of those come from submit(), so the form needs something it can await. To my knowledge, neither Angular nor NgRx ships a helper that turns an event round trip into a promise. When someone asked for an rxjs-flavoured submit in angular/angular#65199, Angular declined, since the existing RxJS APIs already cover it. On the NgRx side, the answers so far point at the store: - In ngrx/platform#5053, the recommendation for Signal Forms with a SignalStore is that the form calls a store method on submit - In ngrx/platform#4980, which asks for a promise from rxMethod, the maintainer's answer is a regular method that returns a promise - Full-Cycle Reactivity in Angular shows that method being a mutation from @ngrx-toolkit/core That sounds like the opposite of how an event-driven store works, since components there dispatch events instead of calling methods. It doesn't have to be, as long as the method keeps one rule. A Mutation That Speaks Events The rule worth keeping is that the reducer is the only thing that changes tasks. A mutation can keep that rule, as long as it reports its outcome as the same API events the effect used to. The Mutation The taskCreated page event and the createTask$ effect that answered it in the previous article are gone. In their place our store gets a createTask mutation: withMutations(store => ({ createTask: rxMutation<NewTask, Task>({ operation: (draft: NewTask) => store._taskService.createTask(draft), onSuccess: task => store._dispatcher.dispatch(taskApiEvents.taskCreatedSuccess(task)), onError: error => store._dispatcher.dispatch( taskApiEvents.taskCreatedFailure(errorMessage(error)) ), }), })), Step by step: 1. operation calls the service with the draft, status included, so the column picked in the form is the column the task lands in. 2. onSuccess and onError dispatch taskCreatedSuccess and taskCreatedFailure, so the existing reducer adds the task unchanged, and every result still shows up as an event. 3. Calling store.createTask(draft) returns a promise of { status: 'success' | 'error' | 'aborted' }, which is the thing submit() was missing. errorMessage is a two-line helper that pulls the message out of whatever the service rejected with and falls back to String(error). Both the mutation and the component use it, so the text under the field is the text the server sent. _taskService and _dispatcher are props the store injects with withProps: our task service, and the events plugin's Dispatcher from @ngrx/signals/events. The Component Now submit() has something to await: async createTask(event: Event) { event.preventDefault(); await submit(this.taskForm, async f => { const result = await this.store.createTask(f().value()); if (result.status === 'error') { return { kind: 'server', message: errorMessage(result.error), fieldTree: f.title, }; } this.taskForm().reset({ ...EMPTY_DRAFT }); return undefined; }); } We await the mutation, turn an error into a message with fieldTree: f.title, and reset on success. A rejected create now lands on the title field as a message under the input, the same way a minLength failure does. Resetting Without the Error Flashing Back Writing an empty draft into this.draft clears the form, but "Title is required" appears the instant it does. The field is still touched, and an empty touched title is required again. So we use taskForm().reset(value) instead, which clears touched and dirty along with the value. When I replaced reset() with a plain signal write, no test failed, so the suite now has one that checks touched() is cleared after a successful create. The Rollback That Was Already There The mutation only replaced the create path, so everything else our store did in the previous article still works, including the rollback when moving a card fails. Moving a task between columns is an optimistic update. The reducer applies the change the moment the page event arrives: on(taskPageEvents.taskStatusChanged, event => updateEntity( { id: event.payload.id, changes: { status: event.payload.status } }, { collection: 'task' } ) ), and reverts it when the API disagrees, using the previous status the component sent with the event, and says why in the board's alert: on(taskApiEvents.taskStatusChangedFailure, event => [ updateEntity( { id: event.payload.id, changes: { status: event.payload.previousStatus } }, { collection: 'task' } ), { error: event.payload.error }, ]), The mock fails every status change on task ID 3, so we can watch a card move and snap back. The form now reports its own failures the same way the board does, on a field instead of a column. > 💡 Events still run the board. Loading, moving a card and deleting still dispatch events, because nothing awaits them. If a form needs the outcome, it calls a mutation; if nothing waits, dispatch an event. Testing It To get a failure only the server can know about, the mock now rejects a duplicate title. This test checks that a server rejection ends up on the title field: it('routes a server rejection onto the title field', async () => { const existing = 'Clean the kitchen'; // seeded in HOUSEHOLD_TASKS component.draft.set({ title: existing, description: '', status: TaskStatus.TODO }); await component.createTask(new Event('submit')); const errors = component.taskForm.title().errors(); expect(errors.map(e => e.kind)).toContain('server'); expect(errors.map(e => e.message)).toContain('A task with this title already exists'); }); The test seeds the model straight through draft, submits the way the template would, and asserts on the title field's errors() rather than on anything the store exposes. It never touches the mutation directly, so it would pass unchanged against any other way of reaching the server. Rejecting duplicates broke an existing test, which created the same task twice to prove ids are unique, so it now creates 2 differently named tasks. When to Reach for Signal Forms Both form systems live in the same app. @angular/forms and @angular/forms/signals are separate entry points, so this is a per-form decision rather than a migration you have to finish. Use Signal Forms when: - You are on v22 and writing a new form, especially in a component that is already signal-based - The form edits data you already have a type for, like a Task, so a renamed or mistyped field fails at compile time instead of in the browser - You want a server error, like a duplicate title, to show under the field it belongs to without wiring that up by hand - You already keep a Zod or Valibot schema for the same shape and would rather validate against it once Stick with reactive forms when: - You are below v22, where the API was still experimental and moved between releases - You have a large body of existing forms, because there is no migration schematic and each one is a manual rewrite - You depend on third-party form controls that have not implemented FormValueControl yet - The form is a throwaway with one field and no validation, where either option is fine and the familiar one is cheaper And if the form submits into an event-driven store, give the store a mutation that dispatches its existing events. > 💡 I would not go back and convert working forms for the sake of it. The case for Signal Forms is strongest where a form has real validation and a real submit, because that is where the typed schema and the field-level error routing earn their keep. A 3-field contact form is not where you will notice the difference. Resources - Code for this article: View on GitHub (v4.1.0-signal-forms-custom-control) - Signal Forms documentation - NgRx Signal Store Events documentation - ngrx/platform#5053: Using Signal Store and Signal Forms - Full-Cycle Reactivity in Angular: Signal Forms, Signal Store, Resources, Mutation API - NgRx custom store features: Known TypeScript Issues, if signalStore() fails with No overload matches this call once a feature declares the state it needs. The fix is an unused generic on the feature, and the signals config of @ngrx/eslint-plugin flags any feature missing it. - Previous article: Event-Driven State Management with NgRx Signal Store (the events plugin this post submits into) - First article: Using NgRx Signal Store (the method-based store the board started with) How are you handling submit in a signal-based app? If you've kept a store purely event-driven and found a cleaner way to await the outcome than a mutation, I would like to see it.

  4. My pull Angular CLI pull request has been merged! 🎉 After three review rounds with Alan Agius, prerender.format is now part of Angular itself. I'm a little proud: a community request from 2024 that never collected enough votes is now an official option of the CLI. angular-cli#34180

    6 likes · 2 reposts
  5. С Karma на Vitest: как мы обновляли unit-тесты Angular и починили Allure Привет! На связи Дмитрий Чепайкин, старший Angular-разработчик в команде Klecks — платформы для разметки данных AI Центра Т-Банка. Долгое время unit-тесты фронтенда у нас крутились на Karma + Jasmine. Схема работала, пайплайн зеленый, переписывать «ради моды» не хотелось. Но в какой-то момент Karma официально признали deprecated, а корректная отгрузка отчетов в Allure перестала быть чем-то, на что можно положиться. Расскажу, как мы выбирали замену, почему сначала смотрели на Jest, а в итоге приняли Vitest — и как устроен итоговый сетап. Триггеры миграции Во фронтенде Klecks три тестовых проекта: customer, operator и shared. Кодовая база включает сотни *.spec.ts, использует Angular TestBed, моки сервисов, фейковые таймеры и кастомные хелперы вроде UnhandledCatcher. При этом интеграционные сценарии уже давно выполнялись на Playwright, а unit-тестирование продолжало опираться на классический Angular-стек: Karma через builder @angular-devkit/build-angular:karma, Jasmine в качестве тестового API и Chrome Headless для запуска в CI. Покрытие кода собиралось через karma-coverage, а результаты unit-тестов пытались выгружать в Allure вместе с e2e-тестами. Формально все работало. Но стоимость поддержки стека росла, а на Karma уже не решалась надежно одна из задач качества — нормальная иерархия unit-тестов в Allure. Поэтому стоял вопрос не «Нужен ли нам новый раннер?», а «Какой раннер позволит уйти с deprecated-инструмента и при этом не развалить существующие тесты и отчетность?». Зачем трогать то, что работает Karma справлялась со своими задачами годами. Руки до миграции не доходили не потому, что стек был идеален, а потому, что он был «достаточно хорош». Локально ng test открывал браузер, в CI Chrome Headless прогонял suites. Менять инфраструктуру при живом продукте — всегда риск. Мы собрали список проблем. Deprecated и тупиковая поддержка. Karma больше не развивается как основной современный test runner, поэтому дальнейшая интеграция с актуальным инструментарием требует все больше поддержки legacy-слоя. Оставаться на Karma — значит тащить устаревающий слой между CLI и тестами, который рано или поздно упрется в несовместимость с новой версией Angular/Webpack/Vite. Allure для unit-тестов. Для e2e у нас уже был рабочий контур Playwright → Allure. Для unit на Karma отчеты не собирались, так как не было стабильного плагина, полноты и стабильности в выгрузке артефактов тестов. В CI это превращалось в «тесты зеленые, а в Allure unit-слой неполноценный». Именно эта боль стала триггером: отчетность — часть процесса, а не опциональный бантик. Тяжелый браузерный рантайм в CI. Karma поднимает реальный Chrome. Для части DOM-тестов это плюс, но для сотен unit-спеков — лишняя тяжесть образа, флаки по браузеру и более дорогой пайплайн. Cypress-образ ради unit-прогона сам по себе сигнал, что инфраструктура разъехалась с задачей. Jasmine-специфика. API привычный, но экосистема вокруг моков, таймеров и матчеров все сильнее расходится с тем, что используют Jest/Vitest-мир и современные Allure-репортеры. Любая новая интеграция требовала прокладок под Karma и Jasmine, а не готового first-class-плагина. Основными целями миграции стали отказ от устаревшего Karma, обеспечение корректной выгрузки результатов unit-тестов в Allure и сохранение существующей бизнес-логики тестов. Изменения должны были затронуть только инфраструктуру тестирования и API тестового раннера. Рассматривали: Jest vs Vitest Мы не прыгали сразу в финальный выбор. Сначала на ветке test_jest собрали полноценный прототип на Jest: jest-preset-angular, отдельные конфиги создали на customer / operator / shared, allure-jest, добавили адаптеры под Jasmine-совместимые хелперы (expectAsync, toBeTrue / toBeFalse, глобальный spyOn). Это был честный spike: можно ли перенести сотни спеков и закрыть Allure. Эксперимент с Jest показал, что можно закрыть основные боли: нативный Node + jsdom без Chrome; официальный путь через jest-preset-angular; рабочий allure-jest с иерархией лейблов; параллельный прогон проектов. Но именно для нашего Angular-монорепо появились минусы: отдельный контур мимо Angular builder — скрипты jest --config ... вместо ng test; свой зоопарк конфигов (create-jest-config, tsconfig.jest.json, project configs); необходимость compatibility-слоев под старый Jasmine API; трансформ ESM/CJS-зависимостей (transformIgnorePatterns на длинный список пакетов Taiga/Angular); два мира инструментов: CLI/сборка на одном стеке, тесты — на другом. Jest как технология нас не провалил. Он показал, что миграция с Karma возможна и что Allure на Node-раннере реально оживает. Но как целевой стек для Angular 21 он выглядел компромиссом, а не финальной точкой. В итоге мы выбрали Vitest. После Jest-spike мы собрали решение на Vitest — коммит с принятым направлением: миграция builders, конфигов и спеков. Выбор опирался на набор практических критериев. Интеграция Vitest через Analog позволила сохранить запуск через Angular CLI и стала подходящим вариантом для нашего текущего workspace. Vitest подключается через @analogjs/vitest-angular:test. Для разработчика вход тот же: ng test customer ng test operator --configuration=cisourcecraft.dev В angular.json вместо karma-builder: "builder": "@analogjs/vitest-angular:test", "options": { "configFile": "vitest.config.mts" }sourcecraft.dev Мы остаемся внутри Angular CLI, а не уходим в параллельную систему запуска. Миграция на Vitest для нас закрыла ряд вопросов. Vite-based pipeline вместо отдельного Jest transform layer. Vitest работает на Vite: быстрее старт, нативная работа с ESM, проще inline зависимостей Angular/Taiga через server.deps.inline. Там, где в Jest копился transformIgnorePatterns, в Vitest мы описываем resolve/alias и список пакетов для инлайна — и это лучше ложится на текущий фронтенд-стек. Прямая интеграция с Allure через allure-vitest. allure-vitest/reporter пишет результаты в allure-results/unit/<project>. Плюс общий setup автоматически проставляет иерархию: parentSuite → suite → layer → epic → subEpic. То есть unit-слой наконец встает рядом с интеграционными отчетами по тем же правилам качества. Меньше «клея», ближе к целевому API. Vitest из коробки дает vi.fn, vi.spyOn, fake timers, globals. Jasmine → Vitest — механическая замена в спеках, без отдельного compatibility-runtime, который пришлось бы сопровождать на Jest. CI без браузера и с контролем воркеров. Unit-джоба переехала на Node-образ. Воркеры ограничены явно: в CI VITEST_MAX_WORKERS=2, потому что Angular + jsdom + coverage прожорливы по RAM, а K8s часто отдает чужой CPU count. Это осознанная настройка под нашу инфраструктуру, а не дефолт «съешь все ядра». Вывод: Jest доказал миграцию, Vitest лучше сел как целевой раннер для Angular-приложений Klecks. Миграция самих тестов Большая часть работы — не переизобрести тесты, а перевести API. Вот типичные замены. Было (Jasmine) Стало (Vitest) jasmine.createSpy() vi.fn() spyOn(obj, 'method') vi.spyOn(obj, 'method') jasmine.createSpyObj(...) ручной stub / vi.fn на методах and.returnValue(x) mockReturnValue(x) and.callFake(fn) mockImplementation(fn) calls.reset() mockClear() / mockReset() jasmine.clock().install() vi.useFakeTimers() jasmine.clock().tick(n) vi.advanceTimersByTime(n) jasmine.objectContaining expect.objectContaining done()-колбэки async/await + Promise jasmine.Spy в типах Mock из vitest TestBed, ComponentFixture, провайдеры и структура describe/it почти не менялись.  Важно, что миграция раннера не должна была стать рефакторингом продуктового кода. Мы отдельно подтянули инфраструктурные хелперы. Например, UnhandledCatcher научили ловить и window.onerror, и rxjs.config.onUnhandledError. В jsdom/Vitest необработанные ошибки Observable ведут себя иначе, чем в Karma/Chrome. Такие места всплывают только на реальном прогоне, поэтому spike на Jest тоже был полезен: часть проблем API мы увидели еще до финального выбора.  Allure: как закрыли исходную боль Что мы сделали по шагам:  Репортер пишет в allure-results/unit/<project>. Автолейблы строят иерархию из пути файла и имени теста. Epic выбирается по расположению: customer / operator / shared. Локально можно собрать и открыть отчет теми же npm-скриптами, что и для integration Параллелизм: Chrome Headless / Karma → Vitest threads, maxWorkers=2 В итоге unit-слой перестал быть «второй реальностью» относительно Playwright: и там и там Allure понимает слой, suite и продуктовый epic. Что принесла миграция на vitest.  Было Стало Раннер Karma + Jasmine + Chrome Vitest + jsdom (+ Analog builder) Статус стека deprecated / legacy path Актуальный Angular-friendly-путь Запуск ng test через karma-builder ng test через @analogjs/vitest-angular:test Allure unit Ненадежная/неполная выгрузка allure-vitest + автолейблы CI-образ Тяжелый browser-based Node 22 Параллелизм Браузерный single-run контур threads + явный лимит воркеров Скорость джобы в CI ~3 минуты 40 секунд ~2 минуты 20 секунд Плюсы, которые мы получили: ушли с deprecated Karma; сохранили привычный DX через ng test; unit-отчеты в Allure стали частью нормального процесса; CI стал проще по зависимостям (без Chrome ради unit); спеки мигрируют механически, без переписывания сценариев; ускорили джобу unit тестов в CI на ~36,4%. Но есть и компромиссы. Они для нас— разумная цена за поддерживаемый стек и рабочую отчетность. Компромиссы, о которых стоит знать: jsdom ≠ полный браузер: редкие DOM/CSS edge-cases могут вести себя иначе; в CI нельзя бездумно крутить maxWorkers: coverage + Angular TestBed легко упираются в память; часть хелперов (UnhandledCatcher, fake timers) потребовала точечной адаптации под новый рантайм. Нативный Angular Vitest (@angular/build:unit-test) мы сознательно не брали, потому что для нашего кейса он слишком закрытый. Официальный builder заточен под zero-config: Angular сам поднимает Vitest, а кастомный vitest.config либо ограничен, либо поддерживается частично. Команда Angular прямо говорила: если нужен полноценный vitest.config.ts (плагины, репортеры, тонкая настройка), берите Analog. Для нас такой уровень контроля был принципиален. Требовалось интегрировать allure-vitest/reporter с собственной иерархией лейблов, создать единый factory-конфиг для customer, operator и shared с общими алиасами, путями Less и директориями покрытия. Кроме того, необходимо было управлять количеством воркеров через VITEST_MAX_WORKERS, чтобы учитывать ограничения памяти в K8s при запуске Angular-тестов с jsdom и coverage, а также гибко настраивать параметры рантайма, включая isolate, inline deps и setupFiles. Analog (@analogjs/vitest-angular) дает полный контроль «из коробки» и при этом оставляет ng test. Jest-spike уже показал, что «просто зеленых тестов» недостаточно — нужна управляемая инфраструктура и отчеты. Native имеет смысл для зеленого поля или простых приложений. Для нашего монорепо с Allure и жестким CI Analog оказался практичнее. Позже, когда @angular/build:unit-test догонит по конфигурируемости, можно будет пересмотреть — но на момент решения Analog закрывал требования лучше. Заключение Мы не меняли раннер, потому что «все переходят на Vitest». Триггером стала связка из двух фактов: Karma deprecated и Allure для unit на текущем стеке больше не закрывал требования качества. Jest на ветке test_jest подтвердил, что Node-раннер + Allure — рабочий путь. Vitest выбрали как целевое решение, потому что он лучше стыкуется с Angular CLI через Analog, естественнее живет с Vite/ESM и дает прямую интеграцию с Allure через allure-vitest без отдельной параллельной инфраструктуры запуска. Итог миграции: один shared Vitest-конфиг на три проекта; builder в angular.json без Karma; автолейблы Allure в @klecks/testing; механический перевод Jasmine API → Vitest в спеках; CI на Node с ограниченными воркерами; unit-тесты в CI проходят на ~36,4% быстрее; Если у вас похожий Angular-монолит на Karma и уже болит Allure/поддержка, имеет смысл сначала сделать короткий spike на Jest или Vitest именно с выгрузкой отчетов, а не только с «тесты зеленые локально». У нас зеленость никогда не была главной проблемой. Главным было сделать так, чтобы unit-слой снова был наблюдаемым и поддерживаемым.

  6. github.com/angular/components/pulls Shuaib Hasan Akib done a lot of work in the Angular/Material docs to modernize their source code, among a lot of other things. If you look at open/closed PRs with him tagged in either repo (merged Material issues in this link) you will see all sorts of good refactors.

  7. Shoutout to our community for an amazing contribution from Angelo Parziale. PR #70527 introduces strictUnclaimedEventNames, a new opt-in compiler option to catch misspelled output bindings by checking directive outputs and native events. Explore the PR: angular#70527 🛠

    5 likes · 2 reposts
  8. @mfpears Hey Mike, I know there are angular-developer skills (angular/skills), but I wonder if you had any that help Agents write declarative code. I feel like the general overview of signals or resource does not quite cut it

    2 likes · 31 views
  9. YouTube · · YouTube description · Chart Design · angular/angular

    週刊プログラミングニュース|Deno・Python・OSSと開発ツール【2026-W42/10月9〜15日】 週刊プログラミングニュース|2026年10月9日〜15日 | 2026-W42 最初にDeno、Python、Bun、GitHub Actions、.NET、シークレット保護。その後、OSS・フレームワーク、ライブラリー・データベース、IDE・開発プラットフォームを章ごとに整理します。 対象期間:10月9日00:00〜15日23:59 JST。時刻を確認できた発表はJSTへ換算し、日付のみの資料は公式表記日を採用しています。境界時刻が未確認の候補は確定分と区別しました。 正式版、プレビュー、今後の予定を区別しています。Dockerは修正済み問題の開示、Nodeの10月21日予定分は未提供の修正予告です。 映像は公式情報に基づく解説図で、実機デモではありません。性能値は発表元の測定で、独自の実測ではありません。 日本語音声・日英字幕。 チャプター 00:00 週刊プログラミングニュース 00:28 DenoチームがCloudflareへ 01:50 Python 3.15が正式公開 03:18 Bun 1.4.3に型チェッカー 04:30 Actionsにワークフローロック 05:38 .NETは安定版更新とRC2 07:00 GitHubのシークレット検出拡大 07:59 Next.jsのセキュリティ更新 09:28 Express・Fastify・Micronaut 10:57 フロントエンドの安定版とRC 12:16 Astro・Rolldown・Laravel 13:21 Hydrogenがライブラリーへ 14:14 Pydantic・NumPy・FastAPI 15:59 PgBouncerとpsqlODBC 17:25 SQLite 3.54.0 18:38 Ruby 4.1のプレビュー 19:39 Node.js更新と次のセキュリティ修正 20:51 VS CodeとVisual Studio 22:07 Cursor Originのアプリ連携 22:54 GitLabの緊急修正 23:57 Docker Hubの修正済み問題の開示 25:04 Lambdaの新しい実行モデルと接続維持 26:27 今週の確認リスト 公式出典 deno.com blog.python.org bun.com github.blog devblogs.microsoft.com github.blog devblogs.microsoft.com github.com/dotnet/core/blob/main/release-notes/10… nextjs.org github.com/expressjs/express/releases/tag/v5.3.0 github.com/fastify/fastify/releases github.com/fastify/fastify/security/advisories/GH… micronaut.io micronaut.io github.com/angular/angular/releases/tag/v22.2.3 github.com/vuejs/core/releases github.com/vuejs/core/blob/main/CHANGELOG.md github.com/vuejs/core/blob/minor/CHANGELOG.md github.com/nuxt/nuxt/releases/tag/v4.6.1 github.com/sveltejs/kit/releases/tag/@sveltejs%2F… github.com/withastro/astro/releases/tag/astro@7.4… github.com/rolldown/rolldown/releases/tag/v1.2.14 github.com/laravel/framework/releases/tag/v13.36.0 github.com/laravel/framework/releases shopify.dev github.com/pydantic/pydantic/releases/tag/v2.14.0 pydantic.dev github.com/pydantic/pydantic/releases github.com/numpy/numpy/releases/tag/v2.5.4 github.com/fastapi/fastapi/releases/tag/0.143.1 github.com/fastapi/fastapi/releases/tag/0.143.2 pgbouncer.org github.com/pgbouncer/pgbouncer/releases/tag/pgbou… postgresql.org sqlite.org ruby-lang.org nodejs.org nodejs.org nodejs.org openwall.com code.visualstudio.com learn.microsoft.com cursor.com docs.gitlab.com docker.com aws.amazon.com developers.cloudflare.com

    40 views
  10. @Armandotrue ‘DeferredContent’ directive from Angular Aria does this and it’s beautiful. I use the exact same directive for my custom components as well. github.com/angular/components/blob/main/src/aria/…

    3 likes · 94 views
Trending awards, every completed day and week
Gold
11st place finishes
Silver
72nd place finishes
Bronze
233rd place finishes
Awarded repositories
2Owned by angular now

Repositories

Ordered by gold, then silver, then bronze.

  1. angular.jsBest: 1st by stars, 199 stars1623
  2. angularBest: 2nd by PR authors, 24 PR authors010

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.