Skip to content

Instantly share code, notes, and snippets.

@mzellho
Last active December 27, 2021 04:15
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mzellho/7fc7d5bd52a29948c47911a993547dad to your computer and use it in GitHub Desktop.
Save mzellho/7fc7d5bd52a29948c47911a993547dad to your computer and use it in GitHub Desktop.
// based on Алексей Сердюков's answer at Stackoverflow (https://stackoverflow.com/a/50837219/1143392)
import {
Directive,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { MediaObserver } from '@angular/flex-layout';
import { MatGridList } from '@angular/material';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ResponsiveColumnsMap {
xs?: number;
sm?: number;
md?: number;
lg?: number;
xl?: number;
}
// Usage: <mat-grid-list [responsiveColumns]="{xs: 1, sm: 2, md: 4, lg: 6, xl: 12}">
@Directive({
selector: '[responsiveColumns]'
})
export class ResponsiveColumnsDirective implements OnInit, OnDestroy {
private static readonly DEFAULT_COLUMNS_MAP: ResponsiveColumnsMap = {
xs: 1,
sm: 2,
md: 4,
lg: 6,
xl: 12
};
@Input() private responsiveColumns: ResponsiveColumnsMap;
private readonly watchers: Subscription[] = [];
constructor(private readonly grid: MatGridList,
private readonly mediaObserver: MediaObserver) {
}
ngOnInit(): void {
this.responsiveColumns = this.responsiveColumns || ResponsiveColumnsDirective.DEFAULT_COLUMNS_MAP;
this.initializeColsCount();
const mediaWatcher = this.mediaObserver.asObservable()
.pipe(
map(changes => {
const matchingAliases = changes.map(change => this.mapAlias(change.mqAlias))
// sort by number of columns desc
.sort((a, b) => this.responsiveColumns[ b ] - this.responsiveColumns[ a ])
// doublecheck
.filter(alias => Object.keys(this.responsiveColumns).includes(alias))
// triplecheck
.filter(alias => this.mediaObserver.isActive(alias));
const matchedAlias = matchingAliases.length > 0
? matchingAliases[ 0 ] // take the first matching alias (most cols)
: 'xs'; // default to xs
return this.responsiveColumns[ matchedAlias ];
})
).subscribe(cols => this.grid.cols = cols);
this.watchers.push(mediaWatcher);
}
ngOnDestroy(): void {
this.watchers
.forEach(watcher => watcher.unsubscribe());
}
private initializeColsCount(): void {
const matchingAliases = Object.keys(this.responsiveColumns)
// sort by number of columns desc
.sort((a, b) => this.responsiveColumns[ b ] - this.responsiveColumns[ a ])
// doublecheck
.filter(alias => this.mediaObserver.isActive(alias));
if (matchingAliases.length > 0) {
const firstMatchingAlias = matchingAliases[ 0 ];
this.grid.cols = this.responsiveColumns[ firstMatchingAlias ];
} else {
this.grid.cols = this.responsiveColumns.xs;
}
}
private mapAlias(mqAlias: string): string {
if (!mqAlias.includes('-')) {
return mqAlias;
}
const parts = mqAlias.split('-');
const ltOrGt = parts[ 0 ];
const alias = parts[ 1 ];
const keys = Object.keys(this.responsiveColumns);
const index = keys.indexOf(alias);
return ltOrGt === 'lt'
? keys[ index - 1 ]
: keys[ index + 1 ];
}
}
@williandrade
Copy link

for those with the following problem

ERROR Error: mat-grid-list: must pass in number of columns. Example: <mat-grid-list cols="3">

It is fixed, just a small change, the new version of Material Design ask for some initial value for the cols section, just that.

import {
    Directive,
    Input,
    OnDestroy,
    OnInit
} from '@angular/core';
import { MediaObserver } from '@angular/flex-layout';
import { MatGridList } from '@angular/material';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs/internal/Observable'

export interface ResponsiveColumnsMap {
    xs?: number;
    sm?: number;
    md?: number;
    lg?: number;
    xl?: number;
}

// Usage: <mat-grid-list [responsiveColumns]="{xs: 1, sm: 2, md: 4, lg: 6, xl: 12}">
@Directive({
    selector: '[responsiveColumns]'
})
export class ResponsiveColumnsDirective implements OnInit, OnDestroy {
    asyncValue: Observable<any[]>;

    private static readonly DEFAULT_COLUMNS_MAP: ResponsiveColumnsMap = {
        xs: 1,
        sm: 2,
        md: 4,
        lg: 6,
        xl: 12
    };

    @Input() private responsiveColumns: ResponsiveColumnsMap;

    private readonly watchers: Subscription[] = [];

    constructor(private readonly grid: MatGridList,
        private readonly mediaObserver: MediaObserver) {
            this.grid.cols = 0;
    }

    ngOnInit(): void {
        this.responsiveColumns = this.responsiveColumns || ResponsiveColumnsDirective.DEFAULT_COLUMNS_MAP;

        this.initializeColsCount();

        const mediaWatcher = this.mediaObserver.asObservable()
            .pipe(
                map(changes => {
                    const matchingAliases = changes.map(change => this.mapAlias(change.mqAlias))
                        // sort by number of columns desc
                        .sort((a, b) => this.responsiveColumns[b] - this.responsiveColumns[a])
                        // doublecheck
                        .filter(alias => Object.keys(this.responsiveColumns).includes(alias))
                        // triplecheck
                        .filter(alias => this.mediaObserver.isActive(alias));

                    const matchedAlias = matchingAliases.length > 0
                        ? matchingAliases[0]     // take the first matching alias (most cols)
                        : 'xs';                    // default to xs

                    return this.responsiveColumns[matchedAlias];
                })
            ).subscribe(cols => this.grid.cols = cols);

        this.watchers.push(mediaWatcher);
    }

    ngOnDestroy(): void {
        this.watchers
            .forEach(watcher => watcher.unsubscribe());
    }

    private initializeColsCount(): void {
        const matchingAliases = Object.keys(this.responsiveColumns)
            // sort by number of columns desc
            .sort((a, b) => this.responsiveColumns[b] - this.responsiveColumns[a])
            // doublecheck
            .filter(alias => this.mediaObserver.isActive(alias));

        if (matchingAliases.length > 0) {
            const firstMatchingAlias = matchingAliases[0];
            this.grid.cols = this.responsiveColumns[firstMatchingAlias];
        } else {
            this.grid.cols = this.responsiveColumns.xs;
        }
    }

    private mapAlias(mqAlias: string): string {
        if (!mqAlias.includes('-')) {
            return mqAlias;
        }

        const parts = mqAlias.split('-');
        const ltOrGt = parts[0];
        const alias = parts[1];

        const keys = Object.keys(this.responsiveColumns);
        const index = keys.indexOf(alias);

        return ltOrGt === 'lt'
            ? keys[index - 1]
            : keys[index + 1];
    }
}

@williandrade
Copy link

Also did one for tiles too, once that you can not have a tile with 4 colspan in a xs situation

import {
    Directive,
    Input,
    OnDestroy,
    OnInit
} from '@angular/core';
import { MediaObserver } from '@angular/flex-layout';
import { MatGridTile } from '@angular/material';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs/internal/Observable'

export interface ResponsiveColumnsMap {
    xs?: number;
    sm?: number;
    md?: number;
    lg?: number;
    xl?: number;
}

// Usage: <mat-grid-tile [responsiveGridTile]="{xs: 1, sm: 2, md: 2, lg: 3, xl: 6}">
@Directive({
    selector: '[responsiveGridTile]'
})
export class ResponsiveGridTileDirective implements OnInit, OnDestroy {
    asyncValue: Observable<any[]>;

    private static readonly DEFAULT_COLUMNS_MAP: ResponsiveColumnsMap = {
        xs: 1,
        sm: 2,
        md: 4,
        lg: 6,
        xl: 12
    };

    @Input() private responsiveGridTile: ResponsiveColumnsMap;

    private readonly watchers: Subscription[] = [];

    constructor(private readonly tile: MatGridTile,
        private readonly mediaObserver: MediaObserver) {
        
    }

    ngOnInit(): void {
        this.responsiveGridTile = this.responsiveGridTile || ResponsiveGridTileDirective.DEFAULT_COLUMNS_MAP;

        this.initializeColsCount();

        const mediaWatcher = this.mediaObserver.asObservable()
            .pipe(
                map(changes => {
                    const matchingAliases = changes.map(change => this.mapAlias(change.mqAlias))
                        // sort by number of columns desc
                        .sort((a, b) => this.responsiveGridTile[b] - this.responsiveGridTile[a])
                        // doublecheck
                        .filter(alias => Object.keys(this.responsiveGridTile).includes(alias))
                        // triplecheck
                        .filter(alias => this.mediaObserver.isActive(alias));

                    const matchedAlias = matchingAliases.length > 0
                        ? matchingAliases[0]     // take the first matching alias (most cols)
                        : 'xs';                    // default to xs

                    return this.responsiveGridTile[matchedAlias];
                })
            ).subscribe(cols => this.tile.colspan = cols);

        this.watchers.push(mediaWatcher);
    }

    ngOnDestroy(): void {
        this.watchers
            .forEach(watcher => watcher.unsubscribe());
    }

    private initializeColsCount(): void {
        const matchingAliases = Object.keys(this.responsiveGridTile)
            // sort by number of columns desc
            .sort((a, b) => this.responsiveGridTile[b] - this.responsiveGridTile[a])
            // doublecheck
            .filter(alias => this.mediaObserver.isActive(alias));

        if (matchingAliases.length > 0) {
            const firstMatchingAlias = matchingAliases[0];
            this.tile.colspan = this.responsiveGridTile[firstMatchingAlias];
        } else {
            this.tile.colspan = this.responsiveGridTile.xs;
        }
    }

    private mapAlias(mqAlias: string): string {
        if (!mqAlias.includes('-')) {
            return mqAlias;
        }

        const parts = mqAlias.split('-');
        const ltOrGt = parts[0];
        const alias = parts[1];

        const keys = Object.keys(this.responsiveGridTile);
        const index = keys.indexOf(alias);

        return ltOrGt === 'lt'
            ? keys[index - 1]
            : keys[index + 1];
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment