Динамическое управление адаптивностью с помощью Angular и Redux

Aleksandr Serenko
F.A.F.N.U.R
Published in
13 min readAug 13, 2019
Angular with dynamic responsive utils on SSR

Реализация адаптивности в современных web приложениях решается стандартными средствами CSS. Однако, возникают задачи, которые требуют изменения приложения взависимости от ширины экрана, и здесь уже стандартных возможностей CSS уже не хватает. В данной статье рассмотрим создание специальных pipe’ов и utils, которые помогут управлять адаптивностью на программном уровне, но в основе которых по-прежнему будет CSS адаптация с помощью Media queries.

Данная статья является частью курса статей, посвященных SSR, Redux, Nx (monorepo). Более подробно ознакомиться с курсом, можно перейдя по ссылкам, приведеным в конце данной статьи или перейдя по ссылке.

Базовые интерфейсы

Создадим библиотеку, которая будет отвечать за динамическую адаптацию.

ng g @nrwl/angular:lib responsive

Для реализации Responsive Library понадобятся следующие интерфесы:

import { Dictionary } from '@medium-stories/common';

/**
* Responsive mode
*/
export interface ResponsiveMode {
/**
* Is mobile size
*/
mobile: string;

/**
* Responsive dictionary
*/
sizes: Dictionary<number>;
}

/**
* Responsive properties
*/
export interface ResponsiveProperties {
[p: string]: any;

/**
* Window inner height
*/
height: number;

/**
* Is mobile
*/
mobile?: boolean;

/**
* Is mobile
*/
responsiveType?: string;

/**
* Window inner width
*/
width: number;
}

ResponsiveMode — задает типы, на которые будет программно реагировать Angular.

  • mobile — свойство, которое говорит с какого responsiveType устройство считается мобильным. Так как для адаптивности используется подход Mobile first, то проверка будет осуществляться в виде: isMobile = responsiveType ≤mobile.
  • sizes — словарь, в котором ключи выступают в качестве responsiveType, а соответствующие значения будут задавать применимую ширину*.

Интерфейс ResponsiveProperties хранит в себе параметры, которые необходимы для адаптивности. В базой реализации, будет достаточно ширины, высоты, и считается ли устройство с данными размерами мобильным.

Базовые сервисы

С форматом данных определились, теперь создадим сервисы для работы с адаптивностью.

Первым сервисом, будет являться ResponsiveStorage, который будет сохранять значения высоты и ширины в cookie.

ResponsiveStorage сделан для того, чтобы при SSR Angular Universal мог отрендерить шаблоны с корректной логикой, и в браузере не было повторного рендера и соответвующего моргания. Но если вы не используете SSR, можете смело удалять данный сервис.

import { ResponsiveProperties } from './responsive.interface';

/**
* Responsive storage interface
*/
export abstract class ResponsiveStorage {
/**
* Return window properties
*/
abstract getProperties(): ResponsiveProperties;

/**
* Remove window properties
*/
abstract removeProperties(): void;

/**
* Set window properties
*/
abstract setProperties(props: Partial<ResponsiveProperties>): void;
}

И соответвующая имплементация

import { Injectable } from '@angular/core';

import { CookieStorage } from '@medium-stories/storage';

import { ResponsiveStorage } from '../interfaces/responsive-storage.interface';
import { ResponsiveProperties } from '../interfaces/responsive.interface';

/**
* Keys for storage
*/
export const RESPONSIVE_STORAGE_KEYS = {
height: 'msthght',
mobile: 'mstmbl',
responsiveType: 'mstrspt',
width: 'mstwdth'
};

@Injectable()
export class BaseResponsiveStorage implements ResponsiveStorage {
readonly defaultProperties: ResponsiveProperties = { height: 0, mobile: false, width: 0 };

constructor(protected storage: CookieStorage) {}

getProperties(): ResponsiveProperties {
const props = { ...this.defaultProperties };
const height = this.storage.getItem(RESPONSIVE_STORAGE_KEYS.height);
if (!Number.isNaN(+height)) {
props.height = +height;
}
const width = this.storage.getItem(RESPONSIVE_STORAGE_KEYS.width);
if (!Number.isNaN(+width)) {
props.width = +width;
}
const mobile = this.storage.getItem(RESPONSIVE_STORAGE_KEYS.mobile);
props.mobile = mobile === 'true';

return props;
}

removeProperties(): void {
for (const key of Object.values(RESPONSIVE_STORAGE_KEYS)) {
this.storage.removeItem(key);
}
}

setProperties(properties: Partial<ResponsiveProperties>): void {
for (const key of Object.keys(RESPONSIVE_STORAGE_KEYS)) {
if (key in properties) {
this.storage.setItem(RESPONSIVE_STORAGE_KEYS[key], properties[key].toString());
}
}
}
}

Вторым, и основным сервисом является ResponsiveService, который сожержит определение текущего responsiveType, да и в целом реализует всю адаптивную логику.

import { ResponsiveProperties } from './responsive.interface';

/**
* Responsive Service
*/
export abstract class ResponsiveService {
/**
* Check is between for rules
*
@param type current type
*
@param expressions collection expressions
*/
abstract checkBetween(type: string, expressions: string[]): boolean;

/**
* Check is equal for rules
*
@param type current type
*
@param expressions collection expressions (union equivalents expression: sm or md Or lg ...)
*/
abstract checkEqual(type: string, expressions: string[]): boolean;

/**
* Check is max for rules
*
@param type current type
*
@param expressions collection expressions
*/
abstract checkMax(type: string, expressions: string[]): boolean;

/**
* Check is min for rules
*
@param type current type
*
@param expressions collection expressions
*/
abstract checkMin(type: string, expressions: string[]): boolean;

/**
* Return changed state props for changed window props
*
@param props Window properties
*/
abstract getChangesByProperties(props: Partial<ResponsiveProperties>): Partial<ResponsiveProperties>;

/**
* Return responsive properties for state
*/
abstract getResponsiveProperties(): ResponsiveProperties;

/**
* Return responsive type by window width
*
@param width window width
*/
abstract getResponsiveTypeByWidth(width: number): string;

/**
* Check is mobile width
*
@param width window width
*/
abstract isMobile(width: number): boolean;
}

Опишем основые методы ResponsiveService:

  • isMobile — метод, который возвращает является ли устройство с данной шириной мобильным
  • getResponsiveTypeByWidth — метод, который по заднной ширине возвращает responsiveType
  • getInitialProperties — метод, который возвращает начальные значения для Responsive State
  • getChangesByProperties — метод, который при изменении ширины или высоты, вычисляет новые значения для Responsive State.
  • checkMin, checkMax, checkBetween, checkEqual — методы, аналогичниые работе media queries, таких как min-width and max-width
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';

import { ResponsiveState } from '../+state/responsive.reducer';
import { ResponsiveMode, ResponsiveProperties } from '../interfaces/responsive.interface';
import { ResponsiveService } from '../interfaces/responsive-service.interface';
import { ResponsiveStorage } from '../interfaces/responsive-storage.interface';
import { RESPONSIVE_MODE } from '../responsive.tokens';

