Входные данные компонента строки
Карточка заявки теперь повторяется в списке и скоро понадобится рядом с другими элементами кабинета. Выделим её отображение в отдельный компонент TicketRow. Родитель продолжит владеть коллекцией и фильтром, а строка получит одну заявку через input. Результат главы — явная граница между выбором данных и их представлением. Она помогает развивать страницу, не распределяя запись предметного состояния между всеми дочерними элементами.
Не каждый повтор HTML требует нового компонента. Здесь граница оправдана самостоятельным смыслом: строка показывает заголовок, идентификатор, подпись статуса и номер версии одной заявки. Она не выбирает, какие записи включить в список, не отправляет запрос и не решает, кому разрешено изменение. Благодаря такому ограничению её можно читать отдельно от фильтра. Если же перенести в строку все обязанности родителя, файл станет короче, но устройство приложения останется столь же связанным.
Договор входа
Signal input возвращает доступ к значению, которое передал родитель. Required-вариант означает обязательность свойства в шаблоне использующего компонента. Это помогает обнаружить забытое связывание на этапе компиляции шаблонов. Он не является runtime-проверкой произвольного серверного JSON: Ticket остаётся типом TypeScript, а проверка unknown будет отдельной функцией. Не смешивайте уверенность компилятора в вызове компонента с проверкой внешних данных во время выполнения приложения.
Ниже полный конечный src/app/ticket-row.ts. В нём уже есть RouterLink, который начнёт содержательно использоваться после главы о маршрутах. На текущем локальном шаге допустимо заменить ссылку обычным заголовком, сохранив input и подписи. Для конечной версии используйте весь файл из архива. Родитель подключает TicketRow в imports и передаёт [ticket]="ticket" внутри своего цикла; имя локальной переменной цикла и имя входа совпадают только ради ясности.
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { statusLabels, Ticket } from './model';
@Component({selector: 'app-ticket-row', imports: [RouterLink], template: `
<article><h2><a [routerLink]="['/tickets', ticket().id]">{{ ticket().title }}</a></h2>
<p>{{ ticket().id }} · {{ labels[ticket().status] }} · версия {{ ticket().version }}</p></article>
`})
export class TicketRow { readonly ticket = input.required<Ticket>(); readonly labels = statusLabels; }
В дочернем шаблоне ticket() читает переданную запись, а labels[ticket().status] переводит машинный статус. Дочерний компонент не копирует запись в собственный signal. Если родитель построит новый объект rq-101 после локального изменения, input получит эту новую запись. Публичное состояние отобразится без дополнительного метода синхронизации. Копия имела бы смысл для независимого редакторского черновика, но такая копия требует правил принятия новой основы и появится в отдельной главе.
Ожидается прежний набор трёх карточек: извлечение компонента само по себе не меняет данные. Это важный критерий удачного разделения. Если после переноса появилась другая подпись статуса, исчезла версия или изменился порядок, причина находится в новом шаблоне либо связывании, а не в самом факте использования input. Сначала сравните внешний результат, затем обязанности классов. Структурное изменение полезно именно тогда, когда сохраняет предусмотренное поведение и облегчает следующий шаг.
Идентичность строки
Родительский @for использует track ticket.id. Angular получает устойчивое соответствие между предметной записью и отображаемым элементом. Индекс массива меняется при фильтрации или перестановке; id остаётся тем же. Представьте, что позже строка содержит раскрытую панель подробностей. Если связывать состояние строки с индексом, удаление первой заявки может перенести раскрытие на другую запись. Предметный ключ выражает, что панель относится к rq-102, а не ко второму месту списка.
Уникальность id — договор коллекции. Два разных объекта с одинаковым rq-101 не становятся двумя независимыми заявками лишь потому, что у них разный title. В конечном readList это отдельно проверяется через множество идентификаторов. На локальном этапе просто не создавайте повторные ключи. Полезно объяснять такую ошибку через модель данных: сначала определите, какая запись действительна, а затем исправляйте отображение. Произвольное добавление индекса к ключу скроет нарушение предметной идентичности.
Input предназначен для получения данных, поэтому строка не должна изменять ticket().status напрямую. Даже при read-only доступе вложенный объект остаётся обычным объектом JavaScript. Такую мутацию родитель может не заметить как новое значение signal, а другие читатели увидят неожиданную запись. Если строке понадобится кнопка действия, она сообщит намерение через output, а владелец коллекции выполнит обновление. Событие должно переносить id или конкретное действие, а не разрешение бесконтрольно менять весь объект.
Намерение без скрытой записи
Например, учебная кнопка «Взять» могла бы вызвать startRequested.emit(ticket().id). Тогда родитель связывает событие с ранее изученным start и сохраняет одно место изменения массива. Этот output не добавлен в конечную строку, потому что окончательный кабинет редактирует заявку на отдельной странице. Мы рассматриваем его как вариант границы, а не как обязательный файл архива. Если реализуете упражнение, явно назовите новый вход и событие и добавьте их только в нужное учебное состояние.
Название события лучше описывает произошедшее намерение пользователя. Оно не обещает успешную серверную запись. Когда диспетчер нажал кнопку, может оказаться, что версия устарела или роль не разрешает PATCH. Событие «сохранено» до ответа API смешало бы действие и результат. Даже в маленьком локальном компоненте полезно удерживать это различие: ребёнок сообщает действие, родитель или сервис решает последствия, а подтверждение приходит из соответствующей границы.
Рассмотрите результат после фильтра «проектор»: родитель создаёт одну строку и передаёт ей rq-101. После очистки фильтра появятся ещё две строки, но каждый экземпляр получает только свою заявку. Следующий этап вынесет общую зависимость из компонента через DI. Чтение input уже подготовило нужный способ мышления: зависимости объявляются явно, а ответственность не определяется тем, какой файл оказался самым близким к кнопке. Именно это позволяет сохранить управляемую структуру кабинета при добавлении запросов.
Наличие отдельной строки также облегчает изменение текста: можно показать version рядом с id и не повторять формат в нескольких циклах. Однако перенос formatting не создаёт новую границу доверия. Если title пришёл повреждённым, input Ticket не выполнит readTicket автоматически. Дочерний компонент вправе рассчитывать на уже проверенную модель лишь потому, что родитель получает её через согласованный источник. Значит, проверка unknown остаётся возле API, а повторяемое оформление — возле TicketRow. Они решают разные задачи и не требуют одинакового расположения кода.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.