-
-
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 ]; | |
} | |
} |
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>
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
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];
}
}
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];
}
}
@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 :-)