fix(tags): move validation to kbq-tag-list (#DS-2943) - #1977
Conversation
|
Visit the preview URL for this PR (updated for commit 0237b43): https://koobiq-next--prs-1977-znmhuue9.web.app (expires Sun, 06 Sep 2026 14:41:08 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c9e37e518febda70d0317d07e8ceb35ac43c534c |
There was a problem hiding this comment.
🟡 Changes recommended
KbqTagInput.ngControl is injected as optional but asserted non-null, creating an incorrect public/runtime contract that should be reconciled (and the API snapshot updated accordingly).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR aligns tag creation behavior across typing/blur/paste so validation is consistently applied via the <kbq-tag-list> form control (instead of the input), and documents the recommended validation approach with updated examples.
Changes:
- Refactors
KbqTagInputto add tags through a single path for typing/blur/paste and removes validation gating based on an input-boundNgControl. - Deprecates the input-bound
ngControl/triggerValidation()surface and updates docs to state validators must be attached to<kbq-tag-list>. - Adds/updates docs examples and expands unit tests to cover paste + validation + error-state behavior.
File summaries
| File | Description |
|---|---|
| tools/public_api_guard/components/tags.api.md | Marks deprecated surfaces in extracted public API snapshot. |
| packages/docs-examples/example-module.ts | Registers new docs examples and lazy-load switch cases. |
| packages/docs-examples/components/tags/tag-input-with-form-control-validators/tag-input-with-form-control-validators-example.ts | Updates validation example to validate the tag array and improves UI messaging. |
| packages/docs-examples/components/tags/tag-autocomplete-with-form-control-validators/tag-autocomplete-with-form-control-validators-example.ts | Adds a new autocomplete + validators example covering typed/pasted/selected tags. |
| packages/docs-examples/components/tags/index.ts | Exports/registers the new validators example component. |
| packages/components/tags/tag-text-control.ts | Documents the interface and deprecates ngControl in the contract. |
| packages/components/tags/tag-list.component.ts | Removes legacy error-bridging from input control to list control. |
| packages/components/tags/tag-input.ts | Unifies tag creation logic and stops blocking paste based on input validation. |
| packages/components/tags/tag-input.spec.ts | Adds regression tests for paste empties + validation behavior + error state matchers. |
| packages/components/tags/tag-input.ru.md | Documents validation model and updates embedded example reference (RU). |
| packages/components/tags/tag-input.en.md | Documents validation model and updates embedded example reference (EN). |
| packages/components/tags/tag-autocomplete.ru.md | Adds validation guidance and example reference (RU). |
| packages/components/tags/tag-autocomplete.en.md | Adds validation guidance and example reference (EN). |
| packages/components/tags/examples.tag-input.ru.md | Removes older embedded validation section in examples (RU). |
| packages/components/tags/examples.tag-input.en.md | Removes older embedded validation section in examples (EN). |
| packages/components-dev/tag/template.html | Removes legacy manual dev demo markup in favor of docs examples. |
| packages/components-dev/tag/styles.scss | Removes unused dev-demo styles after template cleanup. |
| packages/components-dev/tag/module.ts | Simplifies dev app to render docs examples (and adds new example). |
Review details
Suppressed comments (1)
packages/components/tags/tag-input.ts:136
ngControlis injected with{ optional: true }but the non-null assertion (!) makes the type/runtime contract incorrect (it will benullwhen no control is bound). Since this is a public property (even if deprecated), consider making it truly optional/nullable (e.g.ngControl?: NgControlorNgControl | null) and updating the API snapshot accordingly to avoid consumers hitting null-derefs.
/**
* The form control instance bound to the input, if any.
*
* @deprecated Unused. Bind `[formControl]`/`[ngModel]` to `<kbq-tag-list>` instead — it is the
* single form control for the whole tag list. Will be removed in a future major release.
* @docs-private
*/
ngControl = inject(NgControl, { optional: true, self: true })!;
/**
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
lskramarov
left a comment
There was a problem hiding this comment.
Code review — fix(tags): stop bypassing validation on paste (#DS-2943)
The core direction is right: the input-level NgControl gate and the registerInput() → setErrors bridge were genuinely broken (that bridge also wiped the tag list's own errors whenever the input control happened to be valid), and consolidating typed/blur/paste into one addTag() is a real improvement. The onPaste empty-data guard fix is correct — (data && data.length === 0) was dead code. I also ran the full gate set on this branch: jest (tags/form-field/autocomplete, 485 passed), eslint, stylelint, prettier, cspell, ngc --noEmit on docs-examples, and the example-module generator all pass, and the regenerated example-module.ts is byte-identical to what the generator produces.
15 findings below, most severe first.
Blocking-ish (consumer-visible):
validation-tag-list— a shipped docs example relies on both removed mechanisms and is now a no-op demo whose control is permanently invalid.- The
ngControldeprecation and the new prose tell people to move[formControl]/[ngModel]off the input unconditionally — but that binding is the autocomplete'sNG_VALUE_ACCESSORin six examples. - The
pendingUIChangelatch still leaks for rejected non-empty values — the exact pattern the new docs recommend — spuriously marking the control dirty on a later programmaticsetValue. @else ifin both validator examples hides co-occurring errors, reachable with the example's own options.
Correctness (pre-existing, but in code this PR rewrites): [distinct] still can't dedupe within a paste; blur() has no disabled guard.
Process: no BREAKING CHANGE: footer, no docs/guides/migration.{en,ru}.md entry, no schematic — fix(popover) (5e7db141a, same series) shipped all three for a smaller change. And the commit subject describes the opposite of what changed, which is what lands in the changelog.
Tests: 8 of the 12 new tests pass unchanged on main.
Docs: the stated default ErrorStateMatcher behaviour omits form.submitted; "regardless of its source" is not true for in-place tag editing (preventEditSubmit is the real hook and goes unmentioned).
Plus efficiency, reuse and duplicate-coverage notes inline.
🤖 Generated with Claude Code
- migrate `validation-tag-list` example and the validation dev app off the input-level control onto tag-list validators - clear the `pendingUIChange` latch when the consumer rejected a tag, so a later programmatic `setValue` no longer marks the control dirty - skip tag creation on a blur caused by disabling the input - dedupe pasted values against a value set built once per paste, which also closes the within-paste duplicate gap - qualify the `ngControl` deprecation: binding a control to the input stays supported (autocomplete needs it), only its validators are ignored - split co-occurring errors into one `<kbq-error>` per condition and replace the hand-rolled max-count validator with `Validators.maxLength` - document the full default `ErrorStateMatcher` behaviour and `preventEditSubmit` for in-place editing - reshape the validation tests so they fail against the pre-fix code and drop coverage duplicated in `tag-list.component.spec.ts` - type `trimValue` and make `ngControl` nullable Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No description provided.