Skip to content

Instantly share code, notes, and snippets.

@mzellho
Last active December 27, 2021 04:15
Show Gist options
  • 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 ];
}
}
@WetHippie
Copy link

Nice example! Pretty new to Angular 2+. This works as expected in responsive style, but getting a minor annoying error on the console when the page first loads (no impact on actual running code):

LogbookSummaryComponent.html:1 ERROR Error: mat-grid-list: must pass in number of columns. Example: <mat-grid-list cols="3"> at MatGridList._checkCols (grid-list.js:747) at MatGridList.ngOnInit (grid-list.js:729)

Guessing something has changed in the event model between Angular 7 and 8. I'm using 8.1.2 currently. There's an answer following yours on that StackOverflow thread talking about this error and a possible solution.

@mzellho
Copy link
Author

mzellho commented Aug 5, 2019

@WetHippie: Thanks, but the kudos should definitely go to Алексей Сердюков.

I just checked the Stackoverflow thread - I would really love to see an asynchronousl solution using a Subject and the Async Pipe. For the Directive-based approach though, I think you would have to initialize the cols attribute somehow. Personally, I would pick the xs-Option, but probably this could cause some flickering / component resizing.

On the road right now, so no real chance to test it, but in case you find out something, I would be very happy if you could ping me :-)

@romelgomez
Copy link

romelgomez commented Dec 17, 2019

Hi @mzellho

I take some parts of the directive in order to generate a string to set gdColumns attr of fxLayout directive, but like how you implemented with the directive, is it a very simple and reusable solution, but I don't known how to set it for fxLayout only. Here I'm removing MatGridList.

fxLayout ="row" fxLayoutAlign="space-between none" fxLayoutGap="4px" [gdColumns]="gdColumns"

With this solution I can get a responsive Grid of tree, two, one, columns, without use MatGridListModule

https://stackblitz.com/edit/angular-nfng3n

blog-posts-partial.component.ts

import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { MediaObserver } from '@angular/flex-layout';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';


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

type PostCategory = |
  'events' |
  'interviews' |
  'featured';

interface Posts {
  uuid: string;
  title: string;
  body: string;
  thumbnail: string;
  category: PostCategory;
  published: string;
}

@Component({
  selector: 'app-blog-posts-partial',
  templateUrl: './blog-posts-partial.component.html',
  styleUrls: ['./blog-posts-partial.component.scss'],
})
export class BlogPostsPartialComponent implements OnInit, OnDestroy {

  private static readonly DEFAULT_COLUMNS_MAP: ResponsiveColumnsMap = {
    xs: 'auto',
    sm: 'auto auto',
    md: 'auto auto auto',
    lg: 'auto auto auto',
    xl: 'auto auto auto'
  };

  @Input() private responsiveColumns: ResponsiveColumnsMap;


  postsMock: Posts[] = [
    {
      uuid: '1',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/1.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '2',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/2.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '2',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/3.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '4',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/1.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '5',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/2.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '6',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/3.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '7',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/1.png',
      category: 'events',
      published: '20 Julio 2019'
    },
    {
      uuid: '8',
      title: 'Emprender con tecnología, Startup en el rubro',
      body: 'We all know how beneficial a budget . ng off youro you know how much goes...',
      thumbnail: './assets/images/blog/dev/2.png',
      category: 'events',
      published: '20 Julio 2019'
    },
  ];

  public gdColumns = 'auto';


  private readonly watchers: Subscription[] = [];

  constructor(
    private readonly mediaObserver: MediaObserver) {
  }

  ngOnInit(): void {
    this.responsiveColumns = this.responsiveColumns || BlogPostsPartialComponent.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

          console.log('this.responsiveColumns[matchedAlias]', this.responsiveColumns[matchedAlias]);

          return this.responsiveColumns[matchedAlias];
        })
      ).subscribe(cols => this.gdColumns = 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.gdColumns = this.responsiveColumns[firstMatchingAlias];
    } else {
      this.gdColumns = 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];
  }

}

blog-posts-partial.component.html

<div class="blog-posts-partial-component">
  <div fxLayout="row" fxLayoutAlign="space-between none" fxLayoutGap="4px" [gdColumns]="gdColumns" class="block">

    <div fxFlex class="post" *ngFor="let post of postsMock">
      <div class="img-container">
        <img [src]="post.thumbnail" [alt]="post.title" class="img-fluid">
      </div>
      <div class="title">
        <h1>{{post.title}}</h1>
      </div>
      <div class="desc">
        <p>{{post.body}}</p>
      </div>
      <div class="info">
        <div fxLayout='row'>
          <div fxFlex>
            <div class="date">
              <span><img src="./assets/images/blog/date_range.png" alt="post title" class="img-fluid date-range">
                {{post.published}}</span>
            </div>
          </div>
          <div fxFlex>
            <div class="tags">
              <button
                mat-button>#{{post.category === 'events' ?  'Eventos' : post.category === 'interviews' ? 'Entrevistas' :  'NoticiasDestacadas' }}</button>
            </div>
          </div>
        </div>
      </div>
    </div>

  </div>
</div>

@petereskandar
Copy link

Hi all,

anyone could be able to resolve this issue or has any idea on how to resolve it :

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

Thanks in advance

@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