From 2ebb3351a4cd320412893c115694a89893970e4a Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Thu, 18 May 2017 18:59:25 -0500 Subject: [PATCH] fix(api): support query children on API directives * support DI of flexbox/api Directive classes * implement Directive method `activatedValue` getter/setter * to imperatively read current activated input value * to write API values with immediate style updates * implement demo using splitters (ngxSplit) with fxLayout and fxFlex layouts --- src/demo-app/app/demo-app/demo-app.css | 28 ++++ .../app/docs-layout-responsive/_module.ts | 6 +- src/demo-app/app/docs-layout/_module.ts | 10 +- src/demo-app/app/github-issues/_module.ts | 9 +- .../app/github-issues/issue.181.demo.ts | 5 +- .../app/github-issues/issue.266.demo.css | 86 +++++++++++ .../app/github-issues/issue.266.demo.ts | 63 ++++++++ .../splitter/split-area.directive.ts | 12 ++ .../splitter/split-handle.directive.ts | 36 +++++ .../splitter/split.component.scss | 39 +++++ .../github-issues/splitter/split.directive.ts | 79 ++++++++++ .../github-issues/splitter/split.module.ts | 14 ++ src/demo-app/app/shared/_module.ts | 2 +- src/demo-app/app/stack-overflow/_module.ts | 7 +- src/lib/flexbox/api/base.ts | 76 +++++++--- src/lib/flexbox/api/flex.spec.ts | 141 +++++++++++++++--- src/lib/flexbox/api/layout-gap.spec.ts | 4 +- src/lib/flexbox/index.ts | 16 ++ src/lib/media-query/observable-media.ts | 8 +- src/lib/utils/style-transforms.spec.ts | 12 +- src/lib/utils/style-transforms.ts | 4 +- 21 files changed, 579 insertions(+), 78 deletions(-) create mode 100644 src/demo-app/app/github-issues/issue.266.demo.css create mode 100644 src/demo-app/app/github-issues/issue.266.demo.ts create mode 100644 src/demo-app/app/github-issues/splitter/split-area.directive.ts create mode 100644 src/demo-app/app/github-issues/splitter/split-handle.directive.ts create mode 100644 src/demo-app/app/github-issues/splitter/split.component.scss create mode 100644 src/demo-app/app/github-issues/splitter/split.directive.ts create mode 100644 src/demo-app/app/github-issues/splitter/split.module.ts diff --git a/src/demo-app/app/demo-app/demo-app.css b/src/demo-app/app/demo-app/demo-app.css index eb665937c..ca1d3923d 100644 --- a/src/demo-app/app/demo-app/demo-app.css +++ b/src/demo-app/app/demo-app/demo-app.css @@ -226,3 +226,31 @@ md-card-content pre { } .fixed { height:275px; } + +.ngx-split.row-split > .ngx-split-handle .ngx-split-button { + top: 50%; + left: 50%; + cursor: col-resize; + transform: translate(-50%, -50%); +} +.ngx-split.column-split > .ngx-split-handle .ngx-split-button { + left: 50%; + cursor: row-resize; + top: -3px; + transform: translateX(-50%) rotate(270deg); +} +.ngx-split .ngx-split-area { + overflow: auto; +} +.ngx-split .ngx-split-handle { + position: relative; +} +.ngx-split .ngx-split-handle .ngx-split-button { + line-height: 0; + font-size: 32px; + position: absolute; + display: block; + padding: 0; +} + + diff --git a/src/demo-app/app/docs-layout-responsive/_module.ts b/src/demo-app/app/docs-layout-responsive/_module.ts index c90c27dd5..67ad781e6 100644 --- a/src/demo-app/app/docs-layout-responsive/_module.ts +++ b/src/demo-app/app/docs-layout-responsive/_module.ts @@ -13,8 +13,9 @@ import { Component } from '@angular/core'; }) export class DemosResponsiveLayout { } -import {NgModule} from '@angular/core'; -import {FormsModule} from "@angular/forms"; +import {NgModule} from '@angular/core'; +import {FormsModule} from "@angular/forms"; +import {SharedModule} from '../shared/_module'; import {DemoResponsiveRows} from "./responsiveRowColumns.demo"; import {DemoResponsiveLayoutDirection } from "./responsiveLayoutDirections.demo"; @@ -22,7 +23,6 @@ import {DemoResponsiveShowHide} from "./responsiveShowHide.demo"; import {DemoResponsiveFlexDirectives} from "./responsiveFlexDirective.demo"; import {DemoResponsiveFlexOrder} from "./responsiveFlexOrder.demo"; import {DemoResponsiveStyle} from "./responsiveStyle.demo"; -import {SharedModule} from '../shared/_module'; @NgModule({ declarations : [ diff --git a/src/demo-app/app/docs-layout/_module.ts b/src/demo-app/app/docs-layout/_module.ts index b4b3004e3..e50802788 100644 --- a/src/demo-app/app/docs-layout/_module.ts +++ b/src/demo-app/app/docs-layout/_module.ts @@ -16,10 +16,8 @@ export class DemosLayoutAPI { } import {NgModule} from '@angular/core'; -import {CommonModule} from "@angular/common"; import {FormsModule} from "@angular/forms"; -import {MaterialModule} from "@angular/material"; -import {FlexLayoutModule} from "../../../lib"; // `gulp build:components` to deploy to node_modules manually +import {SharedModule} from '../shared/_module'; import {DemoLayoutAlignment} from "./layoutAlignment.demo"; import {DemoFlexRowFill} from "./flexRowFill.demo"; @@ -42,10 +40,8 @@ import {DemoFlexAlignSelf} from "./FlexAlignSelf.demo"; DemoFlexAlignSelf ], imports: [ - CommonModule, - FormsModule, - MaterialModule, - FlexLayoutModule + SharedModule, + FormsModule ] }) export class DemosLayoutAPIModule { diff --git a/src/demo-app/app/github-issues/_module.ts b/src/demo-app/app/github-issues/_module.ts index ff557d4c4..b1473f50d 100644 --- a/src/demo-app/app/github-issues/_module.ts +++ b/src/demo-app/app/github-issues/_module.ts @@ -8,20 +8,24 @@ import {Component} from '@angular/core'; + ` }) export class DemosGithubIssues { } import {NgModule} from '@angular/core'; +import {SplitModule} from './splitter/split.module'; import {DemoIssue5345} from "./issue.5345.demo"; import {DemoIssue9897} from "./issue.9897.demo"; import {DemoIssue135} from "./issue.135.demo"; import {DemoIssue181} from './issue.181.demo'; import {DemoIssue197} from './issue.197.demo'; +import {DemoIssue266} from './issue.266.demo'; import {SharedModule} from '../shared/_module'; + @NgModule({ declarations: [ DemosGithubIssues, // used by the Router with the root app component @@ -29,10 +33,11 @@ import {SharedModule} from '../shared/_module'; DemoIssue9897, DemoIssue135, DemoIssue181, - DemoIssue197 + DemoIssue197, + DemoIssue266 ], imports: [ - SharedModule + SharedModule, SplitModule ] }) export class DemosGithubIssuesModule { diff --git a/src/demo-app/app/github-issues/issue.181.demo.ts b/src/demo-app/app/github-issues/issue.181.demo.ts index 8dcea8d02..3caa7e883 100644 --- a/src/demo-app/app/github-issues/issue.181.demo.ts +++ b/src/demo-app/app/github-issues/issue.181.demo.ts @@ -1,9 +1,6 @@ -import {Component, OnDestroy} from '@angular/core'; -import {Subscription} from "rxjs/Subscription"; +import {Component} from '@angular/core'; import 'rxjs/add/operator/filter'; -import {MediaChange} from "../../../lib/media-query/media-change"; -import {ObservableMedia} from "../../../lib/media-query/observable-media"; @Component({ selector: 'demo-issue-181', diff --git a/src/demo-app/app/github-issues/issue.266.demo.css b/src/demo-app/app/github-issues/issue.266.demo.css new file mode 100644 index 000000000..f695fa70d --- /dev/null +++ b/src/demo-app/app/github-issues/issue.266.demo.css @@ -0,0 +1,86 @@ +.night-theme { + background: #1c2029; + color: #cfcfcf; +} + +.handle { + outline: none; + -webkit-user-select: none; + user-select: none; + z-index: 9999; + height: 5px; + display: block; + padding: 0; + margin: 0; + position: relative; + line-height: 0; +} + +.handle-row { + width: 15px; + top: 50%; + left: -2px; + transform: translateX(-50%) rotate(270deg); + cursor: col-resize;; +} + +.handle-column { + height: 15px; + left: 50%; + top: -4px; + cursor: row-resize; +} + +.c2r1_header, .c2r2_header, .c1r1_header { + background: #13141b; + padding: 10px; + height: 50px; + margin: -8px -8px -9px -9px; + font-size: 1.2em; + font-weight: bold; + color: #ffdb86; +} + +.c1r1 { + background: #3949ab; + padding: 10px; +} + +.c1r1_header { + background: #2c3c7a; +} + + +.c2r2_header { + margin-top: -9px; +} + +.c1r1_header { + margin: -7px -9px -8px -9px; +} + +.c2r1_body { + background: #009688; + padding: 10px; +} + +.c2r1_header { + background: #00695d; +} + + +.c2r2 { + background: #9c27b0; + padding: 10px; +} + +.c2r2_header { + background: #5f1b6d; +} + +.c1r1 > *, .c2r1_header, .c2r1_body > *, .c2r2 > * { + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/src/demo-app/app/github-issues/issue.266.demo.ts b/src/demo-app/app/github-issues/issue.266.demo.ts new file mode 100644 index 000000000..e1c324121 --- /dev/null +++ b/src/demo-app/app/github-issues/issue.266.demo.ts @@ -0,0 +1,63 @@ +import {Component} from '@angular/core'; +import 'rxjs/add/operator/filter'; + +@Component({ + selector: 'demo-issue-266', + styleUrls: [ './issue.266.demo.css' ], + template: ` + + + Issue #266 + + Using ngxSplit with Flex-Layout: + +
+
+
+
Column #1 - Row #1
+
    +
  • 2 Columns: 30% + 70%
  • +
  • 2nd Column: 2 rows
  • +
  • 2nd Column: 50% + 50%
  • +
+
+
+ +
+
+
+
+
Column #2 - Row #1
+

Layout Dashboard

+

+ Demonstrate use of ngxSplit with the Flex-Layout API + and flexbox css layouts. +

+ Haxx0r ipsum cd ctl-c Starcraft concurrently salt unix baz class bar linux + January 1, 1970 syn for mutex daemon todo mountain dew recursively. Mainframe + wannabee machine code hack the mainframe do void python bin big-endian break + tcp ddos emacs public frack.Over clock headers data private *.* pwned + fork script kiddies. +

+
+
+ +
+
+
Column #2 - Row #2
+
    +
  • List Item #1
  • +
  • List Item #2
  • +
  • List Item #3
  • +
+
+
+
+
+
+
+
+ ` +}) +export class DemoIssue266 { +} diff --git a/src/demo-app/app/github-issues/splitter/split-area.directive.ts b/src/demo-app/app/github-issues/splitter/split-area.directive.ts new file mode 100644 index 000000000..69219b0c8 --- /dev/null +++ b/src/demo-app/app/github-issues/splitter/split-area.directive.ts @@ -0,0 +1,12 @@ +import { Directive, Optional, Self } from '@angular/core'; +import { FlexDirective } from "../../../../lib"; + +@Directive({ + selector: '[ngxSplitArea]', + host: { + style: 'overflow: auto;' + } +}) +export class SplitAreaDirective { + constructor(@Optional() @Self() public flex: FlexDirective) { } +} diff --git a/src/demo-app/app/github-issues/splitter/split-handle.directive.ts b/src/demo-app/app/github-issues/splitter/split-handle.directive.ts new file mode 100644 index 000000000..2242159f1 --- /dev/null +++ b/src/demo-app/app/github-issues/splitter/split-handle.directive.ts @@ -0,0 +1,36 @@ +import { Directive, ElementRef, Output } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/takeUntil'; +import 'rxjs/add/operator/switchMap'; + +@Directive({ + selector: '[ngxSplitHandle]', + host: { + class: 'ngx-split-handle', + title : 'Drag to resize' + } +}) +export class SplitHandleDirective { + + @Output() drag: Observable<{ x: number, y: number }>; + + constructor(ref: ElementRef) { + const getMouseEventPosition = (event: MouseEvent) => ({ x: event.movementX, y: event.movementY }); + + const mousedown$ = Observable.fromEvent(ref.nativeElement, 'mousedown').map(getMouseEventPosition); + const mousemove$ = Observable.fromEvent(document, 'mousemove').map(getMouseEventPosition); + const mouseup$ = Observable.fromEvent(document, 'mouseup'); + + this.drag = mousedown$ + .switchMap(mousedown => + mousemove$.map(mousemove => ({ + x: mousemove.x, + y: mousemove.y + })) + .takeUntil(mouseup$) + ); + } + +} diff --git a/src/demo-app/app/github-issues/splitter/split.component.scss b/src/demo-app/app/github-issues/splitter/split.component.scss new file mode 100644 index 000000000..7d54e5830 --- /dev/null +++ b/src/demo-app/app/github-issues/splitter/split.component.scss @@ -0,0 +1,39 @@ +.ngx-split { + &.row-split { + > .ngx-split-handle .ngx-split-button { + top: 50%; + left: 50%; + cursor: col-resize; + transform: translate(-50%, -50%); + } + } + + &.column-split { + > .ngx-split-handle .ngx-split-button { + left: 50%; + cursor: row-resize; + top: -3px; + transform: translateX(-50%) rotate(270deg); + } + } + + .ngx-split-area { + overflow: auto; + } + + .ngx-split-handle { + position: relative; + + .ngx-split-button { + line-height: 0; + font-size: 32px; + position: absolute; + display: block; + padding: 0; + } + } +} + +.icon-split-handle:before { + content: "\f1aa"; +} diff --git a/src/demo-app/app/github-issues/splitter/split.directive.ts b/src/demo-app/app/github-issues/splitter/split.directive.ts new file mode 100644 index 000000000..eefdacc20 --- /dev/null +++ b/src/demo-app/app/github-issues/splitter/split.directive.ts @@ -0,0 +1,79 @@ +import { + Directive, Input, ContentChild, + ContentChildren, AfterContentInit, QueryList, ElementRef, OnDestroy +} from '@angular/core'; + +import {SplitAreaDirective} from './split-area.directive'; +import {SplitHandleDirective} from './split-handle.directive'; +import {FlexDirective} from "../../../../lib"; +import {Subscription} from 'rxjs/Subscription'; + +@Directive({ + selector: '[ngxSplit]', + host: { + class: 'ngx-split' + } +}) +export class SplitDirective implements AfterContentInit, OnDestroy { + watcher : Subscription; + + @Input('ngxSplit') + direction: string = 'row'; + + @ContentChild(SplitHandleDirective) handle: SplitHandleDirective; + @ContentChildren(SplitAreaDirective) areas: QueryList; + + constructor(private elementRef: ElementRef) { } + + ngAfterContentInit(): void { + this.watcher = this.handle.drag.subscribe(pos => this.onDrag(pos)); + } + + ngOnDestroy() { + this.watcher.unsubscribe(); + } + + /** + * While dragging, continually update the `flex.activatedValue` for each area + * managed by the splitter. + */ + onDrag({x, y}): void { + const dragAmount = (this.direction === 'row') ? x : y; + + this.areas.forEach((area, i) => { + // get the cur flex and the % in px + const flex = (area.flex as FlexDirective); + const delta = (i === 0) ? dragAmount : -dragAmount; + const currentValue = flex.activatedValue; + + // Update Flex-Layout value to build/inject new flexbox CSS + flex.activatedValue = this.calculateSize(currentValue, delta); + }); + } + + /** + * Use the pixel delta change to recalculate the area size (%) + * Note: flex value may be "", %, px, or " " + */ + calculateSize(value, delta) { + const containerSizePx = this.elementRef.nativeElement.clientWidth; + const elementSizePx = Math.round(this.valueToPixel(value, containerSizePx)); + + const elementSize = ((elementSizePx + delta) / containerSizePx) * 100; + return Math.round(elementSize * 100) / 100; + } + + /** + * Convert the pixel or percentage value to a raw + * pixel float value. + */ + valueToPixel(value:string|number, parentWidth:number):number { + let isPercent = () => String(value).indexOf('px') < 0; + let size = parseFloat(String(value)); + if ( isPercent() ) { + size = parentWidth * (size/100); // Convert percentage to actual pixel float value + } + return size; + } + +} diff --git a/src/demo-app/app/github-issues/splitter/split.module.ts b/src/demo-app/app/github-issues/splitter/split.module.ts new file mode 100644 index 000000000..580980378 --- /dev/null +++ b/src/demo-app/app/github-issues/splitter/split.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FlexLayoutModule } from "../../../../lib"; + +import { SplitDirective } from './split.directive'; +import { SplitAreaDirective } from './split-area.directive'; +import { SplitHandleDirective } from './split-handle.directive'; + +@NgModule({ + imports: [CommonModule, FlexLayoutModule], + declarations: [SplitDirective, SplitAreaDirective, SplitHandleDirective], + exports: [SplitDirective, SplitAreaDirective, SplitHandleDirective] +}) +export class SplitModule { } diff --git a/src/demo-app/app/shared/_module.ts b/src/demo-app/app/shared/_module.ts index f1e16eb0e..6f205fe03 100644 --- a/src/demo-app/app/shared/_module.ts +++ b/src/demo-app/app/shared/_module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import {MaterialModule} from '@angular/material'; -import {FlexLayoutModule} from "../../../lib"; // `gulp build:components` to deploy to node_modules manually +import {FlexLayoutModule} from "../../../lib"; import {MediaQueryStatus} from './media-query-status'; diff --git a/src/demo-app/app/stack-overflow/_module.ts b/src/demo-app/app/stack-overflow/_module.ts index 579a792fc..a77c18352 100644 --- a/src/demo-app/app/stack-overflow/_module.ts +++ b/src/demo-app/app/stack-overflow/_module.ts @@ -20,6 +20,7 @@ import { DemoComplexColumnOrder } from "./columnOrder.demo"; import {DemoGridAreaRowSpan} from './gridArea.demo'; import {DemoGridColumnSpan} from './columnSpan.demo'; import {DemoMozHolyGrail} from "./mozHolyGrail.demo"; +import {SharedModule} from '../shared/_module'; @NgModule({ declarations : [ @@ -29,10 +30,6 @@ import {DemoMozHolyGrail} from "./mozHolyGrail.demo"; DemoGridAreaRowSpan, DemoMozHolyGrail ], - imports : [ - CommonModule, - MaterialModule, - FlexLayoutModule - ] + imports : [ SharedModule ] }) export class DemosStackOverflowModule{ } diff --git a/src/lib/flexbox/api/base.ts b/src/lib/flexbox/api/base.ts index af6596b37..e7c1dc46c 100644 --- a/src/lib/flexbox/api/base.ts +++ b/src/lib/flexbox/api/base.ts @@ -5,7 +5,10 @@ * 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, Renderer2, OnDestroy} from '@angular/core'; +import { + ElementRef, Renderer2, OnDestroy, SimpleChanges, OnChanges, + SimpleChange +} from '@angular/core'; import {applyCssPrefixes} from '../../utils/auto-prefixer'; import {buildLayoutCSS} from '../../utils/layout-validator'; @@ -18,17 +21,46 @@ import {MediaQuerySubscriber} from '../../media-query/media-change'; * Definition of a css style. Either a property name (e.g. "flex-basis") or an object * map of property name and value (e.g. {display: 'none', flex-order: 5}). */ -export type StyleDefinition = string|{[property: string]: string|number}; +export type StyleDefinition = string | { [property: string]: string | number }; /** Abstract base class for the Layout API styling directives. */ -export abstract class BaseFxDirective implements OnDestroy { +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, @@ -53,6 +85,10 @@ export abstract class BaseFxDirective implements OnDestroy { // Lifecycle Methods // ********************************************* + ngOnChanges(change: SimpleChanges) { + throw new Error('BaseFxDirective::ngOnChanges should be overridden in subclass'); + } + ngOnDestroy() { if (this._mqActivation) { this._mqActivation.destroy(); @@ -68,7 +104,7 @@ export abstract class BaseFxDirective implements OnDestroy { * Was the directive's default selector used ? * If not, use the fallback value! */ - protected _getDefaultVal(key: string, fallbackVal: any): string|boolean { + protected _getDefaultVal(key: string, fallbackVal: any): string | boolean { let val = this._queryInput(key); let hasDefaultVal = (val !== undefined && val !== null); return (hasDefaultVal && val !== '') ? val : fallbackVal; @@ -86,22 +122,22 @@ export abstract class BaseFxDirective implements OnDestroy { } protected _getFlowDirection(target: any, addIfMissing = false): string { - let value = ""; - if ( target ) { - let directionKeys = Object.keys(applyCssPrefixes({'flex-direction': ''})); - let findDirection = (styles) => directionKeys.reduce((direction, key) => { - return direction || styles[key]; - }, null); - - let immediateValue = findDirection(target['style']); - value = immediateValue || findDirection(getComputedStyle(target as Element)); - if ( !immediateValue && addIfMissing ) { - value = value || 'row'; - this._applyStyleToElements(buildLayoutCSS(value), [target]); - } + let value = ''; + if (target) { + let directionKeys = Object.keys(applyCssPrefixes({'flex-direction': ''})); + let findDirection = (styles) => directionKeys.reduce((direction, key) => { + return direction || styles[key]; + }, null); + + let immediateValue = findDirection(target.style); + value = immediateValue || findDirection(getComputedStyle(target as Element)); + if (!immediateValue && addIfMissing) { + value = value || 'row'; + this._applyStyleToElements(buildLayoutCSS(value), [target]); } + } - return value ? value.trim() : "row"; + return value ? value.trim() : 'row'; } /** @@ -121,7 +157,7 @@ export abstract class BaseFxDirective implements OnDestroy { * Applies styles given via string pair or object map to the directive element. */ protected _applyStyleToElement(style: StyleDefinition, - value?: string|number, + value?: string | number, nativeElement?: any) { let styles = {}; let element = nativeElement || this._elementRef.nativeElement; @@ -169,7 +205,7 @@ export abstract class BaseFxDirective implements OnDestroy { protected _listenForMediaQueryChanges(key: string, defaultValue: any, onMediaQueryChange: MediaQuerySubscriber): ResponsiveActivation { // tslint:disable-line:max-line-length - if ( !this._mqActivation ) { + if (!this._mqActivation) { let keyOptions = new KeyOptions(key, defaultValue, this._inputMap); this._mqActivation = new ResponsiveActivation( keyOptions, diff --git a/src/lib/flexbox/api/flex.spec.ts b/src/lib/flexbox/api/flex.spec.ts index 67d949c5f..e0a6e822a 100644 --- a/src/lib/flexbox/api/flex.spec.ts +++ b/src/lib/flexbox/api/flex.spec.ts @@ -5,7 +5,7 @@ * 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 {Component, ViewChild} from '@angular/core'; import {CommonModule} from '@angular/common'; import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing'; @@ -14,6 +14,8 @@ import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-regi import {MockMatchMedia} from '../../media-query/mock/mock-match-media'; import {MatchMedia} from '../../media-query/match-media'; import {FlexLayoutModule} from '../_module'; +import {FlexDirective} from '../../flexbox/api/flex'; +import {LayoutDirective} from '../../flexbox/api/layout'; import {customMatchers, expect} from '../../utils/testing/custom-matchers'; import {_dom as _} from '../../utils/testing/dom-tools'; @@ -47,7 +49,7 @@ describe('flex directive', () => { // Configure testbed to prepare services TestBed.configureTestingModule({ imports: [CommonModule, FlexLayoutModule], - declarations: [TestFlexComponent], + declarations: [TestFlexComponent, TestQueryWithFlexComponent], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, {provide: MatchMedia, useClass: MockMatchMedia} @@ -107,8 +109,8 @@ describe('flex directive', () => { `); fixture.detectChanges(); - let parent = queryFor(fixture, ".test")[0].nativeElement; - let element = queryFor(fixture, "[fxFlex]")[0].nativeElement; + let parent = queryFor(fixture, '.test')[0].nativeElement; + let element = queryFor(fixture, '[fxFlex]')[0].nativeElement; // parent flex-direction found with 'column' with child height styles expect(parent).toHaveCssStyle({'flex-direction': 'column', 'display': 'flex'}); @@ -118,23 +120,23 @@ describe('flex directive', () => { it('should not work with non-direct-parent fxLayouts', async(() => { componentWithTemplate(` -
+
`); fixture.detectChanges(); - let element = queryFor(fixture, "[fxFlex]")[0].nativeElement; - let parent = queryFor(fixture, ".test")[0].nativeElement; + let element = queryFor(fixture, '[fxFlex]')[0].nativeElement; + let parent = queryFor(fixture, '.test')[0].nativeElement; setTimeout(() => { // The parent flex-direction not found; // A flex-direction should have been auto-injected to the parent... // fallback to 'row' and set child width styles accordingly - expect(parent).toHaveCssStyle({ 'flex-direction': 'row' }); - expect(element).toHaveCssStyle({ 'min-width': '40px' }); - expect(element).not.toHaveCssStyle({ 'min-height': '40px' }); + expect(parent).toHaveCssStyle({'flex-direction': 'row'}); + expect(element).toHaveCssStyle({'min-width': '40px'}); + expect(element).not.toHaveCssStyle({'min-height': '40px'}); }); })); @@ -148,12 +150,12 @@ describe('flex directive', () => {
`); fixture.detectChanges(); - let element = queryFor(fixture, "[fxFlex]")[0].nativeElement; - let parent = queryFor(fixture, ".parent")[0].nativeElement; + let element = queryFor(fixture, '[fxFlex]')[0].nativeElement; + let parent = queryFor(fixture, '.parent')[0].nativeElement; // parent flex-direction found with 'column'; set child with height styles - expect(element).toHaveCssStyle({ 'min-height': '60px' }); - expect(parent).toHaveCssStyle({ 'flex-direction': 'column' }); + expect(element).toHaveCssStyle({'min-height': '60px'}); + expect(parent).toHaveCssStyle({'flex-direction': 'column'}); }); it('should work with "1 1 auto" values', () => { @@ -166,7 +168,7 @@ describe('flex directive', () => { `); fixture.detectChanges(); - let nodes = queryFor(fixture, "[fxFlex]"); + let nodes = queryFor(fixture, '[fxFlex]'); expect(nodes.length).toEqual(3); expect(nodes[1].nativeElement).not.toHaveCssStyle({'max-height': '*', 'min-height': '*'}); @@ -472,6 +474,95 @@ describe('flex directive', () => { }); }); + + describe('with API directive queries', () => { + it('should query the ViewChild `fxLayout` directive properly', () => { + fixture = TestBed.createComponent(TestQueryWithFlexComponent); + fixture.detectChanges(); + + const layout: LayoutDirective = fixture.debugElement.componentInstance.layout; + + expect(layout).toBeDefined(); + expect(layout.activatedValue).toBe(''); + expectNativeEl(fixture).toHaveCssStyle({ + 'flex-direction': 'row' + }); + + layout.activatedValue = 'column'; + expect(layout.activatedValue).toBe('column'); + expectNativeEl(fixture).toHaveCssStyle({ + 'flex-direction': 'column' + }); + }); + + it('should query the ViewChild `fxFlex` directive properly', () => { + fixture = TestBed.createComponent(TestQueryWithFlexComponent); + fixture.detectChanges(); + + const flex: FlexDirective = fixture.debugElement.componentInstance.flex; + + // Test for percentage value assignments + expect(flex).toBeDefined(); + expect(flex.activatedValue).toBe('50%'); + + let nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes.length).toEqual(1); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '50%'}); + + // Test for raw value assignments that are converted to percentages + flex.activatedValue = '35'; + expect(flex.activatedValue).toBe('35'); + + nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes.length).toEqual(1); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '35%'}); + + // Test for pixel value assignments + flex.activatedValue = '27.5px'; + expect(flex.activatedValue).toBe('27.5px'); + + nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes.length).toEqual(1); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '27.5px'}); + + }); + + it('should restore `fxFlex` value after breakpoint activations', inject([MatchMedia], + (_matchMedia: MockMatchMedia) => { + fixture = TestBed.createComponent(TestQueryWithFlexComponent); + fixture.detectChanges(); + + const flex: FlexDirective = fixture.debugElement.componentInstance.flex; + + // Test for raw value assignments that are converted to percentages + expect(flex).toBeDefined(); + flex.activatedValue = '35'; + expect(flex.activatedValue).toBe('35'); + + let nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes.length).toEqual(1); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '35%'}); + + _matchMedia.activate('sm'); + fixture.detectChanges(); + + // Test for breakpoint value changes + expect(flex.activatedValue).toBe('71%'); + nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '71%'}); + + _matchMedia.activate('lg'); + fixture.detectChanges(); + + // Confirm activatedValue was restored properly when `sm` deactivated + expect(flex.activatedValue).toBe('35'); + nodes = queryFor(fixture, "[fxFlex]"); + expect(nodes[0].nativeElement).toHaveCssStyle({'max-width': '35%'}); + + }) + ); + }); + }); @@ -483,14 +574,20 @@ describe('flex directive', () => { selector: 'test-layout', template: `PlaceHolder Template HTML` }) -export class TestFlexComponent implements OnInit { +export class TestFlexComponent { public direction = "column"; - - constructor() { - } - - ngOnInit() { - } } +@Component({ + selector: 'test-query-with-flex', + template: ` +
+
+
+ ` +}) +export class TestQueryWithFlexComponent { + @ViewChild(FlexDirective) flex: FlexDirective; + @ViewChild(LayoutDirective) layout: LayoutDirective; +} diff --git a/src/lib/flexbox/api/layout-gap.spec.ts b/src/lib/flexbox/api/layout-gap.spec.ts index 58fa56311..2a5b653f7 100644 --- a/src/lib/flexbox/api/layout-gap.spec.ts +++ b/src/lib/flexbox/api/layout-gap.spec.ts @@ -215,8 +215,8 @@ describe('layout-gap directive', () => { it('should adjust gaps based on layout-wrap presence', () => { let styles = ['.col1 { display:none !important;']; let template = ` -
Div 1
diff --git a/src/lib/flexbox/index.ts b/src/lib/flexbox/index.ts index 103ef8353..1319cf3dc 100644 --- a/src/lib/flexbox/index.ts +++ b/src/lib/flexbox/index.ts @@ -7,5 +7,21 @@ */ export * from './api/base'; export * from './api/base-adapter'; + +export * from './api/flex'; +export * from './api/flex-align'; +export * from './api/flex-fill'; +export * from './api/flex-offset'; +export * from './api/flex-order'; + +export * from './api/layout'; +export * from './api/layout-align'; +export * from './api/layout-gap'; +export * from './api/layout-wrap'; + +export * from './api/class'; +export * from './api/show-hide'; +export * from './api/style'; + export * from './responsive/responsive-activation'; export * from './_module'; diff --git a/src/lib/media-query/observable-media.ts b/src/lib/media-query/observable-media.ts index 41f570dce..308393df4 100644 --- a/src/lib/media-query/observable-media.ts +++ b/src/lib/media-query/observable-media.ts @@ -93,7 +93,7 @@ export class MediaService implements ObservableMedia { isActive(alias): boolean { let query = this._toMediaQuery(alias); return this.mediaWatcher.isActive(query); - }; + } /** * Proxy to the Observable subscribe method @@ -102,7 +102,7 @@ export class MediaService implements ObservableMedia { error?: (error: any) => void, complete?: () => void): Subscription { return this.observable$.subscribe(next, error, complete); - }; + } /** * Access to observable for use with operators like @@ -167,7 +167,7 @@ export class MediaService implements ObservableMedia { */ private _findByQuery(query) { return this.breakpoints.findByQuery(query); - }; + } /** * Find associated breakpoint (if any) @@ -175,7 +175,7 @@ export class MediaService implements ObservableMedia { private _toMediaQuery(query) { let bp: BreakPoint = this._findByAlias(query) || this._findByQuery(query); return bp ? bp.mediaQuery : query; - }; + } private observable$: Observable; } diff --git a/src/lib/utils/style-transforms.spec.ts b/src/lib/utils/style-transforms.spec.ts index 74acb9524..3ecb8b9d0 100644 --- a/src/lib/utils/style-transforms.spec.ts +++ b/src/lib/utils/style-transforms.spec.ts @@ -15,8 +15,8 @@ describe('ngStyleUtils', () => { it('should parse a raw string of key:value pairs', () => { let list: NgStyleRawList = _.buildRawList(` - color:'red'; - font-size :16px; + color:'red'; + font-size :16px; background-color:rgba(116, 37, 49, 0.72); `); @@ -27,8 +27,8 @@ describe('ngStyleUtils', () => { it('should build an iterable map from a raw string of key:value pairs', () => { let map: NgStyleMap = _.buildMapFromList(_.buildRawList(` - color:'red'; - font-size :16px; + color:'red'; + font-size :16px; background-color:rgba(116, 37, 49, 0.72); `)); @@ -41,8 +41,8 @@ describe('ngStyleUtils', () => { it('should build an iterable map from an Array of key:value strings', () => { let map: NgStyleMap = _.buildMapFromList(_.buildRawList(` - color:'red'; - font-size :16px; + color:'red'; + font-size :16px; background-color:rgba(116, 37, 49, 0.72); `)); diff --git a/src/lib/utils/style-transforms.ts b/src/lib/utils/style-transforms.ts index 67cdc273a..3f2187b03 100644 --- a/src/lib/utils/style-transforms.ts +++ b/src/lib/utils/style-transforms.ts @@ -75,7 +75,7 @@ function buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): .filter(entry => !!entry) .map(sanitizeValue) .reduce(keyValuesToMap, {}); -}; +} /** * Convert Set or raw Object to an iterable NgStyleMap @@ -99,7 +99,7 @@ function buildMapFromSet(source: any, sanitize?: NgStyleSanitizer): NgStyleMap { function stringToKeyValue(it: string): NgStyleKeyValue { let [key, val] = it.split(":"); return val ? new NgStyleKeyValue(key, val) : null; -}; +} /** * Convert [ [key,value] ] -> { key : value }