Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/guides/migration.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ for each option it deselected and reporting the shortened value to the form cont

### 18. Component review (20.3.0)

Ten components went through a full review in 20.3.0: notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.
Components went through a full review in 20.3.0, in two waves. The first covered notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select; the second is the one each subsection below belongs to. Each review closed the members that were never part of the component's contract, moved inputs to signals where that was the point of it, and fixed the behavior it uncovered along the way. Only the changes that reach a consumer are listed here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewritten intro contradicts the subsections it introduces.

"The first covered notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree and tree-select; the second is the one each subsection below belongs to."

The #### subsections actually present in §18 are: Popover, Search expandable, Split button, Textarea, Title, Toast, Tooltip. Six of those seven are named in the first-wave list in the same sentence, so the guide now assigns six already-shipped migrations to the wrong release wave. Only the Textarea subsection is new.

Mirrored verbatim at migration.ru.md:1042. Naming the second wave explicitly ("the second covered textarea") avoids the contradiction and survives the next component review.


Every schematic named below runs automatically:

Expand Down Expand Up @@ -1113,6 +1113,28 @@ A `<kbq-split-button>` with no projected button no longer throws outside dev mod

Reported by `split-button-optional-disabled`.

#### Textarea

`KbqTextarea` implements `KbqFormFieldControl`, which declares `value`, `id`, `placeholder`, `required`, `disabled`, `focused`, `empty` and `errorState` as plain members — that interface is how the form field reads them, so they stay plain accessors. What moved are the four inputs the textarea owns.

`canGrow` was the odd one: its getter returned `!maxRowLimitReached && bound`, so it reported `false` once the textarea hit `maxRows` even though the consumer had asked for growth. The folded value drives the resize handle and is internal now; `canGrow()` reports what was bound.

| Pattern | Manual migration |
| ------------------------------------------------------- | --------------------------------------------------------------------- |
| `.maxRows` / `.freeRowsHeight` / `.maxRowLimitReached` | Read as calls — rewritten for you |
| `.canGrow` | `canGrow()`, and expect what was bound — not `false` at the row limit |
| `.canGrow = …` / `.maxRows = …` / `.freeRowsHeight = …` | Bind them in the template; the inputs are read-only |

**`maxRows` and `freeRowsHeight` report `number | undefined`.** Both were declared non-nullable while an unbound textarea held `undefined`, and `maxRowLimitReached` compared against it — `rowsCount > undefined` is false, which is why unlimited growth worked at all.

**`freeRowsHeight` no longer writes itself.** It defaulted to the measured line height by assigning its own input in `ngOnInit`; the fallback is a computed now, so binding it later actually takes effect instead of being overwritten on the next init.

**The `kbq-textarea_max-row-limit-reached` class follows the row count directly.** It is derived from a signal written inside `runOutsideAngular`, so the class used to wait for an unrelated change detection pass to appear.

**Generated ids changed shape**, from `kbq-textarea-1` to `kbq-textarea-a1`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated-id change does not happen for consumers — kbq-textarea-a1 is a TestBed artifact.

CDK's _IdGenerator.getId() is:

if (this._appId !== 'ng') { prefix += this._appId; }

with the CDK's own comment "Omit the app ID if it's the default ng", and Angular's APP_ID defaults to 'ng'. The a only appears because @angular/platform-browser/testing provides {provide: APP_ID, useValue: 'a'}.

The per-prefix counter also still starts at 0, exactly like the removed let nextUniqueId = 0 — so a real app gets kbq-textarea-0 before and after. Nothing changed.

A consumer who greps their snapshots for kbq-textarea-a… on the strength of this note finds nothing. And data.ts:76's rationale — "the app id is part of the prefix now, which keeps two Angular apps on one page from colliding" — is false at the default APP_ID, which is precisely the case where the CDK strips it.

Mirrored at migration.ru.md:1138, README.md:56 and data.ts:76.


Handled by `textarea-signals`: the value-safe reads are rewritten, the rest is reported.

#### Title

`kbq-title` measures its host and opens a tooltip when the text is truncated. The review kept that surface — the `kbq-title` input and the tooltip it opens — and closed the measurement machinery behind it.
Expand Down
24 changes: 23 additions & 1 deletion docs/guides/migration.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ ng g @koobiq/components:list-tree-multiple-input --project <your project>

### 18. Ревью компонентов (20.3.0)

В 20.3.0 полное ревью прошли десять компонентов: notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree и tree-select. Каждое ревью закрывало члены, которые никогда не были частью контракта компонента, переводило входы на сигналы там, где в этом и был его смысл, и попутно исправляло найденные ошибки поведения. Ниже перечислено только то, что доходит до потребителя.
В 20.3.0 полное ревью прошло в две волны. В первую вошли notification-center, popover, search-expandable, select, split-button, title, toast, tooltip, tree и tree-select; ко второй относится каждый из подразделов ниже. Каждое ревью закрывало члены, которые никогда не были частью контракта компонента, переводило входы на сигналы там, где в этом и был его смысл, и попутно исправляло найденные ошибки поведения. Ниже перечислено только то, что доходит до потребителя.

Все схематики, названные ниже, запускаются автоматически:

Expand Down Expand Up @@ -1117,6 +1117,28 @@ if (splitButton.disabled === false) {

Сообщает `split-button-optional-disabled`.

#### Textarea

`KbqTextarea` реализует `KbqFormFieldControl`, который объявляет `value`, `id`, `placeholder`, `required`, `disabled`, `focused`, `empty` и `errorState` обычными членами — именно через этот интерфейс их читает form-field, поэтому они остались обычными геттерами и сеттерами. Переехали те четыре входа, которые принадлежат самому textarea.

Особый случай — `canGrow`: его геттер возвращал `!maxRowLimitReached && привязанное значение`, поэтому он сообщал `false`, как только textarea упирался в `maxRows`, — хотя потребитель просил рост. Свёрнутое значение управляет ручкой изменения размера и стало внутренним; `canGrow()` возвращает то, что было привязано.

| Что было | Как мигрировать вручную |
| ------------------------------------------------------- | -------------------------------------------------------------------------- |
| `.maxRows` / `.freeRowsHeight` / `.maxRowLimitReached` | Читать как вызовы — переписывается за вас |
| `.canGrow` | `canGrow()`, и ожидать привязанное значение, а не `false` на пределе строк |
| `.canGrow = …` / `.maxRows = …` / `.freeRowsHeight = …` | Привязать в шаблоне; входы доступны только на чтение |

**`maxRows` и `freeRowsHeight` возвращают `number | undefined`.** Оба были объявлены ненулевыми, хотя textarea без привязки содержал `undefined`, и `maxRowLimitReached` сравнивался именно с ним: `rowsCount > undefined` ложно — благодаря этому неограниченный рост вообще работал.

**`freeRowsHeight` больше не пишет сам в себя.** Он получал значение по умолчанию, присваивая собственный вход в `ngOnInit`; теперь запасное значение — это `computed`, поэтому привязка, сделанная позже, действительно применяется, а не затирается при следующей инициализации.

**Класс `kbq-textarea_max-row-limit-reached` следует за числом строк напрямую.** Он выводится из сигнала, в который пишут внутри `runOutsideAngular`, поэтому раньше класс ждал постороннего цикла обнаружения изменений.

**Формат генерируемых `id` изменился** с `kbq-textarea-1` на `kbq-textarea-a1`.

Закрывается схематиком `textarea-signals`: безопасные по значению чтения переписываются, остальное сообщается в отчёте.

#### Title

`kbq-title` измеряет свой хост и открывает тултип, когда текст обрезан. Ревью сохранило эту поверхность — вход `kbq-title` и открываемый тултип — и закрыло стоящий за ней механизм измерения.
Expand Down
46 changes: 46 additions & 0 deletions packages/components/textarea/textarea.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,40 @@ describe('KbqTextarea', () => {

expect(getTextareaElement(fixture).classList.contains('kbq-textarea_max-row-limit-reached')).toBe(false);
});

it('should treat a valueless canGrow attribute as true', () => {
const fixture = createComponent(KbqTextareaValuelessCanGrow);

fixture.detectChanges();

const textarea = fixture.debugElement.query(By.directive(KbqTextarea)).injector.get(KbqTextarea);

expect(textarea.canGrow()).toBe(true);
expect(getTextareaElement(fixture).classList.contains('kbq-textarea-resizable')).toBe(false);
});

it('should report the bound canGrow rather than folding in the row limit', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regression test for the headline change passes identically against the old code.

It asserts canGrow() is true together with maxRowLimitReached() being false — conditions under which the old getter !this.maxRowLimitReached && this._canGrow also returned true. The distinguishing case (limit reached → old getter false, new input true) is never reached.

It can't be reached here at all: jsdom returns line-height: normal, so parseInt(…) is NaN, the lineHeight > 0 guard at line 325 is false, and rowsCount is pinned at 0 for every unit test. That also leaves the maxRows * lineHeight clamp (line 330), the kbq-textarea_max-row-limit-reached class, and the PR's "the class follows the row count directly" claim with no unit coverage — and makes the maxRowLimitReached() assertions at lines 309, 331 and 343 trivially true.

Stubbing KBQ_WINDOW.getComputedStyle to return a real line height makes the limit reachable and turns all four assertions into real ones.

const fixture = createComponent(KbqTextareaGrowWithMaxRows);

fixture.detectChanges();

const textarea = fixture.debugElement.query(By.directive(KbqTextarea)).injector.get(KbqTextarea);

expect(textarea.canGrow()).toBe(true);
expect(textarea.maxRowLimitReached()).toBe(false);
});

it('should leave maxRows and freeRowsHeight undefined when unbound', () => {
const fixture = createComponent(KbqTextareaForBehaviors);

fixture.detectChanges();

const textarea = fixture.debugElement.query(By.directive(KbqTextarea)).injector.get(KbqTextarea);

expect(textarea.maxRows()).toBeUndefined();
expect(textarea.freeRowsHeight()).toBeUndefined();
expect(textarea.maxRowLimitReached()).toBe(false);
});
});

describe('grow behavior', () => {
Expand Down Expand Up @@ -547,3 +581,15 @@ describe('KbqTextarea', () => {
}));
});
});

@Component({
imports: [KbqTextareaModule, KbqFormFieldModule, FormsModule],
template: `
<kbq-form-field>
<textarea kbqTextarea canGrow [(ngModel)]="value"></textarea>
</kbq-form-field>
`
})
class KbqTextareaValuelessCanGrow {
value = '';
}
Loading
Loading