From b8c86be1d167f6e4842cf82f6e3358087dcd0038 Mon Sep 17 00:00:00 2001 From: CaerusKaru Date: Sat, 14 Apr 2018 13:00:11 -0400 Subject: [PATCH] feat(grid): add CSS Grid directives and demo (#712) * Add guide for using new CSS Grid directives under the guides directory * Add five demos from gridbyexample.com * Add testing for all components * Change `BaseFxDirective` to `BaseDirective`. `BaseFxDirective` will remain but is deprecated and will be removed in beta.17 * Bump testing for Edge to v16 due to Grid incompatability * Design doc: https://docs.google.com/document/d/1YjKHAV7-wh5UZd4snoyw6bVWe1X5JF-zDaUFa8-JDtE --- docs/documentation/API-Documentation.md | 2 +- docs/documentation/Custom-Breakpoints.md | 4 +- guides/Grid.md | 64 +++ package-lock.json | 6 +- package.json | 2 +- src/apps/demo-app/package.json | 2 +- src/apps/demo-app/src/app/app.component.html | 9 +- .../docs-grid/docs-grid.component.spec.ts | 25 ++ .../app/grid/docs-grid/docs-grid.component.ts | 13 + .../grid-layout/grid-layout.component.spec.ts | 25 ++ .../grid/grid-layout/grid-layout.component.ts | 31 ++ .../grid-minmax/grid-minmax.component.spec.ts | 25 ++ .../grid/grid-minmax/grid-minmax.component.ts | 40 ++ .../grid-nested/grid-nested.component.spec.ts | 25 ++ .../grid/grid-nested/grid-nested.component.ts | 33 ++ .../grid-overlay.component.spec.ts | 25 ++ .../grid-overlay/grid-overlay.component.ts | 33 ++ .../grid-position.component.spec.ts | 25 ++ .../grid-position/grid-position.component.ts | 54 +++ src/apps/demo-app/src/app/grid/grid.module.ts | 35 ++ .../demo-app/src/app/grid/routing.module.ts | 17 + src/apps/demo-app/src/app/routing.module.ts | 1 + src/apps/demo-app/src/styles.scss | 66 ++- src/apps/hello-world/package.json | 2 +- src/apps/universal-app/package.json | 2 +- src/lib/core/README.md | 6 +- src/lib/core/base/base-adapter.spec.ts | 6 +- src/lib/core/base/base-adapter.ts | 16 +- src/lib/core/base/base-legacy.ts | 282 ++++++++++++ src/lib/core/base/base.ts | 7 +- src/lib/core/base/index.ts | 1 + src/lib/core/style-utils/style-utils.ts | 49 ++- src/lib/extended/class/class.ts | 10 +- src/lib/extended/img-src/img-src.ts | 4 +- src/lib/extended/show-hide/show-hide.ts | 4 +- src/lib/extended/style/style.ts | 10 +- src/lib/flex/flex-align/flex-align.ts | 4 +- src/lib/flex/flex-fill/flex-fill.ts | 4 +- src/lib/flex/flex-offset/flex-offset.ts | 6 +- src/lib/flex/flex-order/flex-order.ts | 4 +- src/lib/flex/flex/flex.spec.ts | 4 +- src/lib/flex/flex/flex.ts | 6 +- src/lib/flex/layout-align/layout-align.ts | 4 +- src/lib/flex/layout-gap/layout-gap.ts | 4 +- src/lib/flex/layout/layout.ts | 4 +- src/lib/grid/README.md | 19 + .../grid/align-columns/align-columns.spec.ts | 411 ++++++++++++++++++ src/lib/grid/align-columns/align-columns.ts | 162 +++++++ src/lib/grid/align-rows/align-rows.spec.ts | 411 ++++++++++++++++++ src/lib/grid/align-rows/align-rows.ts | 143 ++++++ src/lib/grid/area/area.spec.ts | 220 ++++++++++ src/lib/grid/area/area.ts | 103 +++++ src/lib/grid/areas/areas.spec.ts | 220 ++++++++++ src/lib/grid/areas/areas.ts | 112 +++++ src/lib/grid/auto/auto.spec.ts | 329 ++++++++++++++ src/lib/grid/auto/auto.ts | 116 +++++ src/lib/grid/column/column.spec.ts | 170 ++++++++ src/lib/grid/column/column.ts | 103 +++++ src/lib/grid/columns/columns.spec.ts | 193 ++++++++ src/lib/grid/columns/columns.ts | 122 ++++++ src/lib/grid/gap/gap.spec.ts | 259 +++++++++++ src/lib/grid/gap/gap.ts | 110 +++++ src/lib/grid/grid-align/grid-align.spec.ts | 366 ++++++++++++++++ src/lib/grid/grid-align/grid-align.ts | 146 +++++++ src/lib/grid/index.ts | 9 + src/lib/grid/module.ts | 50 +++ src/lib/grid/public-api.ts | 11 + src/lib/grid/row/row.spec.ts | 169 +++++++ src/lib/grid/row/row.ts | 103 +++++ src/lib/grid/rows/rows.spec.ts | 193 ++++++++ src/lib/grid/rows/rows.ts | 122 ++++++ src/lib/grid/tsconfig-build.json | 15 + src/lib/module.ts | 5 +- src/lib/package.json | 2 +- src/lib/public-api.ts | 1 + test/browser-providers.js | 7 +- test/karma-test-shim.js | 1 + tools/package-tools/rollup-globals.ts | 1 + 78 files changed, 5309 insertions(+), 96 deletions(-) create mode 100644 guides/Grid.md create mode 100644 src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-position/grid-position.component.spec.ts create mode 100644 src/apps/demo-app/src/app/grid/grid-position/grid-position.component.ts create mode 100644 src/apps/demo-app/src/app/grid/grid.module.ts create mode 100644 src/apps/demo-app/src/app/grid/routing.module.ts create mode 100644 src/lib/core/base/base-legacy.ts create mode 100644 src/lib/grid/README.md create mode 100644 src/lib/grid/align-columns/align-columns.spec.ts create mode 100644 src/lib/grid/align-columns/align-columns.ts create mode 100644 src/lib/grid/align-rows/align-rows.spec.ts create mode 100644 src/lib/grid/align-rows/align-rows.ts create mode 100644 src/lib/grid/area/area.spec.ts create mode 100644 src/lib/grid/area/area.ts create mode 100644 src/lib/grid/areas/areas.spec.ts create mode 100644 src/lib/grid/areas/areas.ts create mode 100644 src/lib/grid/auto/auto.spec.ts create mode 100644 src/lib/grid/auto/auto.ts create mode 100644 src/lib/grid/column/column.spec.ts create mode 100644 src/lib/grid/column/column.ts create mode 100644 src/lib/grid/columns/columns.spec.ts create mode 100644 src/lib/grid/columns/columns.ts create mode 100644 src/lib/grid/gap/gap.spec.ts create mode 100644 src/lib/grid/gap/gap.ts create mode 100644 src/lib/grid/grid-align/grid-align.spec.ts create mode 100644 src/lib/grid/grid-align/grid-align.ts create mode 100644 src/lib/grid/index.ts create mode 100644 src/lib/grid/module.ts create mode 100644 src/lib/grid/public-api.ts create mode 100644 src/lib/grid/row/row.spec.ts create mode 100644 src/lib/grid/row/row.ts create mode 100644 src/lib/grid/rows/rows.spec.ts create mode 100644 src/lib/grid/rows/rows.ts create mode 100644 src/lib/grid/tsconfig-build.json diff --git a/docs/documentation/API-Documentation.md b/docs/documentation/API-Documentation.md index a1020f5a8..24c4519c4 100644 --- a/docs/documentation/API-Documentation.md +++ b/docs/documentation/API-Documentation.md @@ -17,7 +17,7 @@ import {BREAKPOINTS} from '@angular/flex-layout'; providers: [{provide: BREAKPOINTS, useValue: MY_CUSTOM_BREAKPOINTS }] ``` -* **[BaseFxDirectiveAdapter](https://github.com/angular/flex-layout/wiki/BaseFxDirectiveAdapter)**: +* **[BaseDirectiveAdapter](https://github.com/angular/flex-layout/wiki/BaseDirectiveAdapter)**: Adapter class useful to extend existing Directives with MediaQuery activation features. ```typescript import {NgClass} from '@angular/core'; diff --git a/docs/documentation/Custom-Breakpoints.md b/docs/documentation/Custom-Breakpoints.md index b64cf0775..8d1b19031 100644 --- a/docs/documentation/Custom-Breakpoints.md +++ b/docs/documentation/Custom-Breakpoints.md @@ -81,7 +81,7 @@ Consider, for example, the ```typescript import {Directive} from '@angular/core'; -import {BaseFxDirective} from '@angular/flex-layout'; +import {BaseDirective} from '@angular/flex-layout'; @Directive({selector: ` [fxLayout], @@ -95,7 +95,7 @@ import {BaseFxDirective} from '@angular/flex-layout'; [fxLayout.gt-lg], [fxLayout.xl] `}) -export class LayoutDirective extends BaseFxDirective { +export class LayoutDirective extends BaseDirective { ... } ``` diff --git a/guides/Grid.md b/guides/Grid.md new file mode 100644 index 000000000..2dfcd1491 --- /dev/null +++ b/guides/Grid.md @@ -0,0 +1,64 @@ +# CSS Grid with Angular Layout + +### Introduction + +CSS Grid is a relatively new, powerful layout library available in all evergreen browsers. It provides +an extra level of dimensionality for constructing web layouts compared to Flexbox. We have added 11 new +directives with responsive functionality to the Angular Layout library to enable developers to easily add +the new engine to their apps. + +### Usage + +The new suite of directives is extensive, and covers the majority of CSS Grid functionality. The +following table shows the parity between directives and CSS properties: + +| Grid Directive | CSS Property(s) | Extra Inputs | +| ---------------- |:-----------------------------------------:| --------------------------:| +| `gdAlignColumns` | `align-content` and `align-items` | `gdInline` for inline-grid | +| `gdAlignRows` | `justify-content` and `justify-items` | `gdInline` for inline-grid | +| `gdArea` | `grid-area` | none | +| `gdAreas` | `grid-areas` | `gdInline` for inline-grid | +| `gdAuto` | `grid-auto-flow` | `gdInline` for inline-grid | +| `gdColumn` | `grid-column` | none | +| `gdColumns` | `grid-template-columns` | `gdInline` for inline-grid
`!` at the end means `grid-auto-columns` | +| `gdGap` | `grid-gap` | `gdInline` for inline-grid | +| `gdGridAlign` | `justify-self` and `align-self` | none | +| `gdRow` | `grid-row` | none | +| `gdRows` | `grid-template-rows` | `gdInline` for inline-grid
`!` at the end means `grid-auto-rows` | + +Note: unless otherwise specified, the above table represents exact parity. The inputs for the +directives will be mapped verbatim to the CSS property without sanitization + +### Limitations + +While CSS Grid has excellent cross-browser and mobile support, it is currently unsupported in IE11 +due to an outdated spec implementation + +### Using with Flexbox + +The new CSS Grid directives can be used in concert with the existing Flexbox directives seamlessly. +Simply import the top-level `FlexLayoutModule`, or both `FlexModule` and `GridModule` as follows: + +```typescript +import {FlexLayoutModule} from '@angular/flex-layout'; +``` + +```typescript +import {FlexModule} from '@angular/flex-layout/flex'; +import {GridModule} from '@angular/flex-layout/grid'; +``` + +This allows you to use, for example, Flexbox inside a CSS Grid as follows: + +```html +
+
+
+
+
+``` + +### References + +The design doc for this part of the library can be found +[here](https://docs.google.com/document/d/1YjKHAV7-wh5UZd4snoyw6bVWe1X5JF-zDaUFa8-JDtE) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6df0da67a..e4f309c11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16618,9 +16618,9 @@ "dev": true }, "rxjs": { - "version": "6.0.0-beta.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.0.0-beta.4.tgz", - "integrity": "sha512-H+ghJ4H2mPrugoMfgbeky0yhmOrk4y0ykRyZpYvU2za0VbE2WTKgY/9pF7HJCogPFNIyI4GqI9Ujk3Xr4XxHbQ==", + "version": "6.0.0-rc.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.0.0-rc.0.tgz", + "integrity": "sha512-nQqUjqiyiD3OkPd4xlg4eDNG4k8UiarBhU9qr3xKncHYhn3REjC4fCAFlg862JEwg50vQImaI/bv8yzreAHzng==", "requires": { "tslib": "1.9.0" } diff --git a/package.json b/package.json index e399a329f..14fb85a04 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@angular/core": "^6.0.0-rc.0", "@angular/platform-browser": "^6.0.0-rc.0", "core-js": "^2.4.1", - "rxjs": "^6.0.0-beta.4", + "rxjs": "^6.0.0-rc.0", "systemjs": "0.19.43", "tsickle": "^0.27.0", "tslib": "^1.8.0", diff --git a/src/apps/demo-app/package.json b/src/apps/demo-app/package.json index 9d6cd8142..f9abe6775 100644 --- a/src/apps/demo-app/package.json +++ b/src/apps/demo-app/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@angular/cli": "1.7.2", "@angular/compiler-cli": "file:../../../node_modules/@angular/compiler-cli", - "@angular/language-service": "^6.0.0-rc.0", + "@angular/language-service": "^6.0.0-rc.1", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", diff --git a/src/apps/demo-app/src/app/app.component.html b/src/apps/demo-app/src/app/app.component.html index 13dbd6b41..f685d1235 100644 --- a/src/apps/demo-app/src/app/app.component.html +++ b/src/apps/demo-app/src/app/app.component.html @@ -9,12 +9,10 @@

Layout Demos:

- These Layout demos are curated from the Angular Material v1.x documentation, GitHub Issues, - StackOverflow, - and CodePen. + These Layout demos are curated from the AngularJS Material documentation, GitHub Issues, StackOverflow, and CodePen. - Hint: Click on any of the samples below to toggle the layout direction(s). - + Hint: Click on any of the samples below to toggle the layout direction(s). +
Layout Demos: + diff --git a/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.spec.ts b/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.spec.ts new file mode 100644 index 000000000..a60142a88 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DocsLayoutComponent } from './docs-grid.component'; + +describe('DocsLayoutComponent', () => { + let component: DocsLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DocsLayoutComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DocsLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.ts b/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.ts new file mode 100644 index 000000000..0ee5438df --- /dev/null +++ b/src/apps/demo-app/src/app/grid/docs-grid/docs-grid.component.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'demo-docs-grid', + template: ` + + + + + + ` +}) +export class DocsGridComponent {} diff --git a/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.spec.ts b/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.spec.ts new file mode 100644 index 000000000..057ed9de1 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FlexOffsetValuesComponent } from './grid-layout.component'; + +describe('FlexOffsetValuesComponent', () => { + let component: FlexOffsetValuesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FlexOffsetValuesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FlexOffsetValuesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.ts b/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.ts new file mode 100644 index 000000000..d0544d234 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-layout/grid-layout.component.ts @@ -0,0 +1,31 @@ +import {Component} from '@angular/core'; + +// Example taken from https://gridbyexample.com/examples/example13/ +/* tslint:disable */ +@Component({ + selector: 'demo-grid-layout', + template: ` + + Basic Responsive Grid App + +
+
+
Header
+
Sidebar
+
Sidebar 2
+
Content +
More content than we had before so this column is now quite tall.
+
Footer
+
+
+
+
+ ` +}) +export class GridLayoutComponent {} diff --git a/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.spec.ts b/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.spec.ts new file mode 100644 index 000000000..d7a5b8886 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridMinmaxComponent } from './grid-minmax.component'; + +describe('GridMinmaxComponent', () => { + let component: GridMinmaxComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GridMinmaxComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridMinmaxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.ts b/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.ts new file mode 100644 index 000000000..7eb37743b --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-minmax/grid-minmax.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; + +// Example taken from https://gridbyexample.com/examples/example29/ +@Component({ + selector: 'demo-grid-minmax', + template: ` + + Grid with Minmax + +
+
+
A
+
B
+
C
+
D
+
E
+
F
+
G
+
H
+
I
+
J
+
K
+
L
+
M
+
+
+
+
+ `, + styles: [`.box { + /*background-color: #444;*/ + /*color: #fff;*/ + border-radius: 5px; + padding: 20px; + font-size: 150%; + + }`] +}) +export class GridMinmaxComponent { +} diff --git a/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.spec.ts b/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.spec.ts new file mode 100644 index 000000000..90df58a7e --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridNestedComponent } from './grid-nested.component'; + +describe('GridNestedComponent', () => { + let component: GridNestedComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GridNestedComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridNestedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.ts b/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.ts new file mode 100644 index 000000000..4ffdbd4d1 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-nested/grid-nested.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; + +// Example taken from https://gridbyexample.com/examples/example21/ +@Component({ + selector: 'demo-grid-nested', + template: ` + + Nested Grid + +
+
+
A
+
B
+
C
+
+
E
+
F
+
G
+
+
+
+
+
+ `, + styles: [`.box { + border-radius: 5px; + padding: 20px; + font-size: 150%; + }`] +}) +export class GridNestedComponent { +} diff --git a/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.spec.ts b/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.spec.ts new file mode 100644 index 000000000..c21751d75 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridOverlayComponent } from './grid-overlay.component'; + +describe('GridOverlayComponent', () => { + let component: GridOverlayComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GridOverlayComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridOverlayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.ts b/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.ts new file mode 100644 index 000000000..1cffb13bf --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-overlay/grid-overlay.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'demo-grid-overlay', + template: ` + + Grid with Overlay + +
+
+
Header
+
Sidebar
+
Content
+
+ overlay +
+
+
+
+
+ `, + styles: [`.box { + border-radius: 5px; + padding: 20px; + }`, `.overlay { + background-color: red; + z-index: 10; + }`] +}) +export class GridOverlayComponent { +} diff --git a/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.spec.ts b/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.spec.ts new file mode 100644 index 000000000..cda857f92 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridPositionComponent } from './grid-position.component'; + +describe('GridPositionComponent', () => { + let component: GridPositionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GridPositionComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridPositionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.ts b/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.ts new file mode 100644 index 000000000..3fef0b9df --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid-position/grid-position.component.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; + +// Example taken from https://gridbyexample.com/examples/example16/ +@Component({ + selector: 'demo-grid-position', + template: ` + + Grid with Positioning + +
+
+
Header
+
Sidebar
+
Content +
The four arrows are inline images inside the content area. + top left + top right + bottom left + bottom right
+
Footer
+
+
+
+
+ `, + styles: [`.topleft { + position: absolute; + top: 0; + left: 0; + }`, `.topright { + position: absolute; + top: 0; + right: 0; + }`, `.bottomleft { + position: absolute; + bottom: 0; + left: 0; + }`, `.bottomright { + position: absolute; + bottom: 0; + right: 0; + }`, `.box { + border-radius: 5px; + padding: 50px; + font-size: 150%; + }`] +}) +export class GridPositionComponent { +} diff --git a/src/apps/demo-app/src/app/grid/grid.module.ts b/src/apps/demo-app/src/app/grid/grid.module.ts new file mode 100644 index 000000000..4864511a5 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/grid.module.ts @@ -0,0 +1,35 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {MatCardModule} from '@angular/material/card'; +import {MatRadioModule} from '@angular/material/radio'; +import {FlexLayoutModule} from '@angular/flex-layout'; + +import {DocsGridComponent} from './docs-grid/docs-grid.component'; +import {GridLayoutComponent} from './grid-layout/grid-layout.component'; +import {RoutingModule} from './routing.module'; +import {GridNestedComponent} from './grid-nested/grid-nested.component'; +import {GridMinmaxComponent} from './grid-minmax/grid-minmax.component'; +import {GridPositionComponent} from './grid-position/grid-position.component'; +import {GridOverlayComponent} from './grid-overlay/grid-overlay.component'; + + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatRadioModule, + FlexLayoutModule, + RoutingModule, + ], + declarations: [ + DocsGridComponent, + GridLayoutComponent, + GridNestedComponent, + GridMinmaxComponent, + GridPositionComponent, + GridOverlayComponent, + ] +}) +export class DocsGridModule {} diff --git a/src/apps/demo-app/src/app/grid/routing.module.ts b/src/apps/demo-app/src/app/grid/routing.module.ts new file mode 100644 index 000000000..ee95f8711 --- /dev/null +++ b/src/apps/demo-app/src/app/grid/routing.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +import {DocsGridComponent} from './docs-grid/docs-grid.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: DocsGridComponent + } + ]) + ], + exports: [RouterModule] +}) +export class RoutingModule {} diff --git a/src/apps/demo-app/src/app/routing.module.ts b/src/apps/demo-app/src/app/routing.module.ts index 6b2ccb5af..a2aff8feb 100644 --- a/src/apps/demo-app/src/app/routing.module.ts +++ b/src/apps/demo-app/src/app/routing.module.ts @@ -4,6 +4,7 @@ import {HashLocationStrategy, LocationStrategy} from '@angular/common'; const DEMO_APP_ROUTES: Routes = [ {path: '', redirectTo: 'docs', pathMatch: 'full'}, + {path: 'grid', loadChildren: './grid/grid.module#DocsGridModule'}, {path: 'docs', loadChildren: './layout/layout.module#DocsLayoutModule'}, {path: 'responsive', loadChildren: './responsive/responsive.module#DocsResponsiveModule'}, {path: 'issues', loadChildren: './github-issues/github-issues.module#DocsGithubIssuesModule'}, diff --git a/src/apps/demo-app/src/styles.scss b/src/apps/demo-app/src/styles.scss index e4b373157..f37e79889 100644 --- a/src/apps/demo-app/src/styles.scss +++ b/src/apps/demo-app/src/styles.scss @@ -45,29 +45,56 @@ div.coloredContainerX > div, div.colorNested > div > div { text-align: center; } -div.coloredContainerX > div:nth-child(1), div.colorNested > div > div:nth-child(1), div.box1 { +div.coloredContainerX > div:nth-child(10n+1), div.colorNested > div > div:nth-child(10n+1), +div.box1 { background-color: #009688; } -div.coloredContainerX > div:nth-child(2), div.colorNested > div > div:nth-child(2), div.box2 { +div.coloredContainerX > div:nth-child(10n+2), div.colorNested > div > div:nth-child(10n+2), +div.box2 { background-color: #3949ab; } -div.coloredContainerX > div:nth-child(3), div.coloredChildren > div:nth-child(3), -div.colorNested > div > div:nth-child(3), div.box3 { +div.coloredContainerX > div:nth-child(10n+3), div.coloredChildren > div:nth-child(10n+3), +div.colorNested > div > div:nth-child(10n+3), div.box3 { background-color: #9c27b0; } -div.coloredContainerX > div:nth-child(4), div.coloredChildren > div:nth-child(4), -div.colorNested > div > div:nth-child(4) { +div.coloredContainerX > div:nth-child(10n+4), div.coloredChildren > div:nth-child(10n+4), +div.colorNested > div > div:nth-child(10n+4) { background-color: #b08752; } -div.coloredContainerX > div:nth-child(5), div.coloredChildren > div:nth-child(5), -div.colorNested > div > div:nth-child(5) { +div.coloredContainerX > div:nth-child(10n+5), div.coloredChildren > div:nth-child(10n+5), +div.colorNested > div > div:nth-child(10n+5) { background-color: #5ca6b0; } +div.coloredContainerX > div:nth-child(10n+6), div.coloredChildren > div:nth-child(10n+6), +div.colorNested > div > div:nth-child(10n+6) { + background-color: #8bc34a; +} + +div.coloredContainerX > div:nth-child(10n+7), div.coloredChildren > div:nth-child(10n+7), +div.colorNested > div > div:nth-child(10n+7) { + background-color: #9c27b0; +} + +div.coloredContainerX > div:nth-child(10n+8), div.coloredChildren > div:nth-child(10n+8), +div.colorNested > div > div:nth-child(10n+8) { + background-color: #c9954e; +} + +div.coloredContainerX > div:nth-child(10n+9), div.coloredChildren > div:nth-child(10n+9), +div.colorNested > div > div:nth-child(10n+9) { + background-color: #ff5722; +} + +div.coloredContainerX > div:nth-child(10n), div.coloredChildren > div:nth-child(10n), +div.colorNested > div > div:nth-child(10n) { + background-color: #03a9f4; +} + div.colored > div { padding: 8px; box-shadow: 0 2px 5px 0 rgba(52, 47, 51, 0.63); @@ -76,39 +103,38 @@ div.colored > div { text-align: center; } -div.colored > div:nth-child(1), .one { +div.colored > div:nth-child(10n+1), .one { background-color: #009688; } -div.colored > div:nth-child(2), .two { +div.colored > div:nth-child(10n+2), .two { background-color: #3949ab; } -div.colored > div:nth-child(3), .three { +div.colored > div:nth-child(10n+3), .three { background-color: #9c27b0; } -div.colored > div:nth-child(4), .four { +div.colored > div:nth-child(10n+4), .four { background-color: #8bc34a; } -div.colored > div:nth-child(5), .five { +div.colored > div:nth-child(10n+5), .five { background-color: #03a9f4; } -div.colored > div:nth-child(6), .six { +div.colored > div:nth-child(10n+6), .six { background-color: #c9954e; } -div.colored > div:nth-child(7), .seven { +div.colored > div:nth-child(10n+7), .seven { background-color: #ff5722; } .hint { - margin: 5px; + margin: 5px 5px 0; font-size: 0.9em; color: #a3a3a3; - margin-bottom: 0; } .title { @@ -183,8 +209,7 @@ div.colored.box.nopad > div { } .bigger { - padding: 0 20px; - padding-bottom: 30px; + padding: 0 20px 30px; } mat-toolbar .mat-toolbar-layout mat-toolbar-row { @@ -287,8 +312,7 @@ mat-card-content pre { .hint { color: #a3a3a3; font-size: 0.9em; - margin: 5px; - margin-bottom: 0; + margin: 5px 5px 0; } .forceAbove { diff --git a/src/apps/hello-world/package.json b/src/apps/hello-world/package.json index 897788152..81f85eac9 100644 --- a/src/apps/hello-world/package.json +++ b/src/apps/hello-world/package.json @@ -31,7 +31,7 @@ "devDependencies": { "@angular/cli": "~1.7.2", "@angular/compiler-cli": "file:../../../node_modules/@angular/compiler-cli", - "@angular/language-service": "^6.0.0-rc.0", + "@angular/language-service": "^6.0.0-rc.1", "typescript": "file:../../../node_modules/typescript" } } diff --git a/src/apps/universal-app/package.json b/src/apps/universal-app/package.json index e66f6f709..fb78a6720 100644 --- a/src/apps/universal-app/package.json +++ b/src/apps/universal-app/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@angular/cli": "1.7.2", "@angular/compiler-cli": "file:../../../node_modules/@angular/compiler-cli", - "@angular/language-service": "^6.0.0-rc.0", + "@angular/language-service": "^6.0.0-rc.1", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", diff --git a/src/lib/core/README.md b/src/lib/core/README.md index e3fcec6ce..7e618b3f2 100644 --- a/src/lib/core/README.md +++ b/src/lib/core/README.md @@ -2,7 +2,7 @@ The `core` entrypoint contains all of the common utilities to build Layout components. Its primary exports are the `MediaQuery` utilities (`MatchMedia`, `ObservableMedia`) and the module that encapsulates the imports of these providers, the `CoreModule`, and the base directive for layout -components, `BaseFxDirective`. These utilies can be imported separately +components, `BaseDirective`. These utilies can be imported separately from the root module to take advantage of tree shaking. ```typescript @@ -19,7 +19,7 @@ export class AppModule {} ``` ```typescript -import {BaseFxDirective} from '@angular/flex-layout/core'; +import {BaseDirective} from '@angular/flex-layout/core'; -export class NewLayoutDirective extends BaseFxDirective {} +export class NewLayoutDirective extends BaseDirective {} ``` \ No newline at end of file diff --git a/src/lib/core/base/base-adapter.spec.ts b/src/lib/core/base/base-adapter.spec.ts index dde126ca7..83110cfdf 100644 --- a/src/lib/core/base/base-adapter.spec.ts +++ b/src/lib/core/base/base-adapter.spec.ts @@ -6,15 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ import {ElementRef} from '@angular/core'; -import {BaseFxDirectiveAdapter} from './base-adapter'; +import {BaseDirectiveAdapter} from './base-adapter'; import {expect} from '../../utils/testing/custom-matchers'; import {MediaMonitor} from '../media-monitor/media-monitor'; import {StyleUtils} from '../style-utils/style-utils'; -describe('BaseFxDirectiveAdapter class', () => { +describe('BaseDirectiveAdapter class', () => { let component; beforeEach(() => { - component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, {} as ElementRef, {} as StyleUtils); // tslint:disable-line:max-line-length + component = new BaseDirectiveAdapter('', {} as MediaMonitor, {} as ElementRef, {} as StyleUtils); // tslint:disable-line:max-line-length }); describe('cacheInput', () => { it('should call _cacheInputArray when source is an array', () => { diff --git a/src/lib/core/base/base-adapter.ts b/src/lib/core/base/base-adapter.ts index c4639edcb..2bf121180 100644 --- a/src/lib/core/base/base-adapter.ts +++ b/src/lib/core/base/base-adapter.ts @@ -7,7 +7,7 @@ */ import {ElementRef} from '@angular/core'; -import {BaseFxDirective} from './base'; +import {BaseDirective} from './base'; import {ResponsiveActivation} from '../responsive-activation/responsive-activation'; import {MediaQuerySubscriber} from '../media-change'; import {MediaMonitor} from '../media-monitor/media-monitor'; @@ -15,10 +15,10 @@ import {StyleUtils} from '../style-utils/style-utils'; /** - * Adapter to the BaseFxDirective abstract class so it can be used via composition. - * @see BaseFxDirective + * Adapter to the BaseDirective abstract class so it can be used via composition. + * @see BaseDirective */ -export class BaseFxDirectiveAdapter extends BaseFxDirective { +export class BaseDirectiveAdapter extends BaseDirective { /** * Accessor to determine which @Input property is "active" @@ -37,14 +37,14 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { } /** - * @see BaseFxDirective._mqActivation + * @see BaseDirective._mqActivation */ get mqActivation(): ResponsiveActivation { return this._mqActivation; } /** - * BaseFxDirectiveAdapter constructor + * BaseDirectiveAdapter constructor */ constructor(protected _baseKey: string, // non-responsive @Input property name protected _mediaMonitor: MediaMonitor, @@ -62,7 +62,7 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { } /** - * @see BaseFxDirective._queryInput + * @see BaseDirective._queryInput */ queryInput(key) { return key ? this._queryInput(key) : undefined; @@ -88,7 +88,7 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { } /** - * @see BaseFxDirective._listenForMediaQueryChanges + * @see BaseDirective._listenForMediaQueryChanges */ listenForMediaQueryChanges(key: string, defaultValue: any, diff --git a/src/lib/core/base/base-legacy.ts b/src/lib/core/base/base-legacy.ts new file mode 100644 index 000000000..b56f722ba --- /dev/null +++ b/src/lib/core/base/base-legacy.ts @@ -0,0 +1,282 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + ElementRef, + OnDestroy, + SimpleChanges, + OnChanges, + SimpleChange, +} from '@angular/core'; + +import {buildLayoutCSS} from '../../utils/layout-validator'; +import { + StyleDefinition, + StyleUtils, +} from '../style-utils/style-utils'; + +import {ResponsiveActivation, KeyOptions} from '../responsive-activation/responsive-activation'; +import {MediaMonitor} from '../media-monitor/media-monitor'; +import {MediaQuerySubscriber} from '../media-change'; + +/** + * @deprecated + * @deletion-target v6.0.0-beta.17 + * Abstract base class for the Layout API styling directives. + */ +export abstract class BaseFxDirective implements OnDestroy, OnChanges { + get hasMediaQueryListener() { + return !!this._mqActivation; + } + + /** + * Imperatively determine the current activated [input] value; + * if called before ngOnInit() this will return `undefined` + */ + get activatedValue(): string | number { + return this._mqActivation ? this._mqActivation.activatedInput : undefined; + } + + /** + * Change the currently activated input value and force-update + * the injected CSS (by-passing change detection). + * + * NOTE: Only the currently activated input value will be modified; + * other input values will NOT be affected. + */ + set activatedValue(value: string | number) { + let key = 'baseKey', previousVal; + + if (this._mqActivation) { + key = this._mqActivation.activatedInputKey; + previousVal = this._inputMap[key]; + this._inputMap[key] = value; + } + let change = new SimpleChange(previousVal, value, false); + + this.ngOnChanges({[key]: change} as SimpleChanges); + } + + + /** + * Constructor + */ + constructor(protected _mediaMonitor: MediaMonitor, + protected _elementRef: ElementRef, + protected _styler: StyleUtils) { + } + + // ********************************************* + // Accessor Methods + // ********************************************* + + /** + * Access to host element's parent DOM node + */ + protected get parentElement(): any { + return this._elementRef.nativeElement.parentNode; + } + + protected get nativeElement(): HTMLElement { + return this._elementRef.nativeElement; + } + + /** + * Access the current value (if any) of the @Input property. + */ + protected _queryInput(key) { + return this._inputMap[key]; + } + + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * Use post-component-initialization event to perform extra + * querying such as computed Display style + */ + ngOnInit() { + this._display = this._getDisplayStyle(); + this._hasInitialized = true; + } + + ngOnChanges(change: SimpleChanges) { + throw new Error(`BaseFxDirective::ngOnChanges should be overridden in subclass: ${change}`); + } + + ngOnDestroy() { + if (this._mqActivation) { + this._mqActivation.destroy(); + } + delete this._mediaMonitor; + } + + // ********************************************* + // Protected Methods + // ********************************************* + + /** + * Was the directive's default selector used ? + * If not, use the fallback value! + */ + protected _getDefaultVal(key: string, fallbackVal: any): string | boolean { + let val = this._queryInput(key); + let hasDefaultVal = (val !== undefined && val !== null); + return (hasDefaultVal && val !== '') ? val : fallbackVal; + } + + /** + * Quick accessor to the current HTMLElement's `display` style + * Note: this allows us to preserve the original style + * and optional restore it when the mediaQueries deactivate + */ + protected _getDisplayStyle(source: HTMLElement = this.nativeElement): string { + const query = 'display'; + return this._styler.lookupStyle(source, query); + } + + /** + * Quick accessor to raw attribute value on the target DOM element + */ + protected _getAttributeValue(attribute: string, + source: HTMLElement = this.nativeElement): string { + return this._styler.lookupAttributeValue(source, attribute); + } + + /** + * Determine the DOM element's Flexbox flow (flex-direction). + * + * Check inline style first then check computed (stylesheet) style. + * And optionally add the flow value to element's inline style. + */ + protected _getFlowDirection(target: HTMLElement, addIfMissing = false): string { + let value = 'row'; + let hasInlineValue = ''; + + if (target) { + [value, hasInlineValue] = this._styler.getFlowDirection(target); + + if (!hasInlineValue && addIfMissing) { + const style = buildLayoutCSS(value); + const elements = [target]; + this._styler.applyStyleToElements(style, elements); + } + } + + return value.trim() || 'row'; + } + + /** + * Applies styles given via string pair or object map to the directive element. + */ + protected _applyStyleToElement(style: StyleDefinition, + value?: string | number, + element: HTMLElement = this.nativeElement) { + this._styler.applyStyleToElement(element, style, value); + } + + /** + * Applies styles given via string pair or object map to the directive's element. + */ + protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) { + this._styler.applyStyleToElements(style, elements); + } + + /** + * Save the property value; which may be a complex object. + * Complex objects support property chains + */ + protected _cacheInput(key?: string, source?: any) { + if (typeof source === 'object') { + for (let prop in source) { + this._inputMap[prop] = source[prop]; + } + } else { + if (!!key) { + this._inputMap[key] = source; + } + } + } + + /** + * Build a ResponsiveActivation object used to manage subscriptions to mediaChange notifications + * and intelligent lookup of the directive's property value that corresponds to that mediaQuery + * (or closest match). + */ + protected _listenForMediaQueryChanges(key: string, + defaultValue: any, + onMediaQueryChange: MediaQuerySubscriber): ResponsiveActivation { // tslint:disable-line:max-line-length + if (!this._mqActivation) { + let keyOptions = new KeyOptions(key, defaultValue, this._inputMap); + this._mqActivation = new ResponsiveActivation( + keyOptions, + this._mediaMonitor, + (change) => onMediaQueryChange(change) + ); + } + return this._mqActivation; + } + + /** + * Special accessor to query for all child 'element' nodes regardless of type, class, etc. + */ + protected get childrenNodes() { + const obj = this.nativeElement.children; + const buffer: any[] = []; + + // iterate backwards ensuring that length is an UInt32 + for (let i = obj.length; i--; ) { + buffer[i] = obj[i]; + } + return buffer; + } + + /** + * Does this directive have 1 or more responsive keys defined + * Note: we exclude the 'baseKey' key (which is NOT considered responsive) + */ + hasResponsiveAPI(baseKey: string) { + const totalKeys = Object.keys(this._inputMap).length; + const baseValue = this._inputMap[baseKey]; + return (totalKeys - (!!baseValue ? 1 : 0)) > 0; + } + + + /** + * Fast validator for presence of attribute on the host element + */ + protected hasKeyValue(key) { + return this._mqActivation.hasKeyValue(key); + } + + protected get hasInitialized() { + return this._hasInitialized; + } + + /** Original dom Elements CSS display style */ + protected _display; + + /** + * MediaQuery Activation Tracker + */ + protected _mqActivation: ResponsiveActivation; + + /** + * Dictionary of input keys with associated values + */ + protected _inputMap = {}; + + /** + * Has the `ngOnInit()` method fired + * + * Used to allow *ngFor tasks to finish and support queries like + * getComputedStyle() during ngOnInit(). + */ + protected _hasInitialized = false; +} diff --git a/src/lib/core/base/base.ts b/src/lib/core/base/base.ts index 955e4730a..9e395b4c3 100644 --- a/src/lib/core/base/base.ts +++ b/src/lib/core/base/base.ts @@ -18,13 +18,12 @@ import { StyleDefinition, StyleUtils, } from '../style-utils/style-utils'; - import {ResponsiveActivation, KeyOptions} from '../responsive-activation/responsive-activation'; import {MediaMonitor} from '../media-monitor/media-monitor'; import {MediaQuerySubscriber} from '../media-change'; /** Abstract base class for the Layout API styling directives. */ -export abstract class BaseFxDirective implements OnDestroy, OnChanges { +export abstract class BaseDirective implements OnDestroy, OnChanges { get hasMediaQueryListener() { return !!this._mqActivation; } @@ -103,7 +102,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { } ngOnChanges(change: SimpleChanges) { - throw new Error(`BaseFxDirective::ngOnChanges should be overridden in subclass: ${change}`); + throw new Error(`BaseDirective::ngOnChanges should be overridden in subclass: ${change}`); } ngOnDestroy() { @@ -151,7 +150,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * Check inline style first then check computed (stylesheet) style. * And optionally add the flow value to element's inline style. */ - protected _getFlowDirection(target: HTMLElement, addIfMissing = false): string { + protected _getFlexFlowDirection(target: HTMLElement, addIfMissing = false): string { let value = 'row'; let hasInlineValue = ''; diff --git a/src/lib/core/base/index.ts b/src/lib/core/base/index.ts index 855382060..df276cde0 100644 --- a/src/lib/core/base/index.ts +++ b/src/lib/core/base/index.ts @@ -8,3 +8,4 @@ export * from './base'; export * from './base-adapter'; +export * from './base-legacy'; diff --git a/src/lib/core/style-utils/style-utils.ts b/src/lib/core/style-utils/style-utils.ts index 280f320cb..fb3da8fc1 100644 --- a/src/lib/core/style-utils/style-utils.ts +++ b/src/lib/core/style-utils/style-utils.ts @@ -72,7 +72,8 @@ export class StyleUtils { * Find the DOM element's inline style value (if any) */ lookupInlineStyle(element: HTMLElement, styleName: string): string { - return element.style[styleName] || element.style.getPropertyValue(styleName) || ''; + return isPlatformBrowser(this._platformId) ? + element.style[styleName] : this._getServerStyle(element, styleName); } /** @@ -112,13 +113,57 @@ export class StyleUtils { values.sort(); for (let value of values) { if (isPlatformBrowser(this._platformId) || !this._serverModuleLoaded) { - element.style.setProperty(key, value); + isPlatformBrowser(this._platformId) ? + element.style.setProperty(key, value) : this._setServerStyle(element, key, value); } else { this._serverStylesheet.addStyleToElement(element, key, value); } } }); } + + private _setServerStyle(element: any, styleName: string, styleValue?: string|null) { + styleName = styleName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + const styleMap = this._readStyleAttribute(element); + styleMap[styleName] = styleValue || ''; + this._writeStyleAttribute(element, styleMap); + } + + private _getServerStyle(element: any, styleName: string): string { + const styleMap = this._readStyleAttribute(element); + return styleMap[styleName] || ''; + } + + private _readStyleAttribute(element: any): {[name: string]: string} { + const styleMap: {[name: string]: string} = {}; + const styleAttribute = element.getAttribute('style'); + if (styleAttribute) { + const styleList = styleAttribute.split(/;+/g); + for (let i = 0; i < styleList.length; i++) { + const style = styleList[i].trim(); + if (style.length > 0) { + const colonIndex = style.indexOf(':'); + if (colonIndex === -1) { + throw new Error(`Invalid CSS style: ${style}`); + } + const name = style.substr(0, colonIndex).trim(); + styleMap[name] = style.substr(colonIndex + 1).trim(); + } + } + } + return styleMap; + } + + private _writeStyleAttribute(element: any, styleMap: {[name: string]: string}) { + let styleAttrValue = ''; + for (const key in styleMap) { + const newValue = styleMap[key]; + if (newValue) { + styleAttrValue += key + ':' + styleMap[key] + ';'; + } + } + element.setAttribute('style', styleAttrValue); + } } /** diff --git a/src/lib/extended/class/class.ts b/src/lib/extended/class/class.ts index 4966a9367..62dc9f452 100644 --- a/src/lib/extended/class/class.ts +++ b/src/lib/extended/class/class.ts @@ -22,8 +22,8 @@ import { } from '@angular/core'; import {NgClass} from '@angular/common'; import { - BaseFxDirective, - BaseFxDirectiveAdapter, + BaseDirective, + BaseDirectiveAdapter, MediaChange, MediaMonitor, StyleUtils, @@ -45,7 +45,7 @@ export type NgClassType = string | string[] | Set | {[klass: string]: an [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg] ` }) -export class ClassDirective extends BaseFxDirective +export class ClassDirective extends BaseDirective implements DoCheck, OnChanges, OnDestroy, OnInit { /** @@ -138,7 +138,7 @@ export class ClassDirective extends BaseFxDirective * keys have been defined. */ protected _configureAdapters() { - this._base = new BaseFxDirectiveAdapter( + this._base = new BaseDirectiveAdapter( 'ngClass', this.monitor, this._ngEl, @@ -171,5 +171,5 @@ export class ClassDirective extends BaseFxDirective * Special adapter to cross-cut responsive behaviors and capture mediaQuery changes * Delegate value changes to the internal `_ngClassInstance` for processing */ - protected _base: BaseFxDirectiveAdapter; // used for `ngClass.xxx` selectors + protected _base: BaseDirectiveAdapter; // used for `ngClass.xxx` selectors } diff --git a/src/lib/extended/img-src/img-src.ts b/src/lib/extended/img-src/img-src.ts index e4e17ba9e..a4848e3fe 100644 --- a/src/lib/extended/img-src/img-src.ts +++ b/src/lib/extended/img-src/img-src.ts @@ -17,7 +17,7 @@ import { } from '@angular/core'; import {isPlatformServer} from '@angular/common'; import { - BaseFxDirective, + BaseDirective, MediaMonitor, SERVER_TOKEN, StyleUtils, @@ -40,7 +40,7 @@ import { img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg] ` }) -export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChanges { +export class ImgSrcDirective extends BaseDirective implements OnInit, OnChanges { /* tslint:disable */ @Input('src') set srcBase(val) { this.cacheDefaultSrc(val); } diff --git a/src/lib/extended/show-hide/show-hide.ts b/src/lib/extended/show-hide/show-hide.ts index c488a00ae..d1be0423d 100644 --- a/src/lib/extended/show-hide/show-hide.ts +++ b/src/lib/extended/show-hide/show-hide.ts @@ -20,7 +20,7 @@ import { } from '@angular/core'; import {isPlatformServer} from '@angular/common'; import { - BaseFxDirective, + BaseDirective, MediaChange, MediaMonitor, SERVER_TOKEN, @@ -59,7 +59,7 @@ export function negativeOf(hide: any) { [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg] ` }) -export class ShowHideDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class ShowHideDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { /** * Subscription to the parent flex container's layout changes. diff --git a/src/lib/extended/style/style.ts b/src/lib/extended/style/style.ts index ae8ad0087..3c5181a2e 100644 --- a/src/lib/extended/style/style.ts +++ b/src/lib/extended/style/style.ts @@ -23,8 +23,8 @@ import { import {NgStyle} from '@angular/common'; import {DomSanitizer} from '@angular/platform-browser'; import { - BaseFxDirective, - BaseFxDirectiveAdapter, + BaseDirective, + BaseDirectiveAdapter, MediaChange, MediaMonitor, StyleUtils, @@ -50,7 +50,7 @@ import { [ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg] ` }) -export class StyleDirective extends BaseFxDirective +export class StyleDirective extends BaseDirective implements DoCheck, OnChanges, OnDestroy, OnInit { /** @@ -136,7 +136,7 @@ export class StyleDirective extends BaseFxDirective * keys have been defined. */ protected _configureAdapters() { - this._base = new BaseFxDirectiveAdapter( + this._base = new BaseDirectiveAdapter( 'ngStyle', this.monitor, this._ngEl, @@ -216,6 +216,6 @@ export class StyleDirective extends BaseFxDirective * Special adapter to cross-cut responsive behaviors * into the StyleDirective */ - protected _base: BaseFxDirectiveAdapter; + protected _base: BaseDirectiveAdapter; } diff --git a/src/lib/flex/flex-align/flex-align.ts b/src/lib/flex/flex-align/flex-align.ts index 4cc703319..6300c19bb 100644 --- a/src/lib/flex/flex-align/flex-align.ts +++ b/src/lib/flex/flex-align/flex-align.ts @@ -14,7 +14,7 @@ import { OnDestroy, SimpleChanges, } from '@angular/core'; -import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; /** @@ -30,7 +30,7 @@ import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/f [fxFlexAlign.gt-xs], [fxFlexAlign.gt-sm], [fxFlexAlign.gt-md], [fxFlexAlign.gt-lg] ` }) -export class FlexAlignDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class FlexAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { /* tslint:disable */ @Input('fxFlexAlign') set align(val) { this._cacheInput('align', val); }; diff --git a/src/lib/flex/flex-fill/flex-fill.ts b/src/lib/flex/flex-fill/flex-fill.ts index 7ec49629c..2b825794f 100644 --- a/src/lib/flex/flex-fill/flex-fill.ts +++ b/src/lib/flex/flex-fill/flex-fill.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {Directive, ElementRef} from '@angular/core'; -import {BaseFxDirective, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {BaseDirective, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; const FLEX_FILL_CSS = { @@ -27,7 +27,7 @@ const FLEX_FILL_CSS = { [fxFill], [fxFlexFill] `}) -export class FlexFillDirective extends BaseFxDirective { +export class FlexFillDirective extends BaseDirective { constructor(monitor: MediaMonitor, public elRef: ElementRef, styleUtils: StyleUtils) { diff --git a/src/lib/flex/flex-offset/flex-offset.ts b/src/lib/flex/flex-offset/flex-offset.ts index 3380bb7b8..98cad98c9 100644 --- a/src/lib/flex/flex-offset/flex-offset.ts +++ b/src/lib/flex/flex-offset/flex-offset.ts @@ -18,7 +18,7 @@ import { } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { - BaseFxDirective, + BaseDirective, MediaChange, MediaMonitor, StyleDefinition, @@ -39,7 +39,7 @@ import {isFlowHorizontal} from '../../utils/layout-validator'; [fxFlexOffset.lt-sm], [fxFlexOffset.lt-md], [fxFlexOffset.lt-lg], [fxFlexOffset.lt-xl], [fxFlexOffset.gt-xs], [fxFlexOffset.gt-sm], [fxFlexOffset.gt-md], [fxFlexOffset.gt-lg] `}) -export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { private _directionWatcher: Subscription; /* tslint:disable */ @@ -171,7 +171,7 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh // The flex-direction of this element's flex container. Defaults to 'row'. const isRtl = this._directionality.value === 'rtl'; - const layout = this._getFlowDirection(this.parentElement, true); + const layout = this._getFlexFlowDirection(this.parentElement, true); const horizontalLayoutKey = isRtl ? 'margin-right' : 'margin-left'; return isFlowHorizontal(layout) ? {[horizontalLayoutKey]: `${offset}`} : diff --git a/src/lib/flex/flex-order/flex-order.ts b/src/lib/flex/flex-order/flex-order.ts index 5009d5408..54509c618 100644 --- a/src/lib/flex/flex-order/flex-order.ts +++ b/src/lib/flex/flex-order/flex-order.ts @@ -14,7 +14,7 @@ import { OnDestroy, SimpleChanges, } from '@angular/core'; -import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; /** @@ -28,7 +28,7 @@ import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/f [fxFlexOrder.lt-sm], [fxFlexOrder.lt-md], [fxFlexOrder.lt-lg], [fxFlexOrder.lt-xl], [fxFlexOrder.gt-xs], [fxFlexOrder.gt-sm], [fxFlexOrder.gt-md], [fxFlexOrder.gt-lg] `}) -export class FlexOrderDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class FlexOrderDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { /* tslint:disable */ @Input('fxFlexOrder') set order(val) { this._cacheInput('order', val); } diff --git a/src/lib/flex/flex/flex.spec.ts b/src/lib/flex/flex/flex.spec.ts index e9facd4e0..70dbb73c1 100644 --- a/src/lib/flex/flex/flex.spec.ts +++ b/src/lib/flex/flex/flex.spec.ts @@ -283,7 +283,7 @@ describe('flex directive', () => { it('should work with calc values', () => { // @see http://caniuse.com/#feat=calc for IE issues with calc() componentWithTemplate(`
`); - if (!(platform.FIREFOX || platform.TRIDENT)) { + if (!(platform.FIREFOX || platform.EDGE)) { expectNativeEl(fixture).toHaveStyle({ 'box-sizing': 'border-box', 'flex-grow': '1', @@ -296,7 +296,7 @@ describe('flex directive', () => { it('should work with calc without internal whitespaces', async(() => { // @see http://caniuse.com/#feat=calc for IE issues with calc() componentWithTemplate('
'); - if (!(platform.FIREFOX || platform.TRIDENT)) { + if (!(platform.FIREFOX || platform.EDGE)) { fixture.detectChanges(); setTimeout(() => { expectNativeEl(fixture).toHaveStyle({ diff --git a/src/lib/flex/flex/flex.ts b/src/lib/flex/flex/flex.ts index 2e4072b0a..26d183663 100644 --- a/src/lib/flex/flex/flex.ts +++ b/src/lib/flex/flex/flex.ts @@ -19,7 +19,7 @@ import { } from '@angular/core'; import { ADD_FLEX_STYLES, - BaseFxDirective, + BaseDirective, MediaChange, MediaMonitor, StyleUtils, @@ -49,7 +49,7 @@ export type FlexBasisAlias = 'grow' | 'initial' | 'auto' | 'none' | 'nogrow' | ' [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg], ` }) -export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class FlexDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { /** The flex-direction of this element's flex container. Defaults to 'row'. */ protected _layout: Layout; @@ -163,7 +163,7 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, shrink: number|string, basis: string|number|FlexBasisAlias) { // The flex-direction of this element's flex container. Defaults to 'row'. - let layout = this._getFlowDirection(this.parentElement, !!this.addFlexStyles); + let layout = this._getFlexFlowDirection(this.parentElement, !!this.addFlexStyles); let direction = (layout.indexOf('column') > -1) ? 'column' : 'row'; let max = isFlowHorizontal(direction) ? 'max-width' : 'max-height'; diff --git a/src/lib/flex/layout-align/layout-align.ts b/src/lib/flex/layout-align/layout-align.ts index 13818108e..08720db3f 100644 --- a/src/lib/flex/layout-align/layout-align.ts +++ b/src/lib/flex/layout-align/layout-align.ts @@ -16,7 +16,7 @@ import { SimpleChanges, Self, } from '@angular/core'; -import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; import {Subscription} from 'rxjs'; import {extendObject} from '../../utils/object-extend'; @@ -38,7 +38,7 @@ import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator'; [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg] `}) -export class LayoutAlignDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class LayoutAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { protected _layout = 'row'; // default flex-direction protected _layoutWatcher: Subscription; diff --git a/src/lib/flex/layout-gap/layout-gap.ts b/src/lib/flex/layout-gap/layout-gap.ts index 479d4da69..e387c9cbf 100644 --- a/src/lib/flex/layout-gap/layout-gap.ts +++ b/src/lib/flex/layout-gap/layout-gap.ts @@ -19,7 +19,7 @@ import { } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { - BaseFxDirective, + BaseDirective, MediaChange, MediaMonitor, StyleDefinition, @@ -42,7 +42,7 @@ import {LAYOUT_VALUES} from '../../utils/layout-validator'; [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg] ` }) -export class LayoutGapDirective extends BaseFxDirective +export class LayoutGapDirective extends BaseDirective implements AfterContentInit, OnChanges, OnDestroy { protected _layout = 'row'; // default flex-direction protected _layoutWatcher: Subscription; diff --git a/src/lib/flex/layout/layout.ts b/src/lib/flex/layout/layout.ts index 6bbfa37f6..50df899ab 100644 --- a/src/lib/flex/layout/layout.ts +++ b/src/lib/flex/layout/layout.ts @@ -14,7 +14,7 @@ import { OnDestroy, SimpleChanges, } from '@angular/core'; -import {BaseFxDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; import {Observable, ReplaySubject} from 'rxjs'; import {buildLayoutCSS} from '../../utils/layout-validator'; @@ -37,7 +37,7 @@ export type Layout = { [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg] `}) -export class LayoutDirective extends BaseFxDirective implements OnInit, OnChanges, OnDestroy { +export class LayoutDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { /** * Create Observable for nested/child 'flex' directives. This allows diff --git a/src/lib/grid/README.md b/src/lib/grid/README.md new file mode 100644 index 000000000..cece012d0 --- /dev/null +++ b/src/lib/grid/README.md @@ -0,0 +1,19 @@ +The `grid` entrypoint contains all of the CSS Grid APIs provided by the +Layout library. This includes directives for flexbox containers like +`gdArea` (a.k.a. `GridAreaDirective`) and children like `gdRow` +(a.k.a. `GdRowDirective`). The main export from this entrypoint is the +`GridModule` that encapsulates these directives, and can be +imported separately to take advantage of tree shaking. + +```typescript +import {NgModule} from '@angular/core'; +import {GridModule} from '@angular/flex-layout/grid'; + +@NgModule(({ + imports: [ + ... other imports here + GridModule, + ] +})) +export class AppModule {} +``` diff --git a/src/lib/grid/align-columns/align-columns.spec.ts b/src/lib/grid/align-columns/align-columns.spec.ts new file mode 100644 index 000000000..856315791 --- /dev/null +++ b/src/lib/grid/align-columns/align-columns.spec.ts @@ -0,0 +1,411 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {extendObject} from '../../utils/object-extend'; +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('align columns directive', () => { + let fixture: ComponentFixture; + let matchMedia: MockMatchMedia; + let styler: StyleUtils; + let shouldRun = true; + let createTestComponent = (template: string) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestAlignComponent)(template); + + inject([MatchMedia, StyleUtils, Platform], + (_matchMedia: MockMatchMedia, _styler: StyleUtils, _platform: Platform) => { + matchMedia = _matchMedia; + styler = _styler; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestAlignComponent], + providers: [MockMatchMediaProvider], + }); + }); + + describe('with static features', () => { + + it('should add correct styles for default `gdAlignColumns` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + }); + + it('should work with inline grid', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({ + 'display': 'inline-grid' + }, DEFAULT_ALIGNS), + styler); + }); + + describe('for "main-axis" testing', () => { + it('should add correct styles for `gdAlignColumns="start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'start'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'end'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="stretch"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'stretch'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'center'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="space-around"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'space-around'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="space-between"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'space-between'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignColumns="space-evenly"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'align-content': 'space-evenly'}, CROSS_DEFAULT), styler + ); + }); + + it('should add ignore invalid row-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, CROSS_DEFAULT), styler + ); + }); + }); + + describe('for "cross-axis" testing', () => { + it('should add correct styles for `gdAlignColumns="start start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'align-items': 'start'}), styler + ); + }); + it('should add correct styles for `gdAlignColumns="start center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'align-items': 'center'}), styler + ); + }); + it('should add correct styles for `gdAlignColumns="start end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'align-items': 'end'}), styler + ); + }); + it('should add ignore invalid column-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, CROSS_DEFAULT), styler + ); + }); + }); + + describe('for dynamic inputs', () => { + it('should add correct styles and ignore invalid axes values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + fixture.componentInstance.alignBy = 'center end'; + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center', + 'align-items': 'end' + }, styler); + + fixture.componentInstance.alignBy = 'invalid invalid'; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + + fixture.componentInstance.alignBy = ''; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + }); + }); + + }); + + describe('with responsive features', () => { + + it('should ignore responsive changes when not configured', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center', + 'align-items': 'center' + }, styler); + }); + + it('should add responsive styles when configured', () => { + createTestComponent(` +
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center', + 'align-items': 'center' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'end', + 'align-items': 'stretch' + }, styler); + }); + + it('should fallback to default styles when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center', + 'align-items': 'stretch' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'end', + 'align-items': 'stretch' + }, styler); + + matchMedia.activate('xs'); + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center', + 'align-items': 'stretch' + }, styler); + }); + + it('should fallback to closest overlapping value when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + matchMedia.useOverlaps = true; + + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'start' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'center' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('lg', true); + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'end' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('xl', true); + expectNativeEl(fixture).toHaveStyle({ + 'align-content': 'end' + }, styler); + }); + + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestAlignComponent implements OnInit { + mainAxis = 'start'; + crossAxis = 'end'; + + set alignBy(style) { + let vals = style.split(' '); + this.mainAxis = vals[0]; + this.crossAxis = vals.length > 1 ? vals[1] : ''; + } + + get alignBy() { + return `${this.mainAxis} ${this.crossAxis}`; + } + + constructor() { + } + + ngOnInit() { + } +} + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +const DEFAULT_ALIGNS = { + 'align-content': 'start', + 'align-items': 'stretch' +}; +const MAIN_DEFAULT = { + 'align-content': 'start' +}; +const CROSS_DEFAULT = { + 'align-items': 'stretch' +}; + diff --git a/src/lib/grid/align-columns/align-columns.ts b/src/lib/grid/align-columns/align-columns.ts new file mode 100644 index 000000000..f512277d5 --- /dev/null +++ b/src/lib/grid/align-columns/align-columns.ts @@ -0,0 +1,162 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {extendObject} from '../../utils/object-extend'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'alignColumns'; +const DEFAULT_MAIN = 'start'; +const DEFAULT_CROSS = 'stretch'; + +/** + * 'column alignment' CSS Grid styling directive + * Configures the alignment in the column direction + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-19 + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-21 + */ +@Directive({selector: ` + [gdAlignColumns], + [gdAlignColumns.xs], [gdAlignColumns.sm], [gdAlignColumns.md], + [gdAlignColumns.lg], [gdAlignColumns.xl], [gdAlignColumns.lt-sm], + [gdAlignColumns.lt-md], [gdAlignColumns.lt-lg], [gdAlignColumns.lt-xl], + [gdAlignColumns.gt-xs], [gdAlignColumns.gt-sm], [gdAlignColumns.gt-md], + [gdAlignColumns.gt-lg] +`}) +export class GridAlignColumnsDirective extends BaseDirective + implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdAlignColumns') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdAlignColumns.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdAlignColumns.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdAlignColumns.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdAlignColumns.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdAlignColumns.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdAlignColumns.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdAlignColumns.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdAlignColumns.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdAlignColumns.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdAlignColumns.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdAlignColumns.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdAlignColumns.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdAlignColumns.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, + (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(align) { + let css = {}, [mainAxis, crossAxis] = align.split(' '); + + // Main axis + switch (mainAxis) { + case 'center': + css['align-content'] = 'center'; + break; + case 'space-around': + css['align-content'] = 'space-around'; + break; + case 'space-between': + css['align-content'] = 'space-between'; + break; + case 'space-evenly': + css['align-content'] = 'space-evenly'; + break; + case 'end': + css['align-content'] = 'end'; + break; + case 'start': + css['align-content'] = 'start'; + break; + case 'stretch': + css['align-content'] = 'stretch'; + break; + default: + css['align-content'] = DEFAULT_MAIN; // default main axis + break; + } + + // Cross-axis + switch (crossAxis) { + case 'start': + css['align-items'] = 'start'; + break; + case 'center': + css['align-items'] = 'center'; + break; + case 'end': + css['align-items'] = 'end'; + break; + case 'stretch': + css['align-items'] = 'stretch'; + break; + default : // 'stretch' + css['align-items'] = DEFAULT_CROSS; // default cross axis + break; + } + + return extendObject(css, {'display' : this._queryInput('inline') ? 'inline-grid' : 'grid'}); + } +} diff --git a/src/lib/grid/align-rows/align-rows.spec.ts b/src/lib/grid/align-rows/align-rows.spec.ts new file mode 100644 index 000000000..89af7cd2d --- /dev/null +++ b/src/lib/grid/align-rows/align-rows.spec.ts @@ -0,0 +1,411 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {extendObject} from '../../utils/object-extend'; +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; +import {Platform} from '@angular/cdk/platform'; + +describe('align rows directive', () => { + let fixture: ComponentFixture; + let matchMedia: MockMatchMedia; + let styler: StyleUtils; + let shouldRun = true; + let createTestComponent = (template: string) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestAlignComponent)(template); + + inject([MatchMedia, StyleUtils, Platform], + (_matchMedia: MockMatchMedia, _styler: StyleUtils, _platform: Platform) => { + matchMedia = _matchMedia; + styler = _styler; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestAlignComponent], + providers: [MockMatchMediaProvider], + }); + }); + + describe('with static features', () => { + + it('should add correct styles for default `gdAlignRows` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + }); + + it('should work with inline grid', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({ + 'display': 'inline-grid' + }, DEFAULT_ALIGNS), + styler); + }); + + describe('for "main-axis" testing', () => { + it('should add correct styles for `gdAlignRows="start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'start'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'end'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="stretch"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'stretch'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'center'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="space-around"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'space-around'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="space-between"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'space-between'}, CROSS_DEFAULT), styler + ); + }); + it('should add correct styles for `gdAlignRows="space-evenly"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-content': 'space-evenly'}, CROSS_DEFAULT), styler + ); + }); + + it('should add ignore invalid row-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, CROSS_DEFAULT), styler + ); + }); + }); + + describe('for "cross-axis" testing', () => { + it('should add correct styles for `gdAlignRows="start start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'justify-items': 'start'}), styler + ); + }); + it('should add correct styles for `gdAlignRows="start center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'justify-items': 'center'}), styler + ); + }); + it('should add correct styles for `gdAlignRows="start end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, {'justify-items': 'end'}), styler + ); + }); + it('should add ignore invalid column-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(MAIN_DEFAULT, CROSS_DEFAULT), styler + ); + }); + }); + + describe('for dynamic inputs', () => { + it('should add correct styles and ignore invalid axes values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + fixture.componentInstance.alignBy = 'center end'; + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center', + 'justify-items': 'end' + }, styler); + + fixture.componentInstance.alignBy = 'invalid invalid'; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + + fixture.componentInstance.alignBy = ''; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + }); + }); + + }); + + describe('with responsive features', () => { + + it('should ignore responsive changes when not configured', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center', + 'justify-items': 'center' + }, styler); + }); + + it('should add responsive styles when configured', () => { + createTestComponent(` +
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center', + 'justify-items': 'center' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'end', + 'justify-items': 'stretch' + }, styler); + }); + + it('should fallback to default styles when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center', + 'justify-items': 'stretch' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'end', + 'justify-items': 'stretch' + }, styler); + + matchMedia.activate('xs'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center', + 'justify-items': 'stretch' + }, styler); + }); + + it('should fallback to closest overlapping value when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + matchMedia.useOverlaps = true; + + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'start' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'center' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('lg', true); + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'end' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('xl', true); + expectNativeEl(fixture).toHaveStyle({ + 'justify-content': 'end' + }, styler); + }); + + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestAlignComponent implements OnInit { + mainAxis = 'start'; + crossAxis = 'end'; + + set alignBy(style) { + let vals = style.split(' '); + this.mainAxis = vals[0]; + this.crossAxis = vals.length > 1 ? vals[1] : ''; + } + + get alignBy() { + return `${this.mainAxis} ${this.crossAxis}`; + } + + constructor() { + } + + ngOnInit() { + } +} + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +const DEFAULT_ALIGNS = { + 'justify-content': 'start', + 'justify-items': 'stretch' +}; +const MAIN_DEFAULT = { + 'justify-content': 'start' +}; +const CROSS_DEFAULT = { + 'justify-items': 'stretch' +}; + diff --git a/src/lib/grid/align-rows/align-rows.ts b/src/lib/grid/align-rows/align-rows.ts new file mode 100644 index 000000000..6b7e339cc --- /dev/null +++ b/src/lib/grid/align-rows/align-rows.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {extendObject} from '../../utils/object-extend'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'alignRows'; +const DEFAULT_MAIN = 'start'; +const DEFAULT_CROSS = 'stretch'; + +/** + * 'row alignment' CSS Grid styling directive + * Configures the alignment in the row direction + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-18 + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-20 + */ +@Directive({selector: ` + [gdAlignRows], + [gdAlignRows.xs], [gdAlignRows.sm], [gdAlignRows.md], + [gdAlignRows.lg], [gdAlignRows.xl], [gdAlignRows.lt-sm], + [gdAlignRows.lt-md], [gdAlignRows.lt-lg], [gdAlignRows.lt-xl], + [gdAlignRows.gt-xs], [gdAlignRows.gt-sm], [gdAlignRows.gt-md], + [gdAlignRows.gt-lg] +`}) +export class GridAlignRowsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdAlignRows') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdAlignRows.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdAlignRows.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdAlignRows.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdAlignRows.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdAlignRows.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdAlignRows.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdAlignRows.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdAlignRows.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdAlignRows.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdAlignRows.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdAlignRows.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdAlignRows.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdAlignRows.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, + (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(align) { + let css = {}, [mainAxis, crossAxis] = align.split(' '); + + // Main axis + switch (mainAxis) { + case 'center': + case 'space-around': + case 'space-between': + case 'space-evenly': + case 'end': + case 'start': + case 'stretch': + css['justify-content'] = mainAxis; + break; + default: + css['justify-content'] = DEFAULT_MAIN; // default main axis + break; + } + + // Cross-axis + switch (crossAxis) { + case 'start': + case 'center': + case 'end': + case 'stretch': + css['justify-items'] = crossAxis; + break; + default : // 'stretch' + css['justify-items'] = DEFAULT_CROSS; // default cross axis + break; + } + + return extendObject(css, {'display' : this._queryInput('inline') ? 'inline-grid' : 'grid'}); + } +} diff --git a/src/lib/grid/area/area.spec.ts b/src/lib/grid/area/area.spec.ts new file mode 100644 index 000000000..039f5225d --- /dev/null +++ b/src/lib/grid/area/area.spec.ts @@ -0,0 +1,220 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import { + expectEl, + expectNativeEl, + queryFor, + makeCreateTestComponent, +} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid area child directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridAreaComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridAreaComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add area styles for children', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + + let nodes = queryFor(fixture, '[gdArea]'); + expect(nodes.length).toBe(3); + if (platform.WEBKIT) { + expectEl(nodes[1]).toHaveStyle({ + 'grid-row-start': 'grace', + 'grid-row-end': 'grace', + 'grid-column-start': 'sarah', + 'grid-column-end': 'sarah', + }, styler); + } else { + let areaStyles = styler.lookupStyle(nodes[1].nativeElement, 'grid-area'); + let correctArea = areaStyles === 'grace / sarah' || + areaStyles === 'grace / sarah / grace / sarah'; + expect(correctArea).toBe(true); + } + }); + + it('should add dynamic area styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'grid-row-start': 'sidebar', + 'grid-row-end': 'sidebar', + 'grid-column-start': 'sidebar', + 'grid-column-end': 'sidebar', + }, styler); + } else { + fixture.detectChanges(); + let areaStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-area'); + let correctArea = areaStyles === 'sidebar' || + areaStyles === 'sidebar / sidebar / sidebar / sidebar'; + expect(correctArea).toBe(true); + } + + fixture.componentInstance.area = 'header'; + + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'grid-row-start': 'header', + 'grid-row-end': 'header', + 'grid-column-start': 'header', + 'grid-column-end': 'header', + }, styler); + } else { + fixture.detectChanges(); + let areaStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-area'); + let correctArea = areaStyles === 'header' || + areaStyles === 'header / header / header / header'; + expect(correctArea).toBe(true); + } + }); + }); + + describe('with responsive features', () => { + it('should add row styles for a child', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'grid-row-start': 'sidebar', + 'grid-row-end': 'sidebar', + 'grid-column-start': 'sidebar', + 'grid-column-end': 'sidebar', + }, styler); + } else { + fixture.detectChanges(); + let areaStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-area'); + let correctArea = areaStyles === 'sidebar' || + areaStyles === 'sidebar / sidebar / sidebar / sidebar'; + expect(correctArea).toBe(true); + } + + matchMedia.activate('xs'); + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'grid-row-start': 'footer', + 'grid-row-end': 'footer', + 'grid-column-start': 'footer', + 'grid-column-end': 'footer', + }, styler); + } else { + let areaStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-area'); + let correctArea = areaStyles === 'footer' || + areaStyles === 'footer / footer / footer / footer'; + expect(correctArea).toBe(true); + } + + matchMedia.activate('md'); + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'grid-row-start': 'sidebar', + 'grid-row-end': 'sidebar', + 'grid-column-start': 'sidebar', + 'grid-column-end': 'sidebar', + }, styler); + } else { + let areaStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-area'); + let correctArea = areaStyles === 'sidebar' || + areaStyles === 'sidebar / sidebar / sidebar / sidebar'; + expect(correctArea).toBe(true); + } + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridAreaComponent { + area = 'sidebar'; +} diff --git a/src/lib/grid/area/area.ts b/src/lib/grid/area/area.ts new file mode 100644 index 000000000..b207e7376 --- /dev/null +++ b/src/lib/grid/area/area.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + +const CACHE_KEY = 'area'; +const DEFAULT_VALUE = 'auto'; + +/** + * 'grid-area' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-27 + */ +@Directive({selector: ` + [gdArea], + [gdArea.xs], [gdArea.sm], [gdArea.md], [gdArea.lg], [gdArea.xl], + [gdArea.lt-sm], [gdArea.lt-md], [gdArea.lt-lg], [gdArea.lt-xl], + [gdArea.gt-xs], [gdArea.gt-sm], [gdArea.gt-md], [gdArea.gt-lg] +`}) +export class GridAreaDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdArea') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdArea.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdArea.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdArea.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdArea.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdArea.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdArea.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdArea.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdArea.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdArea.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdArea.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdArea.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdArea.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdArea.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + return {'grid-area': value}; + } +} diff --git a/src/lib/grid/areas/areas.spec.ts b/src/lib/grid/areas/areas.spec.ts new file mode 100644 index 000000000..e7b0714cb --- /dev/null +++ b/src/lib/grid/areas/areas.spec.ts @@ -0,0 +1,220 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {expectNativeEl, makeCreateTestComponent} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid area parent directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridAreaComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridAreaComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add area styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Edge currently has a bug with template areas + // when they don't have columns/rows explicitly set. Remove when fixed + if (platform.EDGE) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': '"header" "header" "sidebar" "footer"' + }, styler); + }); + + it('should work with inline grid', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Edge currently has a bug with template areas + // when they don't have columns/rows explicitly set. Remove when fixed + if (platform.EDGE) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'inline-grid', + 'grid-template-areas': '"header" "header" "sidebar" "footer"' + }, styler); + }); + + it('should work with weird spacing', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Edge currently has a bug with template areas + // when they don't have columns/rows explicitly set. Remove when fixed + if (platform.EDGE) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': '"header" "header" "sidebar" "footer"' + }, styler); + }); + + it('should add dynamic area styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Edge currently has a bug with template areas + // when they don't have columns/rows explicitly set. Remove when fixed + if (platform.EDGE) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'grid-template-areas': '"sidebar" "sidebar"' + }, styler); + + fixture.componentInstance.areas = 'header | header | sidebar'; + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': '"header" "header" "sidebar"' + }, styler); + }); + }); + + describe('with responsive features', () => { + it('should add row styles for a child', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Edge currently has a bug with template areas + // when they don't have columns/rows explicitly set. Remove when fixed + if (platform.EDGE) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': + '"header header header" "sidebar content content" "footer footer footer"' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': '"header header" "sidebar content" "footer footer"' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-areas': + '"header header header" "sidebar content content" "footer footer footer"' + }, styler); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridAreaComponent { + areas = 'sidebar | sidebar'; +} diff --git a/src/lib/grid/areas/areas.ts b/src/lib/grid/areas/areas.ts new file mode 100644 index 000000000..f2343a34b --- /dev/null +++ b/src/lib/grid/areas/areas.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'areas'; +const DEFAULT_VALUE = 'none'; +const DELIMETER = '|'; + +/** + * 'grid-template-areas' CSS Grid styling directive + * Configures the names of elements within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-14 + */ +@Directive({selector: ` + [gdAreas], + [gdAreas.xs], [gdAreas.sm], [gdAreas.md], [gdAreas.lg], [gdAreas.xl], + [gdAreas.lt-sm], [gdAreas.lt-md], [gdAreas.lt-lg], [gdAreas.lt-xl], + [gdAreas.gt-xs], [gdAreas.gt-sm], [gdAreas.gt-md], [gdAreas.gt-lg] +`}) +export class GridAreasDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdAreas') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdAreas.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdAreas.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdAreas.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdAreas.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdAreas.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdAreas.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdAreas.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdAreas.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdAreas.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdAreas.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdAreas.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdAreas.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdAreas.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + const areas = value.split(DELIMETER).map(v => `"${v.trim()}"`); + + return { + 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', + 'grid-template-areas': areas.join(' ') + }; + } +} diff --git a/src/lib/grid/auto/auto.spec.ts b/src/lib/grid/auto/auto.spec.ts new file mode 100644 index 000000000..a73e80070 --- /dev/null +++ b/src/lib/grid/auto/auto.spec.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {expectNativeEl, makeCreateTestComponent} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid auto parent directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridAutoComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridAutoComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add auto styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row' + }, styler); + }); + + it('should work with inline grid', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'inline-grid', + 'grid-auto-flow': 'row' + }, styler); + }); + + it('should work with row values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row' + }, styler); + }); + + it('should work with column values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'column' + }, styler); + }); + + it('should work with dense values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': (platform.FIREFOX || platform.EDGE) ? 'row dense' : 'dense' + }, styler); + }); + + it('should filter double dense values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': (platform.FIREFOX || platform.EDGE) ? 'row dense' : 'dense' + }, styler); + }); + + it('should work with column dense values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'column dense' + }, styler); + }); + + it('should work with row dense values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row dense' + }, styler); + }); + + it('should work with invalid direction values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row dense' + }, styler); + }); + + it('should work with invalid dense values', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'column' + }, styler); + }); + + it('should add dynamic area styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row' + }, styler); + + fixture.componentInstance.auto = 'column'; + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'column' + }, styler); + }); + }); + + describe('with responsive features', () => { + it('should add row styles for a child', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'column' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-flow': 'row' + }, styler); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridAutoComponent { + auto = 'row'; +} diff --git a/src/lib/grid/auto/auto.ts b/src/lib/grid/auto/auto.ts new file mode 100644 index 000000000..3eaf81540 --- /dev/null +++ b/src/lib/grid/auto/auto.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'autoFlow'; +const DEFAULT_VALUE = 'initial'; + +/** + * 'grid-auto-flow' CSS Grid styling directive + * Configures the auto placement algorithm for the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-23 + */ +@Directive({selector: ` + [gdAuto], + [gdAuto.xs], [gdAuto.sm], [gdAuto.md], [gdAuto.lg], [gdAuto.xl], + [gdAuto.lt-sm], [gdAuto.lt-md], [gdAuto.lt-lg], [gdAuto.lt-xl], + [gdAuto.gt-xs], [gdAuto.gt-sm], [gdAuto.gt-md], [gdAuto.gt-lg] +`}) +export class GridAutoDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdAuto') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdAuto.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdAuto.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdAuto.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdAuto.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdAuto.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdAuto.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdAuto.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdAuto.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdAuto.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdAuto.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdAuto.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdAuto.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdAuto.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + let [direction, dense] = value.split(' '); + if (direction !== 'column' && direction !== 'row' && direction !== 'dense') { + direction = 'row'; + } + + dense = (dense === 'dense' && direction !== 'dense') ? ' dense' : ''; + + return { + 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', + 'grid-auto-flow': direction + dense + }; + } +} diff --git a/src/lib/grid/column/column.spec.ts b/src/lib/grid/column/column.spec.ts new file mode 100644 index 000000000..6bbddc0f3 --- /dev/null +++ b/src/lib/grid/column/column.spec.ts @@ -0,0 +1,170 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import { + expectEl, + queryFor, + makeCreateTestComponent, +} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid column child directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridColumnComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridColumnComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add column styles for children', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + + let nodes = queryFor(fixture, '[gdColumn]'); + expect(nodes.length).toBe(3); + + if (platform.WEBKIT) { + expectEl(nodes[1]).toHaveStyle({ + 'grid-column-start': 'span 2', + 'grid-column-end': '6', + }, styler); + } else { + expectEl(nodes[1]).toHaveStyle({'grid-column': 'span 2 / 6'}, styler); + } + }); + + it('should add dynamic column styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + + let colStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-column'); + let correctCol = colStyles === 'apples' || colStyles === 'apples / apples' || + colStyles === 'apples apples'; + + expect(correctCol).toBe(true); + + fixture.componentInstance.col = 'oranges'; + fixture.detectChanges(); + + colStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, 'grid-column'); + correctCol = colStyles === 'oranges' || colStyles === 'oranges / oranges' || + colStyles === 'oranges oranges'; + expect(correctCol).toBe(true); + }); + }); + + describe('with responsive features', () => { + it('should add row styles for a child', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + let colStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-column'); + let correctCol = colStyles === 'sidebar' || colStyles === 'sidebar / sidebar' || + colStyles === 'sidebar sidebar'; + expect(correctCol).toBe(true); + + matchMedia.activate('xs'); + colStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-column'); + correctCol = colStyles === 'footer' || colStyles === 'footer / footer' || + colStyles === 'footer footer'; + expect(correctCol).toBe(true); + + matchMedia.activate('md'); + colStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-column'); + correctCol = colStyles === 'sidebar' || colStyles === 'sidebar / sidebar' || + colStyles === 'sidebar sidebar'; + expect(correctCol).toBe(true); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridColumnComponent { + col = 'apples'; +} diff --git a/src/lib/grid/column/column.ts b/src/lib/grid/column/column.ts new file mode 100644 index 000000000..14b09f9c8 --- /dev/null +++ b/src/lib/grid/column/column.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + +const CACHE_KEY = 'column'; +const DEFAULT_VALUE = 'auto'; + +/** + * 'grid-column' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 + */ +@Directive({selector: ` + [gdColumn], + [gdColumn.xs], [gdColumn.sm], [gdColumn.md], [gdColumn.lg], [gdColumn.xl], + [gdColumn.lt-sm], [gdColumn.lt-md], [gdColumn.lt-lg], [gdColumn.lt-xl], + [gdColumn.gt-xs], [gdColumn.gt-sm], [gdColumn.gt-md], [gdColumn.gt-lg] +`}) +export class GridColumnDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdColumn') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdColumn.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdColumn.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdColumn.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdColumn.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdColumn.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdColumn.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdColumn.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdColumn.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdColumn.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdColumn.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdColumn.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdColumn.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdColumn.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + return {'grid-column': value}; + } +} diff --git a/src/lib/grid/columns/columns.spec.ts b/src/lib/grid/columns/columns.spec.ts new file mode 100644 index 000000000..bbd894267 --- /dev/null +++ b/src/lib/grid/columns/columns.spec.ts @@ -0,0 +1,193 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {expectNativeEl, makeCreateTestComponent} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid columns parent directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridColumnsComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridColumnsComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add column styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '100px 1fr' + }, styler); + }); + + it('should add auto column styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Firefox has an issue with auto tracks, + // caused by rachelandrew/gridbugs#1 + if (!platform.FIREFOX) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-columns': '100px 1fr auto' + }, styler); + } + }); + + it('should work with inline grid', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'inline-grid', + 'grid-template-columns': '100px 1fr' + }, styler); + }); + + it('should add dynamic columns styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '50px 1fr' + }, styler); + + fixture.componentInstance.cols = '100px 1fr'; + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '100px 1fr' + }, styler); + }); + }); + + describe('with responsive features', () => { + it('should add col styles for a parent', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '100px 1fr' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '50px 1fr' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-columns': '100px 1fr' + }, styler); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridColumnsComponent { + cols = '50px 1fr'; +} diff --git a/src/lib/grid/columns/columns.ts b/src/lib/grid/columns/columns.ts new file mode 100644 index 000000000..2166e81d7 --- /dev/null +++ b/src/lib/grid/columns/columns.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'columns'; +const DEFAULT_VALUE = 'none'; +const AUTO_SPECIFIER = '!'; + +/** + * 'grid-template-columns' CSS Grid styling directive + * Configures the sizing for the columns in the grid + * Syntax: [auto] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 + */ +@Directive({selector: ` + [gdColumns], + [gdColumns.xs], [gdColumns.sm], [gdColumns.md], [gdColumns.lg], [gdColumns.xl], + [gdColumns.lt-sm], [gdColumns.lt-md], [gdColumns.lt-lg], [gdColumns.lt-xl], + [gdColumns.gt-xs], [gdColumns.gt-sm], [gdColumns.gt-md], [gdColumns.gt-lg] +`}) +export class GridColumnsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdColumns') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdColumns.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdColumns.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdColumns.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdColumns.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdColumns.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdColumns.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdColumns.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdColumns.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdColumns.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdColumns.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdColumns.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdColumns.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdColumns.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + let auto = false; + if (value.endsWith(AUTO_SPECIFIER)) { + value = value.substring(0, value.indexOf(AUTO_SPECIFIER)); + auto = true; + } + + let css = { + 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', + 'grid-auto-columns': '', + 'grid-template-columns': '', + }; + const key = (auto ? 'grid-auto-columns' : 'grid-template-columns'); + css[key] = value; + + return css; + } +} diff --git a/src/lib/grid/gap/gap.spec.ts b/src/lib/grid/gap/gap.spec.ts new file mode 100644 index 000000000..de9d1b216 --- /dev/null +++ b/src/lib/grid/gap/gap.spec.ts @@ -0,0 +1,259 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {expectNativeEl, makeCreateTestComponent} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid gap directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestLayoutGapComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestLayoutGapComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add gap styles for a parent', () => { + let template = ` +
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '10px', + 'grid-column-gap': '10px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({'display': 'grid'}, styler); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '10px' || gapStyle == '10px 10px'; + expect(correctGap).toBe(true); + } + }); + + it('should add gap styles with multiple values for a parent', () => { + let template = ` +
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '10px', + 'grid-column-gap': '15px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-gap': '10px 15px', + }, styler); + } + }); + + it('should add dynamic gap styles', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '8px', + 'grid-column-gap': '8px', + }, styler); + } else { + fixture.detectChanges(); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '8px' || gapStyle == '8px 8px'; + expect(correctGap).toBe(true); + } + + fixture.componentInstance.gap = '16px'; + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '16px', + 'grid-column-gap': '16px', + }, styler); + } else { + fixture.detectChanges(); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '16px' || gapStyle == '16px 16px'; + expect(correctGap).toBe(true); + } + }); + + it('should add inline grid css style', () => { + let template = ` +
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'inline-grid', + 'grid-row-gap': '10px', + 'grid-column-gap': '10px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({'display': 'inline-grid'}, styler); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '10px' || gapStyle == '10px 10px'; + expect(correctGap).toBe(true); + } + }); + }); + + describe('with responsive features', () => { + it('should add gap styles for a parent', () => { + let template = ` +
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '10px', + 'grid-column-gap': '10px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({'display': 'grid'}, styler); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '10px' || gapStyle == '10px 10px'; + expect(correctGap).toBe(true); + } + + matchMedia.activate('xs'); + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '16px', + 'grid-column-gap': '16px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({'display': 'grid'}, styler); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '16px' || gapStyle == '16px 16px'; + expect(correctGap).toBe(true); + } + + matchMedia.activate('md'); + if (platform.WEBKIT) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-row-gap': '10px', + 'grid-column-gap': '10px', + }, styler); + } else { + expectNativeEl(fixture).toHaveStyle({'display': 'grid'}, styler); + let gapStyle = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-gap'); + let correctGap = gapStyle === '10px' || gapStyle == '10px 10px'; + expect(correctGap).toBe(true); + } + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestLayoutGapComponent { + gap = '8px'; +} diff --git a/src/lib/grid/gap/gap.ts b/src/lib/grid/gap/gap.ts new file mode 100644 index 000000000..de163550c --- /dev/null +++ b/src/lib/grid/gap/gap.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'gap'; +const DEFAULT_VALUE = '0'; + +/** + * 'grid-gap' CSS Grid styling directive + * Configures the gap between items in the grid + * Syntax: [] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-17 + */ +@Directive({selector: ` + [gdGap], + [gdGap.xs], [gdGap.sm], [gdGap.md], [gdGap.lg], [gdGap.xl], + [gdGap.lt-sm], [gdGap.lt-md], [gdGap.lt-lg], [gdGap.lt-xl], + [gdGap.gt-xs], [gdGap.gt-sm], [gdGap.gt-md], [gdGap.gt-lg] +`}) +export class GridGapDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdGap') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdGap.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdGap.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdGap.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdGap.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdGap.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdGap.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdGap.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdGap.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdGap.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdGap.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdGap.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdGap.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdGap.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + return { + 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', + 'grid-gap': value + }; + } +} diff --git a/src/lib/grid/grid-align/grid-align.spec.ts b/src/lib/grid/grid-align/grid-align.spec.ts new file mode 100644 index 000000000..db7c08324 --- /dev/null +++ b/src/lib/grid/grid-align/grid-align.spec.ts @@ -0,0 +1,366 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {FlexLayoutModule} from '../../module'; +import {extendObject} from '../../utils/object-extend'; +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; + +describe('align directive', () => { + let fixture: ComponentFixture; + let matchMedia: MockMatchMedia; + let styler: StyleUtils; + let shouldRun = true; + let createTestComponent = (template: string) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestAlignComponent)(template); + + inject([MatchMedia, StyleUtils, Platform], + (_matchMedia: MockMatchMedia, _styler: StyleUtils, _platform: Platform) => { + matchMedia = _matchMedia; + styler = _styler; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, FlexLayoutModule], + declarations: [TestAlignComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true} + ] + }); + }); + + describe('with static features', () => { + + it('should add correct styles for default `fxLayoutAlign` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({'justify-self': 'stretch'}, styler); + }); + + describe('for "main-axis" testing', () => { + it('should add correct styles for `gdGridAlign="start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-self': 'start'}, COLUMN_DEFAULT), styler + ); + }); + it('should add correct styles for `gdGridAlign="center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-self': 'center'}, COLUMN_DEFAULT), styler + ); + }); + it('should add correct styles for `gdGridAlign="end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-self': 'end'}, COLUMN_DEFAULT), styler + ); + }); + it('should add correct styles for `gdGridAlign="stretch"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-self': 'stretch'}, COLUMN_DEFAULT), styler + ); + }); + it('should add ignore invalid row-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject({'justify-self': 'stretch'}, COLUMN_DEFAULT), styler + ); + }); + }); + + describe('for "column-axis" testing', () => { + it('should add correct styles for `gdGridAlign="start start"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(ROW_DEFAULT, {'align-self': 'start'}), styler + ); + }); + it('should add correct styles for `gdGridAlign="start center"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(ROW_DEFAULT, {'align-self': 'center'}), styler + ); + }); + it('should add correct styles for `gdGridAlign="start end"` usage', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(ROW_DEFAULT, {'align-self': 'end'}), styler + ); + }); + it('should add ignore invalid column-axis values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle( + extendObject(ROW_DEFAULT, {'align-self': 'stretch'}), styler + ); + }); + }); + + describe('for dynamic inputs', () => { + it('should add correct styles and ignore invalid axes values', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + fixture.componentInstance.alignBy = 'center end'; + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center', + 'align-self': 'end' + }, styler); + + fixture.componentInstance.alignBy = 'invalid invalid'; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + + fixture.componentInstance.alignBy = ''; + expectNativeEl(fixture).toHaveStyle(DEFAULT_ALIGNS, styler); + }); + }); + + }); + + describe('with responsive features', () => { + + it('should ignore responsive changes when not configured', () => { + createTestComponent(`
`); + + if (!shouldRun) { + return; + } + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center', + 'align-self': 'center' + }, styler); + }); + + it('should add responsive styles when configured', () => { + createTestComponent(` +
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center', + 'align-self': 'center' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'end', + 'align-self': 'stretch' + }, styler); + }); + + it('should fallback to default styles when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center', + 'align-self': 'stretch' + }, styler); + + matchMedia.activate('md'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'end', + 'align-self': 'stretch' + }, styler); + + matchMedia.activate('xs'); + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center', + 'align-self': 'stretch' + }, styler); + }); + + it('should fallback to closest overlapping value when the active mediaQuery change is not configured', () => { // tslint:disable-line:max-line-length + createTestComponent(` +
+
+ `); + + if (!shouldRun) { + return; + } + + matchMedia.useOverlaps = true; + + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'start' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'center' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('lg', true); + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'end' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'start' + }, styler); + + // Should fallback to value for 'gt-xs' or default + matchMedia.activate('xl', true); + expectNativeEl(fixture).toHaveStyle({ + 'justify-self': 'end' + }, styler); + }); + + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestAlignComponent implements OnInit { + mainAxis = 'start'; + crossAxis = 'end'; + + set alignBy(style) { + let vals = style.split(' '); + this.mainAxis = vals[0]; + this.crossAxis = vals.length > 1 ? vals[1] : ''; + } + + get alignBy() { + return `${this.mainAxis} ${this.crossAxis}`; + } + + constructor() { + } + + ngOnInit() { + } +} + + +// ***************************************************************** +// Template Component +// ***************************************************************** + +const DEFAULT_ALIGNS = { + 'justify-self': 'stretch', + 'align-self': 'stretch' +}; +const ROW_DEFAULT = { + 'justify-self': 'stretch' +}; +const COLUMN_DEFAULT = { + 'align-self': 'stretch' +}; + diff --git a/src/lib/grid/grid-align/grid-align.ts b/src/lib/grid/grid-align/grid-align.ts new file mode 100644 index 000000000..03d37923f --- /dev/null +++ b/src/lib/grid/grid-align/grid-align.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + +const CACHE_KEY = 'align'; +const ROW_DEFAULT = 'stretch'; +const COL_DEFAULT = 'stretch'; + +/** + * 'align' CSS Grid styling directive for grid children + * Defines positioning of child elements along row and column axis in a grid container + * Optional values: {row-axis} values or {row-axis column-axis} value pairs + * + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-justify-self + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-align-self + */ +@Directive({selector: ` + [gdGridAlign], + [gdGridAlign.xs], [gdGridAlign.sm], [gdGridAlign.md], [gdGridAlign.lg],[gdGridAlign.xl], + [gdGridAlign.lt-sm], [gdGridAlign.lt-md], [gdGridAlign.lt-lg], [gdGridAlign.lt-xl], + [gdGridAlign.gt-xs], [gdGridAlign.gt-sm], [gdGridAlign.gt-md], [gdGridAlign.gt-lg] +`}) +export class GridAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdGridAlign') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdGridAlign.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdGridAlign.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdGridAlign.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdGridAlign.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdGridAlign.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdGridAlign.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdGridAlign.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdGridAlign.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdGridAlign.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdGridAlign.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdGridAlign.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdGridAlign.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdGridAlign.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, ROW_DEFAULT, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + /** + * + */ + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || ROW_DEFAULT; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + protected _buildCSS(align) { + let css = {}, [rowAxis, columnAxis] = align.split(' '); + + // Row axis + switch (rowAxis) { + case 'end': + css['justify-self'] = 'end'; + break; + case 'center': + css['justify-self'] = 'center'; + break; + case 'stretch': + css['justify-self'] = 'stretch'; + break; + case 'start': + css['justify-self'] = 'start'; + break; + default: + css['justify-self'] = ROW_DEFAULT; // default row axis + break; + } + + // Column axis + switch (columnAxis) { + case 'end': + css['align-self'] = 'end'; + break; + case 'center': + css['align-self'] = 'center'; + break; + case 'stretch': + css['align-self'] = 'stretch'; + break; + case 'start': + css['align-self'] = 'start'; + break; + default: + css['align-self'] = COL_DEFAULT; // default column axis + break; + } + + return css; + } +} diff --git a/src/lib/grid/index.ts b/src/lib/grid/index.ts new file mode 100644 index 000000000..676ca90f1 --- /dev/null +++ b/src/lib/grid/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/lib/grid/module.ts b/src/lib/grid/module.ts new file mode 100644 index 000000000..6bcf949e5 --- /dev/null +++ b/src/lib/grid/module.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {NgModule} from '@angular/core'; +import {CoreModule} from '@angular/flex-layout/core'; + +import {GridAlignDirective} from './grid-align/grid-align'; +import {GridAlignColumnsDirective} from './align-columns/align-columns'; +import {GridAlignRowsDirective} from './align-rows/align-rows'; +import {GridAreaDirective} from './area/area'; +import {GridAreasDirective} from './areas/areas'; +import {GridAutoDirective} from './auto/auto'; +import {GridColumnDirective} from './column/column'; +import {GridColumnsDirective} from './columns/columns'; +import {GridGapDirective} from './gap/gap'; +import {GridRowDirective} from './row/row'; +import {GridRowsDirective} from './rows/rows'; + + +const ALL_DIRECTIVES = [ + GridAlignDirective, + GridAlignColumnsDirective, + GridAlignRowsDirective, + GridAreaDirective, + GridAreasDirective, + GridAutoDirective, + GridColumnDirective, + GridColumnsDirective, + GridGapDirective, + GridRowDirective, + GridRowsDirective, +]; + +/** + * ***************************************************************** + * Define module for the CSS Grid API + * ***************************************************************** + */ + +@NgModule({ + imports: [CoreModule], + declarations: [...ALL_DIRECTIVES], + exports: [...ALL_DIRECTIVES] +}) +export class GridModule { +} diff --git a/src/lib/grid/public-api.ts b/src/lib/grid/public-api.ts new file mode 100644 index 000000000..dad40e4ae --- /dev/null +++ b/src/lib/grid/public-api.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './module'; + + diff --git a/src/lib/grid/row/row.spec.ts b/src/lib/grid/row/row.spec.ts new file mode 100644 index 000000000..d0fc0442d --- /dev/null +++ b/src/lib/grid/row/row.spec.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import { + expectEl, + queryFor, + makeCreateTestComponent, +} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid row child directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridRowComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridRowComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add row styles for children', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + + let nodes = queryFor(fixture, '[gdRow]'); + expect(nodes.length).toBe(3); + if (platform.WEBKIT) { + expectEl(nodes[1]).toHaveStyle({ + 'grid-row-start': 'span 2', + 'grid-row-end': '6', + }, styler); + } else { + expectEl(nodes[1]).toHaveStyle({'grid-row': 'span 2 / 6'}, styler); + } + }); + + it('should add dynamic row styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + + let rowStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-row'); + let correctRow = rowStyles === 'apples' || rowStyles === 'apples / apples' || + rowStyles === 'apples apples'; + + expect(correctRow).toBe(true); + + fixture.componentInstance.row = 'oranges'; + fixture.detectChanges(); + + rowStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, 'grid-row'); + correctRow = rowStyles === 'oranges' || rowStyles === 'oranges / oranges' || + rowStyles === 'oranges oranges'; + expect(correctRow).toBe(true); + }); + }); + + describe('with responsive features', () => { + it('should add row styles for a child', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + fixture.detectChanges(); + let rowStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-row'); + let correctRow = rowStyles === 'sidebar' || rowStyles === 'sidebar / sidebar' || + rowStyles === 'sidebar sidebar'; + expect(correctRow).toBe(true); + + matchMedia.activate('xs'); + rowStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-row'); + correctRow = rowStyles === 'footer' || rowStyles === 'footer / footer' || + rowStyles === 'footer footer'; + expect(correctRow).toBe(true); + + matchMedia.activate('md'); + rowStyles = styler.lookupStyle(fixture.debugElement.children[0].nativeElement, + 'grid-row'); + correctRow = rowStyles === 'sidebar' || rowStyles === 'sidebar / sidebar' || + rowStyles === 'sidebar sidebar'; + expect(correctRow).toBe(true); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridRowComponent { + row = 'apples'; +} diff --git a/src/lib/grid/row/row.ts b/src/lib/grid/row/row.ts new file mode 100644 index 000000000..18761562f --- /dev/null +++ b/src/lib/grid/row/row.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + +const CACHE_KEY = 'row'; +const DEFAULT_VALUE = 'auto'; + +/** + * 'grid-row' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 + */ +@Directive({selector: ` + [gdRow], + [gdRow.xs], [gdRow.sm], [gdRow.md], [gdRow.lg], [gdRow.xl], + [gdRow.lt-sm], [gdRow.lt-md], [gdRow.lt-lg], [gdRow.lt-xl], + [gdRow.gt-xs], [gdRow.gt-sm], [gdRow.gt-md], [gdRow.gt-lg] +`}) +export class GridRowDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdRow') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdRow.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdRow.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdRow.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdRow.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdRow.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdRow.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdRow.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdRow.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdRow.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdRow.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdRow.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdRow.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdRow.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + return {'grid-row': value}; + } +} diff --git a/src/lib/grid/rows/rows.spec.ts b/src/lib/grid/rows/rows.spec.ts new file mode 100644 index 000000000..7894754f9 --- /dev/null +++ b/src/lib/grid/rows/rows.spec.ts @@ -0,0 +1,193 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Platform} from '@angular/cdk/platform'; +import { + MatchMedia, + MockMatchMedia, + MockMatchMediaProvider, + SERVER_TOKEN, + StyleUtils, +} from '@angular/flex-layout/core'; + +import {customMatchers} from '../../utils/testing/custom-matchers'; +import {expectNativeEl, makeCreateTestComponent} from '../../utils/testing/helpers'; + +import {GridModule} from '../module'; + +describe('grid rows parent directive', () => { + let fixture: ComponentFixture; + let styler: StyleUtils; + let matchMedia: MockMatchMedia; + let platform: Platform; + let shouldRun = true; + let createTestComponent = (template: string, styles?: any) => { + shouldRun = true; + fixture = makeCreateTestComponent(() => TestGridRowsComponent)(template, styles); + inject([StyleUtils, MatchMedia, Platform], + (_styler: StyleUtils, _matchMedia: MockMatchMedia, _platform: Platform) => { + styler = _styler; + matchMedia = _matchMedia; + platform = _platform; + + // TODO(CaerusKaru): Grid tests won't work with Edge 14 + if (_platform.EDGE) { + shouldRun = false; + } + })(); + }; + + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [CommonModule, GridModule], + declarations: [TestGridRowsComponent], + providers: [ + MockMatchMediaProvider, + {provide: SERVER_TOKEN, useValue: true}, + ], + }); + }); + + describe('with static features', () => { + it('should add row styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '100px 1fr' + }, styler); + }); + + it('should add auto row styles for parent', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + // TODO(CaerusKaru): Firefox has an issue with auto tracks, + // caused by rachelandrew/gridbugs#1 + if (!platform.FIREFOX) { + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-auto-rows': '100px 1fr auto' + }, styler); + } + }); + + it('should work with inline grid', () => { + let template = ` +
+
+
+
+
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'inline-grid', + 'grid-template-rows': '100px 1fr' + }, styler); + }); + + it('should add dynamic rows styles', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '50px 1fr' + }, styler); + + fixture.componentInstance.cols = '100px 1fr'; + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '100px 1fr' + }, styler); + }); + }); + + describe('with responsive features', () => { + it('should add col styles for a parent', () => { + let template = ` +
+ `; + createTestComponent(template); + + if (!shouldRun) { + return; + } + + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '100px 1fr' + }, styler); + + matchMedia.activate('xs'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '50px 1fr' + }, styler); + + matchMedia.activate('md'); + expectNativeEl(fixture).toHaveStyle({ + 'display': 'grid', + 'grid-template-rows': '100px 1fr' + }, styler); + }); + }); + +}); + + +// ***************************************************************** +// Template Component +// ***************************************************************** +@Component({ + selector: 'test-layout', + template: `PlaceHolder Template HTML` +}) +class TestGridRowsComponent { + cols = '50px 1fr'; +} diff --git a/src/lib/grid/rows/rows.ts b/src/lib/grid/rows/rows.ts new file mode 100644 index 000000000..1f636175d --- /dev/null +++ b/src/lib/grid/rows/rows.ts @@ -0,0 +1,122 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + Directive, + ElementRef, + Input, + OnInit, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; + +const CACHE_KEY = 'rows'; +const DEFAULT_VALUE = 'none'; +const AUTO_SPECIFIER = '!'; + +/** + * 'grid-template-rows' CSS Grid styling directive + * Configures the sizing for the rows in the grid + * Syntax: [auto] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 + */ +@Directive({selector: ` + [gdRows], + [gdRows.xs], [gdRows.sm], [gdRows.md], [gdRows.lg], [gdRows.xl], + [gdRows.lt-sm], [gdRows.lt-md], [gdRows.lt-lg], [gdRows.lt-xl], + [gdRows.gt-xs], [gdRows.gt-sm], [gdRows.gt-md], [gdRows.gt-lg] +`}) +export class GridRowsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { + + /* tslint:disable */ + @Input('gdRows') set align(val) { this._cacheInput(`${CACHE_KEY}`, val); } + @Input('gdRows.xs') set alignXs(val) { this._cacheInput(`${CACHE_KEY}Xs`, val); } + @Input('gdRows.sm') set alignSm(val) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; + @Input('gdRows.md') set alignMd(val) { this._cacheInput(`${CACHE_KEY}Md`, val); }; + @Input('gdRows.lg') set alignLg(val) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; + @Input('gdRows.xl') set alignXl(val) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; + + @Input('gdRows.gt-xs') set alignGtXs(val) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; + @Input('gdRows.gt-sm') set alignGtSm(val) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; + @Input('gdRows.gt-md') set alignGtMd(val) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; + @Input('gdRows.gt-lg') set alignGtLg(val) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; + + @Input('gdRows.lt-sm') set alignLtSm(val) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; + @Input('gdRows.lt-md') set alignLtMd(val) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; + @Input('gdRows.lt-lg') set alignLtLg(val) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; + @Input('gdRows.lt-xl') set alignLtXl(val) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; + + @Input('gdInline') set inline(val) { this._cacheInput('inline', coerceBooleanProperty(val)); }; + + /* tslint:enable */ + constructor(monitor: MediaMonitor, + elRef: ElementRef, + styleUtils: StyleUtils) { + super(monitor, elRef, styleUtils); + } + + // ********************************************* + // Lifecycle Methods + // ********************************************* + + /** + * For @Input changes on the current mq activation property, see onMediaQueryChanges() + */ + ngOnChanges(changes: SimpleChanges) { + if (changes[CACHE_KEY] != null || this._mqActivation) { + this._updateWithValue(); + } + } + + /** + * After the initial onChanges, build an mqActivation object that bridges + * mql change events to onMediaQueryChange handlers + */ + ngOnInit() { + super.ngOnInit(); + + this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { + this._updateWithValue(changes.value); + }); + this._updateWithValue(); + } + + // ********************************************* + // Protected methods + // ********************************************* + + protected _updateWithValue(value?: string) { + value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; + if (this._mqActivation) { + value = this._mqActivation.activatedInput; + } + + this._applyStyleToElement(this._buildCSS(value)); + } + + + protected _buildCSS(value) { + let auto = false; + if (value.endsWith(AUTO_SPECIFIER)) { + value = value.substring(0, value.indexOf(AUTO_SPECIFIER)); + auto = true; + } + + let css = { + 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', + 'grid-auto-rows': '', + 'grid-template-rows': '', + }; + const key = (auto ? 'grid-auto-rows' : 'grid-template-rows'); + css[key] = value; + + return css; + } +} diff --git a/src/lib/grid/tsconfig-build.json b/src/lib/grid/tsconfig-build.json new file mode 100644 index 000000000..c235e7ee3 --- /dev/null +++ b/src/lib/grid/tsconfig-build.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig-build", + "files": [ + "public-api.ts", + "../typings.d.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": false, // Workaround for Angular #22210 + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/flex-layout/grid", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/lib/module.ts b/src/lib/module.ts index a75376dbf..9189ea5b7 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -10,6 +10,7 @@ import {isPlatformServer} from '@angular/common'; import {SERVER_TOKEN} from '@angular/flex-layout/core'; import {ExtendedModule} from '@angular/flex-layout/extended'; import {FlexModule} from '@angular/flex-layout/flex'; +import {GridModule} from '@angular/flex-layout/grid'; /** @@ -24,8 +25,8 @@ import {FlexModule} from '@angular/flex-layout/flex'; * */ @NgModule({ - imports: [FlexModule, ExtendedModule], - exports: [FlexModule, ExtendedModule] + imports: [FlexModule, ExtendedModule, GridModule], + exports: [FlexModule, ExtendedModule, GridModule] }) export class FlexLayoutModule { diff --git a/src/lib/package.json b/src/lib/package.json index 07acc4f79..93c5e6329 100644 --- a/src/lib/package.json +++ b/src/lib/package.json @@ -26,7 +26,7 @@ "@angular/cdk": ">=6.0.0-beta.0 <7.0.0", "@angular/core": "0.0.0-NG", "@angular/common": "0.0.0-NG", - "rxjs": "^6.0.0-beta.4" + "rxjs": "^6.0.0-rc.0" }, "dependencies": { "tslib": "^1.7.1" diff --git a/src/lib/public-api.ts b/src/lib/public-api.ts index 908fb5a5a..4328e143a 100644 --- a/src/lib/public-api.ts +++ b/src/lib/public-api.ts @@ -16,6 +16,7 @@ export * from './version'; export * from '@angular/flex-layout/core'; export * from '@angular/flex-layout/extended'; export * from '@angular/flex-layout/flex'; +export * from '@angular/flex-layout/grid'; // Flex-Layout Module export * from './module'; diff --git a/test/browser-providers.js b/test/browser-providers.js index ec8fe63bd..1d33f8d5b 100644 --- a/test/browser-providers.js +++ b/test/browser-providers.js @@ -13,7 +13,7 @@ const browserConfig = { 'FirefoxDev': { unitTest: {target: null, required: true }}, 'IE9': { unitTest: {target: null, required: false }}, 'IE10': { unitTest: {target: null, required: true }}, - 'IE11': { unitTest: {target: 'SL', required: true }}, + 'IE11': { unitTest: {target: null, required: false }}, 'Edge': { unitTest: {target: 'SL', required: true }}, 'Android4.1': { unitTest: {target: null, required: false }}, 'Android4.2': { unitTest: {target: null, required: false }}, @@ -22,11 +22,12 @@ const browserConfig = { 'Android5': { unitTest: {target: null, required: false }}, 'Safari7': { unitTest: {target: null, required: false }}, 'Safari8': { unitTest: {target: null, required: false }}, - 'Safari9': { unitTest: {target: 'SL', required: true }}, + 'Safari9': { unitTest: {target: null, required: false }}, 'Safari10': { unitTest: {target: 'BS', required: true }}, 'iOS7': { unitTest: {target: null, required: false }}, 'iOS8': { unitTest: {target: null, required: false }}, - 'iOS9': { unitTest: {target: 'BS', required: true }}, + 'iOS9': { unitTest: {target: null, required: false }}, + 'iOS10': { unitTest: {target: 'BS', required: true }}, 'WindowsPhone': { unitTest: {target: null, required: false }} }; diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index 518e5f06f..ce80b2fcd 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -63,6 +63,7 @@ System.config({ '@angular/flex-layout/core': 'dist/packages/flex-layout/core/index.js', '@angular/flex-layout/extended': 'dist/packages/flex-layout/extended/index.js', '@angular/flex-layout/flex': 'dist/packages/flex-layout/flex/index.js', + '@angular/flex-layout/grid': 'dist/packages/flex-layout/grid/index.js', '@angular/flex-layout/server': 'dist/packages/flex-layout/server/index.js', }, packages: { diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index 44d8f7ee6..ffc668105 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -36,6 +36,7 @@ export const rollupGlobals = { '@angular/common/http/testing': 'ng.common.http.testing', '@angular/material/button': 'ng.material.button', '@angular/cdk/bidi': 'ng.cdk.bidi', + '@angular/cdk/coercion': 'ng.cdk.coercion', '@angular/cdk/platform': 'ng.cdk.platform', // Some packages are not really needed for the UMD bundles, but for the missingRollupGlobals rule.