@Injectable()
export class BaseResponsiveService implements ResponsiveService {
constructor(
protected responsiveStorage: ResponsiveStorage,
@Inject(RESPONSIVE_MODE) protected responsiveMode: ResponsiveMode,
@Inject(PLATFORM_ID) protected platformId: any
) {}

checkBetween(type: string, expressions: string[]): boolean {
let result = false;
expressions.forEach(expression => {
const args: string[] = expression.split(',');

if (
args.length === 2 &&
this.responsiveMode.sizes[type] >= this.responsiveMode.sizes[args[0]] &&
this.responsiveMode.sizes[type] < this.responsiveMode.sizes[args[1]]
) {
result = true;
}
});

return result;
}

checkEqual(type: string, expressions: string[]): boolean {
let result = false;
expressions.forEach(expression => {
const args = expression.split(',');

if (args.length === 1 && args[0] === type) {
result = true;
}
});

return result;
}

checkMax(type: string, expressions: string[]): boolean {
let result = false;
expressions.forEach(expression => {
const args = expression.split(',');

if (args.length === 1 && this.responsiveMode.sizes[type] < this.responsiveMode.sizes[args[0]]) {
result = true;
}
});

return result;
}

checkMin(type: string, expressions: string[]): boolean {
let result = false;
expressions.forEach(expression => {
const args = expression.split(',');

if (args.length === 1 && this.responsiveMode.sizes[type] >= this.responsiveMode.sizes[args[0]]) {
result = true;
}
});

return result;
}

getChangesByProperties(props: Partial<ResponsiveProperties>): Partial<ResponsiveProperties> {
const changes: Partial<ResponsiveState> = {};

if ('height' in props) {
changes.height = props.height;
}

if ('width' in props) {
changes.width = props.width;
changes.mobile = this.isMobile(props.width);
changes.responsiveType = this.getResponsiveTypeByWidth(props.width);
}
// Save properties on cookie storage
this.responsiveStorage.setProperties(changes);

return changes;
}

getResponsiveProperties(): ResponsiveProperties {
let props: ResponsiveProperties;
if (isPlatformBrowser(this.platformId)) {
props = this.getChangesByProperties(
this.getChangesByProperties({ height: window.innerHeight, width: window.innerWidth })
) as ResponsiveProperties;
this.responsiveStorage.setProperties(props);
} else {
props = this.responsiveStorage.getProperties();
}

return props;
}

getResponsiveTypeByWidth(width: number): string {
let responseType: string = null;
const sizes = Object.entries(this.responsiveMode.sizes).sort((a, b) => b[1] - a[1]);
for (const [type, size] of sizes) {
if (width >= size) {
responseType = type;
break;
}
}
if (!responseType) {
responseType = sizes[sizes.length - 1][0];
}

return responseType;
}

isMobile(width: number): boolean {
return width < this.responsiveMode.sizes[this.responsiveMode.mobile];
}
}

Создание Adaptive Pipe

Так как основной функционал уже реализован, добавим pipe’ы, с помощью которых будет возможно управлять содержимым.

Первым добавим AdaptivePipe, который для заданного adaptive expression и текущего responsiveType будет возвращать true/false.

import { Pipe, PipeTransform } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { ResponsiveFacade } from '../+state/responsive.facade';
import { AdaptiveMode } from '../interfaces/adaptive.interface';
import { ResponsiveService } from '../interfaces/responsive-service.interface';

@Pipe({
name: 'adaptive'
})
export class AdaptivePipe implements PipeTransform {
constructor(private responsiveService: ResponsiveService, private responsiveFacade: ResponsiveFacade) {}

/**
* Example use 'sm,md|hd,xh' | adaptive:'between' | async
*
*
@param expression Adaptive Expression
*
@param mode Adaptive mode
*/
transform(expression: string, mode?: AdaptiveMode | string): Observable<boolean> {
if (typeof expression !== 'string') {
return of(false);
}

if (!mode || typeof mode !== 'string') {
mode = AdaptiveMode.Equal;
}
const expressions = expression.split('|');

return this.responsiveFacade.responsiveType$.pipe(
map<string, boolean>(type => {
let result = false;
switch (mode) {
case AdaptiveMode.Min:
result = this.responsiveService.checkMin(type, expressions);
break;
case AdaptiveMode.Max:
result = this.responsiveService.checkMax(type, expressions);
break;
case AdaptiveMode.Between:
result = this.responsiveService.checkBetween(type, expressions);
break;
case AdaptiveMode.Equal:
default:
result = this.responsiveService.checkEqual(type, expressions);
}

return result;
})
);
}
}

Как видно из кода, pipe принимает adaptive expression и adaptive mode и производит вычисление.

  • adaptive expression — строковое выражение, которое сожержит responsive тайпы и условие проверки
  • adaptive mode — тип применения условия к выражению
  • responsiveType$ — значение текущего responsiveType в State

