- name
- client-conventions
- description
- Apply Artemis conventions when changing Angular application or TUM AET UI code, migrating components, or fixing client lint violations.
# Artemis client conventions
Use `reference/migration-recipes.md` for migration examples. Check changes with `pnpm run lint`
and `pnpm run prettier:check`. Prefer standalone components.
## Signals are mandatory for new code
Use `input()` / `input.required()`, `output()`, `viewChild()` / `viewChild.required()`,
`viewChildren()`, `signal()`, `computed()`, `effect()`, and `inject()` for dependency injection.
The legacy decorators `@Input`, `@Output`, `@ViewChild`, `@ViewChildren`, `@ContentChild`, and
`@ContentChildren` are banned throughout the application, including co-located specs and test
helpers. `localRules/enforce-signal-apis` (`rules/enforce-signal-apis.mjs`) enforces this under
`src/main/webapp/app/` and `src/test/javascript/`. Use signal APIs when changing an existing
component; there is no unmigrated-module exception.
A `computed()`, `linkedSignal()`, `effect()` or `afterRenderEffect()` must read a signal, or it
never re-runs; a value that reads none is a constant and belongs in a plain `readonly` field
(`@angular-eslint/reactive-context-must-read-signal`).
Read a signal with `()` when checking its value; `@angular-eslint/no-uncalled-signals` catches
accidental checks of the signal function. Keep Angular lifecycle hooks synchronous. Delegate
work that awaits to a separate async method (`@angular-eslint/no-async-lifecycle-method`).
Do not repeat a declaration in a component's metadata arrays
(`@angular-eslint/no-duplicates-in-metadata-arrays`).
## Injection and services
Declare every `inject()` field before any other class member (`@angular-eslint/inject-at-top`).
Fields initialize in declaration order, so a getter called from an earlier initializer would read
`undefined` from a service declared further down.
Declare an application-wide service with `@Service()`, not `@Injectable({ providedIn: 'root' })`
(`@angular-eslint/prefer-service-decorator`, autofixable). `@Service()` rejects constructor
injection and cannot share a class with another Angular decorator. The rule still reports a `@Pipe`
that is also injected as a service, and its autofix then breaks `ng build` with NG1006 (Vitest
compiles JIT and does not notice), so keep `@Injectable` on such a pipe with a justified line-level
disable and do not autofix it. Other provider metadata also keeps `@Injectable`.
## Checking access before a request
Ask `AccountService` before sending a request that needs more than a signed-in user, above all one
the client sends on its own (on sign-in, on page init, from the navbar): a refused request shows a
403 alert on the current page. Use its high-level methods such as `hasEditorAccess()` rather than
`hasAnyAuthorityDirect(IS_AT_LEAST_EDITOR)`, which counts an administrator whose session the server
does not grant the administrator rights. When no method fits, add one to `AccountService` and
`MockAccountService` instead of combining authorities, module features and passkey state in the
caller.
## `ngOnChanges` is banned
Use `computed()` or `effect()`. Enforced at error level by
`localRules/prefer-signal-reactivity-over-ngonchanges` (`rules/prefer-signal-reactivity-over-ngonchanges.mjs`)
across `src/main/webapp/app`, `packages/tum-aet-ui/src/lib`, and `src/test/javascript`, including specs
and undecorated base classes.
This is a consistency ban, not a correctness fix. Angular does call inherited `ngOnChanges` hooks
and does fire them for signal inputs, so existing uses are not dead code.
A genuinely unavoidable case, meaning you need `SimpleChanges.previousValue` or `isFirstChange()`,
or ordering before child initialisation, needs a detailed comment and a justified line-level
`eslint-disable-next-line`. `ngOnInit` and `ngOnDestroy` are unaffected.
## Template control flow
Use `@if`, `@for`, `@switch`. Never `*ngIf`, `*ngFor`, `*ngSwitch`.
Every `@switch` has a `@default` (`@angular-eslint/template/require-switch-default`). Use
`@default never;` when the cases cover the whole union or enum, so the strict template check
reports a missing case, and an empty `@default {}` otherwise. Both render nothing for an unmatched
value.
Bind styles with `[style.prop]`, `[style.prop.unit]` or `[style]`, never `[ngStyle]`
(`@angular-eslint/template/prefer-style-binding`); a constant is a static `style` attribute. Never
bind `outerHTML` (`@angular-eslint/template/no-outerhtml`).
Do not use `$any()` in templates (`@angular-eslint/template/no-any`). Prefer a typed template
reference for DOM input values and a typed component method for library event payloads.
## Images and keyboard order
Use `NgOptimizedImage` with `ngSrc` for images with known intrinsic dimensions or a positioned,
sized container for `fill`. Mark an image `priority` only when it is expected to be the largest
visible image on initial load. For arbitrary user images that must retain their intrinsic layout,
keep native `src` and use appropriate loading and decoding hints.
Give every `<img>` a useful, localized text alternative, or `alt=""` when the image is decorative
or already described next to it (`@angular-eslint/template/alt-text`). Keep keyboard focus in DOM
order; do not use a positive `tabindex` to reorder controls
(`@angular-eslint/template/no-positive-tabindex`). Move markup when the DOM order is wrong.
## Redirecting from guards and resolvers
A guard returns or emits `router.createUrlTree(...)`, or `new RedirectCommand(urlTree, options)`
when it needs `replaceUrl`, `skipLocationChange` or `state`. This also applies inside RxJS and
promise callbacks: return the redirect rather than throw it. The guards in one `canActivate`
array run together, and the first emitted result that is not `true`, in array order, wins.
A returned or emitted redirect waits for every guard ahead of it to return `true`; a thrown
`RedirectCommand` bypasses that ordering and can redirect before an earlier authority check
rejects the route.
A resolver returns or emits a `RedirectCommand`; a `UrlTree` returned from a resolver becomes
route data and does not redirect. Inside a resolver's RxJS operator or promise callback, it can
throw the `RedirectCommand` instead. The router cancels the running navigation with a redirect
that keeps its `replaceUrl` and `skipLocationChange`, and alerts shown before the throw still
appear.
Never call `router.navigate()` or `navigateByUrl()` in a guard or resolver. It cancels the running
navigation on the spot and starts a new one, so the original `replaceUrl` and
`skipLocationChange` are lost (Back then redirects forward again), a caller awaiting the original
navigation receives `false`, and the navigation still happens when another guard rejects the
route. A `return false` or `EMPTY` after the call changes nothing.
`localRules/no-navigation-in-guard-or-resolver` (`rules/no-navigation-in-guard-or-resolver.mjs`)
enforces this at error level under `src/main/webapp`. It follows the guard into nested callbacks,
into methods of its own class reached through `this`, and into functions of the same file. It is
file-local and does not resolve types, so it misses navigation in an injected service the guard
calls, a `Router` from a base-class field, `const router = this.router` or `injector.get(Router)`,
namespace imports, static helper calls and route objects without a marker key such as `path`.
Moving the call into a service silences the rule without fixing anything. A `catchError` after a
thrown redirect must rethrow what it does not handle.
In specs, use the real router (`TestBed.inject(Router)`, no `MockRouter`) and assert the result:
`router.serializeUrl(result as UrlTree)` for a returned redirect, or an `error` callback that
receives a `RedirectCommand` for a thrown one. Do not assert a `navigate` spy. For guard
combinations and browser history, route with `provideRouter(...)` and `provideLocationMocks()`
as in `src/main/webapp/app/localci/shared/localci-guard.spec.ts`.
## Form labels
Associate each visible form label with its native input using a matching `for` and `id`, or wrap
the input in the label. For a custom control, connect the label to the input inside the component,
not its host element. Use a heading or `span` for informational text that does not label a control;
give groups of controls an accessible group name. The
`@angular-eslint/template/label-has-associated-control` rule checks the label association, not
the element choice or group names, in TUM AET UI and the client template areas listed in
`eslint.config.mjs`.
## Copying objects
In production `src/main/webapp/app/**/*.ts`, use the wrappers in
`src/main/webapp/app/foundation/util/deep-clone.util.ts`:
- `deepClone(x)` detaches nested state while preserving supported prototypes.
- `cloneWith(x, { a, b })` deep-clones the source and applies overrides by reference.
- `hydrate(new Course(), dto)` gives a parsed DTO its prototype.
`rules/prefer-deep-clone.mjs` bans object spread, `Object.assign` and `structuredClone` in that
scope, even for plain objects; specs are exempt. `eslint.config.mjs` also restricts direct lodash
cloning imports. Array spread and object rest remain allowed.
Shallow copies share nested state; `structuredClone` loses custom prototypes such as `dayjs`.
Do not clone merely to notify a signal if nested identity must survive. For that case and the
child-input identity boundary, read the cloning section of `reference/migration-recipes.md`.
`packages/tum-aet-ui` is outside these application rules and must not import `app/` utilities.
Choose copying behavior appropriate to the package's data and identity requirements.
## Styling
Use TUM AET UI components (`@tumaet/ui-angular`) and Tailwind v4 utilities. Do not add Bootstrap or
ng-bootstrap in new work.
Colours use semantic tokens. Use TUM AET UI component variants, or `text-state-danger`,
`text-state-success`, `text-state-warning`, `text-state-info` for plain markup. Never `--p-<color>-N`
primitives, never `text-red-500`, never `text-danger`, never the superseded arbitrary
`text-(--danger)` form.
`localRules/no-raw-tailwind-color-palette` enforces the palette part across
`src/main/webapp/app/**/*.html` and `packages/tum-aet-ui/src/lib/**/*.html`. **The Bootstrap ban is
only partly enforced**: `localRules/no-bootstrap-classes` covers the migrated directories listed
in `eslint.config.mjs`.
The convention applies throughout the client even where lint does not enforce it. Add newly
migrated directories to that list.
Never hand-write PrimeNG root classes such as `class="p-button"` or `class="p-inputtext"`. Render
the real PrimeNG component so its styles load deterministically. Enforced by
`localRules/no-primeng-component-classes`.
PrimeNG itself is a transitional fallback, used only when a TUM AET UI gap cannot reasonably be closed
in the same change. Explain the contained fallback in the pull request.
If TUM AET UI lacks a reusable capability, add or evolve a package component around native HTML,
Angular Aria (`@angular/aria`, for composite widgets such as menus and tabs), or stable Angular CDK
primitives, and keep Artemis-specific composition in the application. See
`documentation/docs/developer/guidelines/tum-aet-ui-kit.mdx`.
## Other rules worth knowing
Prefer `undefined` over `null`. Aim for full type safety; `localRules/no-as-any-cast` and
`localRules/no-as-unknown-cast` block the usual escape hatches. Filenames are kebab-case.
Full guidance: `documentation/docs/developer/guidelines/client-development.mdx`.
GitHub에서 보기