Любой более менее опытный фронтендер, работающий с Angular, умеет пользоваться роутером. Тут путь. Здесь компонент. Не забудь положить router-outlet в темплейт в нужном месте и вуаля.
И это покрывает 95% всех кейсов любого приложения. Остальное можно подпереть костылями. Одни из них хрупки, как китайский фарфор. Другие вполне себе претендуют на решение, достойное самого ядра приложения.
Давайте представим не такой уж редкий случай: онлайн магазин, выбрали покупки, посмотрели корзину, приступаем к оформлению.
И тут дизайнер поменял наркотики: все формы оформления заказа отрисованы в диалоговом окне. На вопрос “Зачем?” получаем отсутствующий взгляд и глупое хихиканье. Заказчик не добавляет позитива и соглашается во всем с дизайнером.
В позитивном сценарии мы прошли все шаги, безошибочно заполнив данные и последовали к подтверждению.
Однако, может случится так, что на этапе конфирмации диагностировали болезнь толстых пальцев. Надо вернуться и исправить.
И все бы ничего, но мы в диалоговом окне. Хорошо, если у нас есть жирнющая кнопка "Назад". Но пользовательский опыт пальцем не задавишь: кнопки браузерной навигации - самое понятное пользователю действие.
// app-routing.module.ts
const routes = [
...
{
path: 'order',
component: OrderComponent,
// можно в гарды положить логику открытия/закрытия диалога
// и убрать из инита/дестроя - дело вкуса
children: [
path: '**',
component: null
]
}
];
// order.component.html
<router-outlet></router-outlet>
// order.component.ts
...
ngOnInit(): void {
this._ref = this._dialog.open(OrderDetailsComponent, {
data: { activatedRoute: this._route }
...
});
}
// order-details.component.ts - компонент, который будет отрисован в диалоге
// imports
const STEP_MAP = {
'personal-details': PersonalDetailsComponent,
'delivery-details': DeliveryDetailsComponent,
'payment-details': PaymentDetailsComponent,
'order-confirmation': OrderConfirmationComponent
};
@Component({
selector: 'app-order-details',
template: '<ng-container #ref></ng-container>'
styleUrls: ['./order-details.scss']
})
export class OrderDetails implements OnInit, OnDestroy {
private _destroyed$ = new Subject<void>();
@ViewChild('ref', { read: ViewContainerRef }) private _vcr: ViewContainerRef:
constructor(
private _vcr: ViewContainerRef,
private _router: Router,
@Inject(MAT_DIALOG_DATA) private _d: unknown
) {}
ngOnInit(): void {
this._router.events
.pipe(
filter(evt => evt instanceof NavigationEnd),
map(evt => {
const tree = this._router.parseUrl(this._router.url);
const { segments } = tree.root.children.primary;
return segments[segments.length - 1]?.path;
}),
distinctUntilChanged(),
takeUntil(this._destroyed$)
)
.subscribe(this._renderStep.bind(this));
// start with first step
this._router.navigate(['personal-details'], { relativeTo: this._d.activatedRoute });
}
ngOnDestroy(): void {
this._destroyed$.next();
this._destroyed$.complete();
}
private _renderStep(stepId: keyof typeof STEP_MAP): void {
...
}
}
Достаточно прямолинейно, что уже хорошо. Компонент OrderDetailsComponent можно генерализировать во что-то переиспользуемое. Но что в этом всем плохо?
// app-routing.module.ts
const routes = [
...
{
path: 'order',
component: OrderComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'personal-details'
},
{
... конфигурируем остальные роуты
}
]
}
];
// order.component.html
<ng-template><router-outlet></router-outlet></ng-template>
// order.component.ts
@ViewChild(TemplateRef) _tpl!: TemplateRef<unknown>;
...
ngAfterViewInit(): void {
this._ref = this._dialog.open(this._tpl);
}
ngOnDestroy(): void {
this._ref?.close();
}
Весь трюк в том, чтобы router-outlet ворочался в диалоговом окне. Объявляем его в компоненте-плейсхолдере OrderComponent внутри ng-template, забираем после инициализации вьюхи наш темплейт и тулим его сразу в диалог.
И это покрывает 95% всех кейсов любого приложения. Остальное можно подпереть костылями. Одни из них хрупки, как китайский фарфор. Другие вполне себе претендуют на решение, достойное самого ядра приложения.
Давайте представим не такой уж редкий случай: онлайн магазин, выбрали покупки, посмотрели корзину, приступаем к оформлению.
И тут дизайнер поменял наркотики: все формы оформления заказа отрисованы в диалоговом окне. На вопрос “Зачем?” получаем отсутствующий взгляд и глупое хихиканье. Заказчик не добавляет позитива и соглашается во всем с дизайнером.
Что надо сделать?
По сути, надо просто заполнять пошагово формы.В позитивном сценарии мы прошли все шаги, безошибочно заполнив данные и последовали к подтверждению.
Однако, может случится так, что на этапе конфирмации диагностировали болезнь толстых пальцев. Надо вернуться и исправить.
И все бы ничего, но мы в диалоговом окне. Хорошо, если у нас есть жирнющая кнопка "Назад". Но пользовательский опыт пальцем не задавишь: кнопки браузерной навигации - самое понятное пользователю действие.
Включаем костыли
Первый подход (интуитивный): создаем компонент-контейнер (OrderDetailsComponent) (aka router-outlet на минималках) и начинаем следить за роутером и подменять вьюхи соответственно.// app-routing.module.ts
const routes = [
...
{
path: 'order',
component: OrderComponent,
// можно в гарды положить логику открытия/закрытия диалога
// и убрать из инита/дестроя - дело вкуса
children: [
path: '**',
component: null
]
}
];
// order.component.html
<router-outlet></router-outlet>
// order.component.ts
...
ngOnInit(): void {
this._ref = this._dialog.open(OrderDetailsComponent, {
data: { activatedRoute: this._route }
...
});
}
// order-details.component.ts - компонент, который будет отрисован в диалоге
// imports
const STEP_MAP = {
'personal-details': PersonalDetailsComponent,
'delivery-details': DeliveryDetailsComponent,
'payment-details': PaymentDetailsComponent,
'order-confirmation': OrderConfirmationComponent
};
@Component({
selector: 'app-order-details',
template: '<ng-container #ref></ng-container>'
styleUrls: ['./order-details.scss']
})
export class OrderDetails implements OnInit, OnDestroy {
private _destroyed$ = new Subject<void>();
@ViewChild('ref', { read: ViewContainerRef }) private _vcr: ViewContainerRef:
constructor(
private _vcr: ViewContainerRef,
private _router: Router,
@Inject(MAT_DIALOG_DATA) private _d: unknown
) {}
ngOnInit(): void {
this._router.events
.pipe(
filter(evt => evt instanceof NavigationEnd),
map(evt => {
const tree = this._router.parseUrl(this._router.url);
const { segments } = tree.root.children.primary;
return segments[segments.length - 1]?.path;
}),
distinctUntilChanged(),
takeUntil(this._destroyed$)
)
.subscribe(this._renderStep.bind(this));
// start with first step
this._router.navigate(['personal-details'], { relativeTo: this._d.activatedRoute });
}
ngOnDestroy(): void {
this._destroyed$.next();
this._destroyed$.complete();
}
private _renderStep(stepId: keyof typeof STEP_MAP): void {
...
}
}
Достаточно прямолинейно, что уже хорошо. Компонент OrderDetailsComponent можно генерализировать во что-то переиспользуемое. Но что в этом всем плохо?
- не масштабируемо. Все, что не уложится в линейную схему роутинга, будет вызывать зубную боль;
- парсинг урла будет работать условно хорошо только на один уровень. Дальше - мрак;
- в _renderStep мы фактически пишем свой роутер, только без блэкджека и шл...
- конфигурация роутинга нам ни о чем не расскажет. Нужно лезть внутрь компонентов, чтобы понять, что и как устроено.
// app-routing.module.ts
const routes = [
...
{
path: 'order',
component: OrderComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'personal-details'
},
{
... конфигурируем остальные роуты
}
]
}
];
// order.component.html
<ng-template><router-outlet></router-outlet></ng-template>
// order.component.ts
@ViewChild(TemplateRef) _tpl!: TemplateRef<unknown>;
...
ngAfterViewInit(): void {
this._ref = this._dialog.open(this._tpl);
}
ngOnDestroy(): void {
this._ref?.close();
}
Весь трюк в том, чтобы router-outlet ворочался в диалоговом окне. Объявляем его в компоненте-плейсхолдере OrderComponent внутри ng-template, забираем после инициализации вьюхи наш темплейт и тулим его сразу в диалог.
Выводы
Никакой смены парадигмы, работаем в тех же координатах.Роутим диалоги в Angular + Material
Любой более менее опытный фронтендер, работающий с Angular, умеет пользоваться роутером. Тут путь. Здесь компонент. Не забудь положить router-outlet в темплейт в нужном месте и вуаля. И это покрывает...
habr.com