Примеры использования Adaptive Expression

Для отображение блока, только в размере sm:

<ng-container *ngIf=" 'sm' | adaptive: 'equal' | async ">
Content ...
</ng-container>

Для отображения начиная с md:

<ng-container *ngIf=" 'md' | adaptive: 'min' | async ">
Content ...
</ng-container>

Для отображения до md:

<ng-container *ngIf=" 'md' | adaptive: 'max' | async ">
Content ...
</ng-container>

Для отображения начиная с sm до md:

<ng-container *ngIf=" 'sm,md' | adaptive: 'between' | async ">
Content ...
</ng-container>

Выражение могут быть групповыми для equal и between, но для min и max будет применено максимальное, минимальное значение.

<ng-container *ngIf=" 'sm,md|xl,hg' | adaptive: 'between' | async ">
Content ...
</ng-container>
<ng-container *ngIf=" 'sm|lg' | adaptive: 'equal' | async ">
Content ...
</ng-container>

Второй пайп, это AdaptiveSyncPipe. В отличии от AdaptivePipe, который был асинхронным из-за привязки к текущему значению responsiveType из State’а, то AdaptiveSyncPipe зависит от сохраненного значения в storage, в данном случае в куках.

import { Pipe, PipeTransform } from '@angular/core';

import { AdaptiveMode } from '../interfaces/adaptive.interface';
import { ResponsiveService } from '../interfaces/responsive-service.interface';
import { ResponsiveStorage } from '../interfaces/responsive-storage.interface';

@Pipe({
name: 'adaptiveSync'
})
export class AdaptiveSyncPipe implements PipeTransform {
constructor(private responsiveService: ResponsiveService, private responsiveStorage: ResponsiveStorage) {}

/**
* Example use 'sm,md|hd,xh' | adaptiveSync:'between'
*
*
@param expression Adaptive Expression
*
@param mode Adaptive mode
*/
transform(expression: string, mode?: AdaptiveMode | string): boolean {
let result = false;

if (typeof expression === 'string') {
const type = this.getType();

if (!mode || typeof mode !== 'string') {
mode = AdaptiveMode.Equal;
}
const expressions = expression.split('|');

switch (mode) {
case AdaptiveMode.Min:
result = this.responsiveService.checkMin(type, expressions);
break;
case AdaptiveMode.Max:
result = this.responsiveService.checkMax(type, expressions);
break;
case AdaptiveMode.Between:
result = this.responsiveService.checkBetween(type, expressions);
break;
case AdaptiveMode.Equal:
default:
result = this.responsiveService.checkEqual(type, expressions);
}
}

return result;
}

/**
* Return responsiveType from storage
*/
private getType(): string {
const props = this.responsiveStorage.getProperties();

return props.responsiveType || this.responsiveService.getResponsiveTypeByWidth(props.width);
}
}

Создание Responsive State

Сгенерируем state для библиотеки, назвав его responsive.

ng g @nrwl/angular:ngrx responsive --module=libs/responsive/src/lib/responsive-common.module.ts

Так как основные параметры для адаптации это window width и height, то создадим action’ы для задания данных свойств.

import { Action } from '@ngrx/store';

import { ResponsiveProperties } from '../interfaces/responsive.interface';

export enum ResponsiveActionTypes {
InitWindowProps = '[Responsive] Init window properties',
WindowPropsInitCanceled = '[Responsive] Window properties inited',
InitiatingWindowProps = '[Responsive] Initiating window properties',
WindowPropsInitialized = '[Responsive] Window properties initialized',
WindowPropsInitError = '[Responsive] Window properties init error',

ChangeWindowProps = '[Responsive] Change window properties',
SetWindowProps = '[Responsive] Set window properties'
}

export class InitWindowProps implements Action {
readonly type = ResponsiveActionTypes.InitWindowProps;

constructor(public payload?: boolean) {}
}

export class WindowPropsInitCanceled implements Action {
readonly type = ResponsiveActionTypes.WindowPropsInitCanceled;
}

export class InitiatingWindowProps implements Action {
readonly type = ResponsiveActionTypes.InitiatingWindowProps;
}

export class WindowPropsInitialized implements Action {
readonly type = ResponsiveActionTypes.WindowPropsInitialized;

constructor(public payload: ResponsiveProperties) {}
}

export class WindowPropsInitError implements Action {
readonly type = ResponsiveActionTypes.WindowPropsInitError;

constructor(public payload: string) {}
}

export class SetWindowProps implements Action {
readonly type = ResponsiveActionTypes.SetWindowProps;

constructor(public payload: object) {}
}

export type ResponsiveAction =
| InitWindowProps
| WindowPropsInitCanceled
| InitiatingWindowProps
| WindowPropsInitialized
| WindowPropsInitError
| SetWindowProps;

export const fromResponsiveActions = {
InitWindowProps,
WindowPropsInitCanceled,
InitiatingWindowProps,
WindowPropsInitialized,
WindowPropsInitError,
SetWindowProps
};

Создадим responsiveReducer

import { ResponsiveActionTypes, ResponsiveAction } from './responsive.actions';

export const RESPONSIVE_FEATURE_KEY = 'responsive';

/**
* Responsive state
*/
export interface ResponsiveState {
/**
* Current Window inner height
*/
height: number;

/**
* Responsive init error
*/
initError: string;

/**
* Responsive initialized
*/
initialized: boolean;

/**
* Responsive is initialized
*/
initiating: boolean;

/**
* Current Window inner width
*/
width: number;

/**
* Is mobile width
* Notice: Now if width < 768 then true. May need to be changed later.
*/
mobile: boolean;

/**
* Current responsive type
*/
responsiveType: string;
}

export const responsiveInitialState: ResponsiveState = {
height: 0,
initError: null,
initialized: false,
initiating: false,
width: 0,
mobile: true,
responsiveType: null
};

export interface ResponsivePartialState {
readonly [RESPONSIVE_FEATURE_KEY]: ResponsiveState;
}

export function responsiveReducer(state: ResponsiveState = responsiveInitialState, action: ResponsiveAction): ResponsiveState {
switch (action.type) {
case ResponsiveActionTypes.InitiatingWindowProps:
state = {
...state,
initError: null,
initiating: true
};
break;
case ResponsiveActionTypes.WindowPropsInitialized:
state = {
...state,
...action.payload,
initialized: true,
initiating: false
};
break;
case ResponsiveActionTypes.WindowPropsInitError:
state = {
...state,
initError: action.payload,
initialized: true,
initiating: false
};
break;
case ResponsiveActionTypes.SetWindowProps:
state = {
...state,
...action.payload
};
break;
}

return state;
}

В Responsive State будем хранить следующие значения:

  • height — текущая высота window
  • initError — ошибка при инициализации Responsive State
  • initialized — статус, что Responsive State был инициализирован
  • initiating — статус, что Responsive State находится в состоянии инициализации
  • mobile — является ли устройство с заданной шириной мобильным
  • responsiveType — текущий responsiveType, который определяется из словаря, который задается через Inject
  • width — текущая ширина window

Reducer содержит реакцию только на action’ы которые изменяют ширину, и как можно заметить из реализации ResponsiveService, при каждом изменении хотя бы одного свойтва, идет пересчет свойств — responsiveType, mobile.

Добавим эффекты для инициализации Responsive State, а также события на установку новых размеров.

import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/angular';

import { Debounce } from '@medium-stories/common';
import { AbstractEffects } from '@medium-stories/store';

import { ResponsiveService } from '../interfaces/responsive-service.interface';
import {
InitiatingWindowProps,
InitWindowProps,
ResponsiveActionTypes,
SetWindowProps,
WindowPropsInitCanceled,
WindowPropsInitError,
WindowPropsInitialized
} from './responsive.actions';
import { RESPONSIVE_FEATURE_KEY, ResponsivePartialState, ResponsiveState } from './responsive.reducer';

@Injectable()
export class ResponsiveEffects extends AbstractEffects<ResponsiveState> {
@Effect() init$ = this.dataPersistence.fetch(ResponsiveActionTypes.InitWindowProps, {
run: (action: InitWindowProps, store: ResponsivePartialState) => {
return !this.getState(store).initiating || action.payload ? new InitiatingWindowProps() : new WindowPropsInitCanceled();
},
onError: (action: InitWindowProps, error) => console.error(error.toString())
});

@Effect() initiating$ = this.dataPersistence.fetch(ResponsiveActionTypes.InitiatingWindowProps, {
run: (action: InitiatingWindowProps, store: ResponsivePartialState) => {
if (isPlatformBrowser(this.platformId)) {
this.eventManager.addGlobalEventListener('window', 'resize', event => this.windowResizeHandler(event));
}

return new WindowPropsInitialized(this.responsiveService.getResponsiveProperties());
},
onError: (action: InitiatingWindowProps, error) => new WindowPropsInitError(error.toString())
});

constructor(
private actions$: Actions,
private dataPersistence: DataPersistence<ResponsivePartialState>,
private eventManager: EventManager,
private responsiveService: ResponsiveService,
/* tslint:disable-next-line:ban-types */
@Inject(PLATFORM_ID) private platformId: Object
) {
super(RESPONSIVE_FEATURE_KEY);
}

@Debounce(100)
windowResizeHandler(event: any): void {
const window = event.target as Window;
const props = this.responsiveService.getChangesByProperties({
height: window.innerHeight,
width: window.innerWidth
});
this.dataPersistence.store.dispatch(new SetWindowProps(props));
}
}

Единственным замечанием, можно вынести, что в процессе инициализации для браузерной платформы создается событие, которое при resize window вызывает событие изменения размеров.

Добавим в ResponsiveFacade методы для инициализации и явного вызова изменения размеров

import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';

import { ResponsiveProperties } from '../interfaces/responsive.interface';
import { ResponsiveService } from '../interfaces/responsive-service.interface';
import { fromResponsiveActions } from './responsive.actions';
import { ResponsivePartialState } from './responsive.reducer';
import { responsiveQuery } from './responsive.selectors';

@Injectable()
export class ResponsiveFacade {
/**
* Observed height
*/
height$ = this.store.pipe(select(responsiveQuery.getHeight));

/**
* Observed initialized
*/
initialized$ = this.store.pipe(select(responsiveQuery.getInitialized));

/**
* Observed has mobile
*/
mobile$ = this.store.pipe(select(responsiveQuery.getMobile));

/**
* Observed properties
*/
props$ = this.store.pipe(select(responsiveQuery.getProperties));

/**
* Observed responsive type
*/
responsiveType$ = this.store.pipe(select(responsiveQuery.getResponsiveType));

/**
* Observed width
*/
width$ = this.store.pipe(select(responsiveQuery.getWidth));

constructor(private store: Store<ResponsivePartialState>, private responsiveService: ResponsiveService) {}

/**
* Init responsive
*
@param payload Force
*/
init(payload?: boolean): void {
this.store.dispatch(new fromResponsiveActions.InitWindowProps(payload));
}

/**
* Set window properties
*
@param props Window properties
*/
setProps(props: Partial<ResponsiveProperties>): void {
const payload = this.responsiveService.getChangesByProperties(props);

this.store.dispatch(new fromResponsiveActions.SetWindowProps(payload));
}
}

Демонстрация

Подключим ResponsiveModule в HomeModule

include: [
...
ResponsiveModule,
...
]

Добавим несколько шаблонов и условий для демонстрации в home.component.html

<mat-card class="responsive-card">
<mat-card-header>
<mat-card-title>{{ 'responsive.title' | translate }}</mat-card-title>
</mat-card-header>

<mat-card-content>
<div *ngIf="'sm' | adaptive | async">
<h3>SM equal</h3>
</div>

<div *ngIf="'md' | adaptive: 'min' | async">
<h3>MD min</h3>
</div>

<div *ngIf="'lg' | adaptive: 'max' | async">
<h3>LG max</h3>
</div>

<div *ngIf="'xl,hg' | adaptive: 'between' | async">
<h3>XL,HG between</h3>
</div>
</mat-card-content>
</mat-card>

Запустим и посмотрим, как работает responsive library

ng serve frontend-responsive

Как можно увидеть из видео, при изменении размера экрана, происходит изменение страницы в зависимости от установленной логики.

На певром скриншоте можно увидеть, что отображается только блол lg max.

Angular responsive

На втором скриншоте, видно отображение блока, который показывается только в sm.

Angular responsive

На третьем скриншоте, блок sm пропадает, но начинает показываться блок, для отображение которого responsiveType должен быть не меньше md.

Angular responsive

На четвертом скнишоте видно, что пропал блок c lg, у которого ограничение на показ до lg.

Angular responsive

На пятом скриншоте, можно увидеть отображение блока, котоый показывается только от XL до HG.

Angular responsive

На последнем скриншоте можно видеть, что остался блок, который отображается от md.

Angular responsive

Работоспособность в Angular Universal

Продублируем блок с демонстрацией и добавим adaptiveSync pipe

<mat-card class="responsive-card">
<mat-card-header>
<mat-card-title>{{ 'responsive.titleSync' | translate }}</mat-card-title>
</mat-card-header>

<mat-card-content>
<div *ngIf="'sm' | adaptiveSync">
<h3>SM equal</h3>
</div>

<div *ngIf="'md' | adaptiveSync: 'min'">
<h3>MD min</h3>
</div>

<div *ngIf="'lg' | adaptiveSync: 'max'">
<h3>LG max</h3>
</div>

<div *ngIf="'xl,hg' | adaptiveSync: 'between'">
<h3>XL,HG between</h3>
</div>
</mat-card-content>
</mat-card>

Сбилдим SSR версию и запустим

yarn run ng build frontend-responsive --prod && ng run frontend-responsive:server:production && webpack --config apps/frontend/responsive/webpack.ssr.config.js --progress --colorsyarn run node dist/apps/frontend/responsive/server.js
Angular responsive Universal and SSR

Как и ожидалось, у обоих pipe’ов одинаковое поведение.

Заметим, так как в проекте используется changeDetection: ChangeDetectionStrategy.OnPush, то для того чтобы adaptiveSync pipe мог получать уведомления о изменении, необходимо установить pure — false.

Теперь если открыть исходный код, то можно увидеть, что adaptive pipe просто пригнорировал все и ничего не отрендерил. Но это нормальное поведение, так как если async pipe действительно отрабатывал как должен, то загрузка страницы занимала бы несколько минут.

Однако, adaptiveSync, благодаря значениям, которые были сохранены в cookie, отрисовал соответсвующие блоки.

Angular responsive Universal and SSR sources

Заключение

Приведенные решение не являются панацеей от всех болезней, но предоставляют гибкое решение для динамического управления адаптивностью приложения.

Исходники

Все исходники находятся на github, в репозитории https://github.com/Fafnur/medium-stories

Демо можно посмотреть в проекте frontend/responsive.

Для того, чтобы увидеть проект на момент написания статьи, нужно выбрать соответвующий tag — responsive.

git checkout responsive

Предыдущие статьи:

  1. Статья про LocalStorage, SessionStorage в Angular Universal
  2. Статья про мультиязычность в Angular & Universal с помощью ngx-translate
  3. Статья про Redux в Angular с помощью Ngrx. Создание Store в Angular
  4. Статья про Настройка CSS предпроцессора в Angular с Nx
  5. Интеграция CSS framework’ов в Angular или темезируем Angular с помощью Material и Bootstrap

Следующие статьи:

  1. Статья про Создание вспомогательных директив в Angular для упрощения верстки

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock