xiaohongshu
xiaohongshu
Use this Skill when working in the Connect monorepo and needing to modernize legacy Angular components.
Full skill instructions
When to use this skill:
FormComponent or BaseComponent@Select decorators for state managementFormNode instead of typed FormGroupChangeDetectionStrategy.OnPushContext: Connect's frontend is modernizing Angular components to use NgRx ComponentStore for state management, OnPush change detection, standalone components, and proper TypeScript typing. This provides better type safety, performance, and maintainability.
Strictness hierarchy: ALWAYS/NEVER > PREFER > CAN > NOTE/EXAMPLE
libs/connect/cms/qa/feature/src/lib/edit-topic/ - Edit topic component with form managementlibs/connect/cms/qa/feature/src/lib/settings-page/ - Settings page with conditional form logiclibs/connect/cms/qa/feature/src/lib/topics-page/ - Topics page with complex stateALWAYS read all component files before starting:
ALWAYS identify patterns to migrate:
extends FormComponent, extends BaseComponent)@Select decorators for stateFormNode usagesubscribe(), takeUntil())onInit() vs ngOnInit())ALWAYS identify business logic:
component-name.store.ts)initialState as a constantreadonly for immutability#privateFields for encapsulation(tuple): tuple is [void, DataType] => tuple[1] !== nullpipe() directly in effects: this.effect<Type>(pipe(...)) not this.effect<Type>((param$) => param$.pipe(...))EXAMPLE:
import { inject, Injectable } from "@angular/core";
import { ComponentStore } from "@ngrx/component-store";
import { Store } from "@ngrx/store";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { filter, pipe, switchMap, tap, withLatestFrom } from "rxjs";
// Define form controls interface
export interface ComponentNameFormControls {
field1: FormControl<string>;
field2: FormControl<boolean>;
}
// Define state interface
interface ComponentNameState {
readonly currentData: DataType | null;
}
const initialState: ComponentNameState = {
currentData: null,
};
@Injectable()
export class ComponentNameStore extends ComponentStore<ComponentNameState> {
readonly #store = inject(Store);
// Selectors
readonly currentData$ = this.select((state) => state.currentData);
readonly externalData$ = this.#store.select(getExternalData.selector);
// Form definition
readonly form = new FormGroup<ComponentNameFormControls>({
field1: new FormControl<string>("", {
validators: [Validators.required],
nonNullable: true,
}),
field2: new FormControl<boolean>(false, { nonNullable: true }),
});
// Updaters
readonly setCurrentData = this.updater<DataType>(
(state, data): ComponentNameState => ({
...state,
currentData: data,
})
);
// Effects - use pipe() directly
readonly initializeForm = this.effect<DataType>(
pipe(
tap((data: DataType) => {
this.setCurrentData(data);
this.form.reset(data);
this.#applyConditionalLogic(this.form);
})
)
);
readonly saveChanges = this.effect<void>(
pipe(
tap(() => {
this.#store.dispatch(updateAction.start(this.form.getRawValue()));
})
)
);
readonly cancelChanges = this.effect<void>(
pipe(
withLatestFrom(this.currentData$),
filter((tuple): tuple is [void, DataType] => tuple[1] !== null),
tap(([, data]) => {
this.form.reset(data);
this.#applyConditionalLogic(this.form);
})
)
);
// Private methods
#applyConditionalLogic(form: FormGroup<ComponentNameFormControls>): void {
// Conditional enabling/disabling logic
}
constructor() {
super(initialState);
// Initialize effects that don't take parameters
this.applyConditionalDisabling();
}
}
ChangeDetectionStrategy.OnPushstandalone: trueproviders arrayinject() for dependency injectioninject() calls first in the class as readonly fields#privateField syntax for private memberspublic or private keywords in TypeScript@Select decoratorsDestroyRef and takeUntilDestroyed (ComponentStore handles cleanup)EXAMPLE:
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from "@angular/core";
import { animate, style, transition, trigger } from "@angular/animations";
import { ComponentNameStore } from "./component-name.store";
@Component({
selector: "co-component-name",
templateUrl: "./component-name.component.html",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ComponentNameStore],
imports: [
// Only what you need
],
animations: [
trigger("slideDown", [
transition(":enter", [
style({ height: "0", opacity: 0, overflow: "hidden" }),
animate("300ms ease-out", style({ height: "*", opacity: 1 })),
]),
transition(":leave", [
style({ height: "*", opacity: 1, overflow: "hidden" }),
animate("300ms ease-in", style({ height: "0", opacity: 0 })),
]),
]),
],
})
export class ComponentNameComponent {
readonly #componentStore = inject(ComponentNameStore);
@Input() set data(data: DataType) {
if (data) {
this.#componentStore.initializeForm(data);
}
}
readonly form = this.#componentStore.form;
readonly data$ = this.#componentStore.currentData$;
saveChanges(): void {
this.#componentStore.saveChanges();
}
cancelChanges(): void {
this.#componentStore.cancelChanges();
}
}
@if, @for, @switch) instead of *ngIf, *ngFor, *ngSwitch*ngrxLet directive or ngrxPush pipe to handle Observables
ngrxPush pipe over async for one-off async bindings*ngrxLet or ngrxPush multiple times for the same Observable; instead assign it to a template variable using @letEXAMPLE - Native control flow with animation:
@if (form.controls.parentField.value) {
<div @slideDown>
<mat-slide-toggle formControlName="childField">
Child Field
</mat-slide-toggle>
</div>
}
EXAMPLE - Form bindings:
<form [formGroup]="form" class="tw-space-y-4">
<mat-form-field class="tw-w-full" subscriptSizing="fixed">
<mat-label>Field Name</mat-label>
<input matInput formControlName="fieldName" required />
@if (form.controls.fieldName.hasError("required")) {
<mat-error>Field is required</mat-error>
}
</mat-form-field>
</form>
afterClosed() and only proceed if confirmedEXAMPLE:
deleteItem(): void {
this.#dialog
.open(ConfirmDeleteModalComponent, {
data: { itemName: this.data$.value?.name },
})
.afterClosed()
.subscribe((confirmed) => {
if (confirmed) {
this.#componentStore.deleteItem();
}
});
}
successMessage in ApiAction definitions (not manual toasts in stores)CoSnackService only for local operations (cancel, info messages)EXAMPLE - Success Messages:
// ❌ WRONG - shows before API completes
readonly saveChanges = this.effect<void>(
pipe(
tap(() => {
this.#store.dispatch(updateAction.start(this.form.getRawValue()));
this.#snacks.success('Saved!'); // ← BAD
})
)
);
// ✅ CORRECT - shows only on actual success
export const updateAction = new ApiAction<State, Input, Output>(
'Entity',
'Update',
'Feature',
{
showErrors: true,
successMessage: 'Saved!', // ← GOOD
}
);
EXAMPLE:
<button
matSuffix
mat-icon-button
matTooltip="Copy to clipboard"
[cdkCopyToClipboard]="form.controls.id.value"
(cdkCopyToClipboardCopied)="onIdCopied($event)"
>
<fa-icon [icon]="copyIcon" />
</button>
corepack yarn nx lint <library-name>pipe() directlyany types{ emitEvent: false } when programmatically enabling/disabling controlsEXAMPLE:
#applyConditionalDisabling(form: FormGroup<FormControls>): void {
const parentValue = form.controls.parentField.value;
if (!parentValue) {
form.controls.childField.disable({ emitEvent: false });
} else {
form.controls.childField.enable({ emitEvent: false });
}
}
// Call after form reset and in an effect watching the parent field
readonly applyConditionalDisabling = this.effect<void>(
pipe(
switchMap(() => this.form.controls.parentField.valueChanges),
tap(() => {
this.#applyConditionalDisabling(this.form);
})
)
);
nonNullable: true to form controls to ensure type safetynull after resetEXAMPLE:
readonly form = new FormGroup<ComponentNameFormControls>({
field1: new FormControl<string>('', {
validators: [Validators.required],
nonNullable: true // ← ALWAYS include this
}),
field2: new FormControl<boolean>(false, { nonNullable: true }),
});
nonNullable: true to form controlsFormGroup and FormControl (not FormNode)pipe() directly: this.effect<Type>(pipe(...))this.effect<Type>((param$) => param$.pipe(...))DestroyRef and takeUntilDestroyed for ComponentStore subscriptionssuccessMessage in ApiAction definitionsCoSnackService only for local operationsany types
unknown when type is uncertain#privateField syntax for encapsulationpublic or private keywords in TypeScript class membersComponentStore.get()
readonly properties$ suffixpipe() directlyOnPush change detectionstandalone: true@Select decoratorsinject() calls first in class#privateField syntaxany typeschangeDetection: ChangeDetectionStrategy.OnPush in @Component decorator for new componentsinject() calls first in the class as readonly fields@Input and @Output properties second in the classclass bindings instead of ngClassstyle bindings instead of ngStyletw- prefix (enforced in libs/co/ui-tailwind-preset/tailwind.config.js)@apply directive@fortawesome/angular-fontawesome package<fa-icon> component, not <i> tags with CSS classes@fortawesome/pro-*-svg-icons (not free packages)yarn run format (from Connect/ng-app-monolith)yarn exec nx affected --targets=lint,test --skip-nx-cacheALWAYS refer to these files for complete examples:
libs/connect/cms/qa/feature/src/lib/edit-topic/cms-qa-edit-topic.store.tslibs/connect/cms/qa/feature/src/lib/settings-page/cms-qa-settings-page.store.tslibs/connect/cms/qa/feature/src/lib/topics-page/cms-qa-topics-page.store.tsAGENTS.md - Angular Development Patterns sectionSee the Instructions section.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience