

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# Portlet Development Guide23> **Parent node**: [`core-web/CLAUDE.md`](../../CLAUDE.md) (Angular rules, commands, testing)4> **Reference portlet**: `libs/portlets/dot-tags/` — read the source when in doubt5> **SignalStore docs**: https://ngrx.io/guide/signals/signal-store67## Architecture Pieces89Every CRUD portlet has these parts:1011| Piece | What to build | Reference file (`dot-tags`) |12|-------|--------------|----------------------------|13| **Shell** | Minimal wrapper, renders the list component | `dot-tags-shell/dot-tags-shell.component.ts` |14| **List + Store** | Data table with pagination, search, sort; store manages state & HTTP | `dot-tags-list/` and `dot-tags-list/store/` |15| **Create/Edit dialog** | Single component, two modes via `DynamicDialogConfig.data` | `dot-tags-create/dot-tags-create.component.ts` |16| **Routes** | `dotFeatureRoutes` exported from `lib.routes.ts`, registered in `app.routes.ts` | `lib.routes.ts` |1718Optional: Import dialog (CSV/file upload) — see `dot-tags-import/`.1920## Separation of Concerns (Critical Rule)2122| Layer | Responsibility | Owns |23|-------|---------------|------|24| **Store** | Data fetching, state mutations, API calls | HTTP calls, `patchState`, error handling via `DotHttpErrorManagerService` |25| **List Component** | UI orchestration | Opens dialogs, shows confirmations, translates `TableLazyLoadEvent`, debounces search |26| **Create Component** | Form logic | Reactive form, validation, `DynamicDialogRef.close(formValue)` |27| **Shell Component** | Routing wrapper | Just renders the list component |2829**Store MUST NOT** open dialogs, inject `DialogService`, or interact with UI. Store is data only.3031## Key Rules3233- `untracked()` inside `effect()` to prevent infinite loops34- `take(1)` on one-shot HTTP calls (e.g. `loadById`, dialog saves). **Do NOT add `take(1)` inside `rxMethod`** — `rxMethod` manages subscription lifetime automatically; adding `take(1)` there breaks cancellation35- Error handling: always `catchError` → `httpErrorManager.handle(error)` → `return EMPTY`36- On error from CRUD actions, set status back to `'loaded'` (not `'error'`) so the list stays usable37- `DotHttpErrorManagerService.handle(error)` for all HTTP errors — no custom error UI38- All user-facing text uses i18n keys via `DotMessagePipe` (`| dm`) or `DotMessageService.get()`39- Key naming: `{feature}.{context}.{element}` (e.g., `tags.confirm.delete.header`)40- `data-testid` (all lowercase) on every interactive element; `[attr.aria-label]` on inputs and icon-only buttons4142## Dialog Sizing Standards4344| Dialog type | Width |45|-------------|-------|46| Form / add / edit / import | `700px` |47| Confirmation / warning / delete | `500px` |48| Special (iframes, full-screen) | responsive — `min(92vw, 75rem)` or as needed |4950Apply `width` on `DialogService.open()` config. For `p-confirmDialog`, set `[style]` on the template element (PrimeNG's `Confirmation` type does not expose `style` as a confirm-call option):5152```typescript53// Form dialog (TypeScript)54this.dialogService.open(MyFormComponent, { width: '700px', ... });55```5657```html58<!-- Confirmation dialog (template) -->59<p-confirmDialog [draggable]="false" [style]="{ width: '500px' }" />60```6162**Exception**: upload/import dialogs also set `contentStyle: { height: '460px' }` to keep a fixed layout while showing inline errors.6364## CRUD Patterns6566**Modal dialogs (default)**: List component opens `DialogService.open(CreateComponent, ...)`. The dialog closes with the form value; the list component passes it to the store. This is the pattern used in `dot-tags` and should be the default for new portlets.6768**Routed CRUD (rare)**: Separate route for create/edit pages. Use only when the form is too complex for a dialog (many tabs, nested data). See `dot-experiments` for this pattern.6970## When the CRUD Pattern Is Not Enough7172Standard CRUD portlets (Shell + List + Store) cover most cases. For portlets with complex domain logic, multiple interconnected features, or large state surfaces, decompose the store into feature slices using `signalStoreFeature()`:7374```typescript75export const UVEStore = signalStore(76 withUve(), // system lifecycle77 withFlags(), // feature flags78 withPage(), // page asset domain79 withPageApi(), // backend interactions80 withWorkflow(), // workflow/lock81 withEditor(), // editor UI state82);83```8485Each feature slice owns a named prefix in the flat state (e.g. `editor*`, `view*`, `page*`) and exposes only the methods and computeds relevant to its domain. See `libs/portlets/edit-ema/portlet/README.md` for a full example.8687## Nx Generator Post-Setup8889After running the generator:9091```bash92yarn nx generate @nx/angular:library --name=portlet \93 --directory=libs/portlets/dot-{feature} \94 --tags=type:feature,scope:dotcms-ui,portlet:{feature} \95 --prefix=dot --standalone --no-interactive96```9798**Required fixes**:991001. **tsconfig alias** in `core-web/tsconfig.base.json`: change generated `"portlet"` → `"@dotcms/portlets/dot-{feature}/portlet"`1012. **project.json** `name`: change to `portlets-dot-{feature}-portlet`1023. **jest.config.ts** `displayName`: change to `portlets-dot-{feature}-portlet`1034. **tsconfig.spec.json**: add `isolatedModules: true` in transform options (required for transitive deps)1045. **tsconfig.spec.json**: keep minimal — only `module`, `target`, `types`1056. **Delete** generated `README.md` and boilerplate component in `src/lib/portlet/`106107## Anti-Patterns108109| Do NOT | Do Instead |110|--------|-----------|111| Store opens dialogs or injects DialogService | Component opens dialogs, passes result to store |112| Missing `untracked()` in effect | Wrap store method calls in `untracked()` |113| Missing `isolatedModules: true` in jest config | Add it — transitive deps fail without it |114| Adding `"strict": true` to tsconfig.json | Omit — causes issues with Angular compiler |115| Adding `"module": "preserve"` to tsconfig.spec.json | Use `"module": "commonjs"` |116| Hardcoded text in templates | Use `DotMessagePipe` (`| dm`) for all user-facing text |117| Custom error dialogs | Use `DotHttpErrorManagerService.handle(error)` everywhere |118| `@Input()` / `@Output()` decorators | Use `input()` / `output()` signal functions |119| `*ngIf` / `*ngFor` structural directives | Use `@if` / `@for` control flow |120121## Other Reference Portlets122123- **`dot-tags`** — Canonical reference for the standard CRUD pattern (modal dialogs, SignalStore)124- **`dot-experiments`** — Full CRUD with guards, resolvers, shell, routed create/edit125- **`dot-analytics`** — Enterprise license checking, lazy loading126- **`dot-content-drive`** — Complex nested routing, reference for test config127- **`edit-ema/portlet`** — Reference for complex portlets: feature slice decomposition, Container/Presentational pattern, flat prefixed state128- **`dot-locales`** — Legacy pattern only (uses `ComponentStore`). Do not use as a model for new work129
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| dotCMS/core.cursor/rules/e2e-rules.mdc · 949 | Cursor rules | setupteststylearch+5 | 89/100 | 14 days ago | |
| dotCMS/core.github/instructions/frontend.instructions.md · 949 | Copilot instructions | testlint-formatstylearch+3 | 61/100 | today | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today | |
| dotCMS/corecore-web/AGENTS.md · 949 | AGENTS.md | style | 63/100 | 14 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| dotCMS/corecore-web/apps/dotcms-ui-e2e/AGENTS.md · 949 | AGENTS.md | setupstylearchtesting-strategy+2 | 78/100 | 14 days ago | |
| dotCMS/corecore-web/apps/dotcms-ui/AGENTS.md · 949 | AGENTS.md | buildteststyledependencies+3 | 94/100 | 14 days ago | |
| dotCMS/corecore-web/apps/mcp-server/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+5 | 89/100 | 14 days ago | |
| dotCMS/corecore-web/libs/block-editor/CLAUDE.md · 949 | CLAUDE.md | archdo-not | 69/100 | 14 days ago | |
| dotCMS/corecore-web/libs/new-block-editor/CLAUDE.md · 949 | CLAUDE.md | lint-formatstyledo-notagent-behaviour | 61/100 | 14 days ago | |
| dotCMS/corecore-web/libs/portlets/edit-ema/portlet/src/lib/store/CLAUDE.md · 949 | CLAUDE.md | teststylearchtypes+2 | 65/100 | 7 days ago | |
| dotCMS/corecore-web/libs/sdk/client/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+9 | 97/100 | 14 days ago | |
| dotCMS/corecore-web/libs/sdk/react/CLAUDE.md · 949 | CLAUDE.md | setupbuildtestlint-format+9 | 97/100 | 14 days ago | |
| dotCMS/coredotCMS/src/main/java/com/dotcms/rest/CLAUDE.md · 949 | CLAUDE.md | typesdatabaseapido-not+1 | 57/100 | 14 days ago | |
| dotCMS/coretest-jmeter/CLAUDE.md · 949 | CLAUDE.md | testarchsecurityperformance+2 | 77/100 | 14 days ago | |
| dotCMS/core.cursor/rules/frontend-context.mdc · 949 | Cursor rules | teststyledocs | 78/100 | today | |
| dotCMS/core.cursor/rules/java-context.mdc · 949 | Cursor rules | buildstyle | 44/100 | 14 days ago | |
| dotCMS/core.cursor/rules/test-context.mdc · 949 | Cursor rules | testtesting-strategy | 54/100 | today | |
| dotCMS/core.github/copilot-instructions.md · 949 | Copilot instructions | setupbuildtestlint-format+11 | 84/100 | 11 days ago | |
| dotCMS/core.cursor/rules/doc-updates.mdc · 949 | Cursor rules | docs | 30/100 | 14 days ago |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/dotcms-core-core-web-libs-portlets-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.