From 8967593512259dbef4f2ebabb7b5dcfdbde37178 Mon Sep 17 00:00:00 2001 From: Ben Grynhaus Date: Thu, 28 Jun 2018 19:36:31 +0300 Subject: [PATCH] [Panel] WIP --- .vscode/settings.json | 2 + apps/demo/src/app/app.component.html | 35 ++++- apps/demo/src/app/app.component.ts | 38 +++++ apps/demo/src/app/app.module.ts | 27 +++- apps/demo/src/app/wrapper.component.ts | 8 + libs/core/package.json | 4 + libs/core/public-api.ts | 3 +- libs/core/src/components/wrapper-component.ts | 4 +- libs/core/src/renderer/react-content.ts | 2 +- libs/core/src/renderer/react-node.ts | 31 +++- libs/core/src/renderer/renderer.ts | 33 ++-- .../src/{types.d.ts => types/StringMap.d.ts} | 0 libs/fabric/public-api.ts | 6 +- libs/fabric/src/button/button.component.ts | 28 ++-- libs/fabric/src/dialog/dialog.component.ts | 32 ++-- libs/fabric/src/image/image.component.ts | 2 +- libs/fabric/src/panel/index.ts | 1 + libs/fabric/src/panel/panel.component.ts | 145 ++++++++++++++++++ libs/fabric/src/panel/panel.module.ts | 25 +++ libs/fabric/src/panel/public-api.ts | 2 + tsconfig.packages.json | 11 +- 21 files changed, 354 insertions(+), 85 deletions(-) create mode 100644 apps/demo/src/app/wrapper.component.ts rename libs/core/src/{types.d.ts => types/StringMap.d.ts} (100%) create mode 100644 libs/fabric/src/panel/index.ts create mode 100644 libs/fabric/src/panel/panel.component.ts create mode 100644 libs/fabric/src/panel/panel.module.ts create mode 100644 libs/fabric/src/panel/public-api.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index a5da40e..ac73338 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,10 @@ { "cSpell.words": [ + "Focusable", "Injectable", "Packagr", "Renderable", + "nrwl", "unmount", "whitelisted" ] diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html index 2c29b47..5ebb57d 100644 --- a/apps/demo/src/app/app.component.html +++ b/apps/demo/src/app/app.component.html @@ -3,21 +3,40 @@

Getting up and running...

    -
  1. Add AngularReactBrowserModule> to app.module.ts in place of the default BrowserModule.
  2. -
  3. Add Fab[component]Module or Mat[component]Module to app.module.ts imports.
  4. +
  5. Add + AngularReactBrowserModule> to + app.module.ts in place of the default + BrowserModule.
  6. +
  7. Add + Fab[component]Module or + Mat[component]Module to + app.module.ts imports.
  8. Add Fabric or Material components to your views.
- - + + Hello world! {{ counter }} - - + + -
- + + + + + + + + +
Hello header!
+
+
+ diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts index 5850888..6fec2a2 100644 --- a/apps/demo/src/app/app.component.ts +++ b/apps/demo/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { DialogType } from 'office-ui-fabric-react/lib/Dialog'; +import { IButtonProps } from '../../../../node_modules/office-ui-fabric-react/lib/Button'; @Component({ selector: 'app-root', @@ -12,10 +13,47 @@ export class AppComponent { dialogHidden = true; counter = 0; + isPanelOpen = false; + + + menuProps: IButtonProps['menuProps'] = { + items: [ + { + key: 'emailMessage', + text: 'Email message', + onClick: () => alert('email clicked!'), + iconProps: { + iconName: 'Mail', + style: { + color: 'red', + }, + } + }, + { + key: 'calendarEvent', + text: 'Calendar event', + iconProps: { + iconName: 'Calendar' + } + } + ] + } + + iconProps = { + iconName: 'Add', + styles: { + root: { fontSize: 'x-large' } + } + }; + toggleDialog() { this.dialogHidden = !this.dialogHidden; } + onClick() { + alert('clicked!'); + } + incrementCounter() { this.counter += 1; } diff --git a/apps/demo/src/app/app.module.ts b/apps/demo/src/app/app.module.ts index d22375e..49da734 100644 --- a/apps/demo/src/app/app.module.ts +++ b/apps/demo/src/app/app.module.ts @@ -1,14 +1,25 @@ +import { AngularReactBrowserModule } from '@angular-react/core'; +import { FabButtonModule, FabDialogModule, FabIconModule, FabImageModule, FabPanelModule } from '@angular-react/fabric'; import { NgModule } from '@angular/core'; import { NxModule } from '@nrwl/nx'; - -import { AngularReactBrowserModule } from '@angular-react/core'; -import { FabDialogModule, FabButtonModule } from '@angular-react/fabric'; +import { initializeIcons } from 'office-ui-fabric-react/lib/Icons'; import { AppComponent } from './app.component'; - +import { WrapperComponent } from './wrapper.component'; @NgModule({ - imports: [AngularReactBrowserModule, NxModule.forRoot(), FabButtonModule, FabDialogModule], - declarations: [AppComponent], - bootstrap: [AppComponent] + imports: [AngularReactBrowserModule, + NxModule.forRoot(), + FabIconModule, + FabButtonModule, + FabDialogModule, + FabImageModule, + FabPanelModule, + ], + declarations: [AppComponent, WrapperComponent], + bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { + constructor() { + initializeIcons(); + } +} diff --git a/apps/demo/src/app/wrapper.component.ts b/apps/demo/src/app/wrapper.component.ts new file mode 100644 index 0000000..1171809 --- /dev/null +++ b/apps/demo/src/app/wrapper.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'wrapper', + template: ``, + styles: ['react-renderer'], +}) +export class WrapperComponent { } diff --git a/libs/core/package.json b/libs/core/package.json index 21a4d24..9b5b387 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -4,6 +4,10 @@ "version": "0.1.12", "ngPackage": { "lib": { + "languageLevel": [ + "dom", + "es2017" + ], "entryFile": "public-api.ts", "umdModuleIds": { "react": "React", diff --git a/libs/core/public-api.ts b/libs/core/public-api.ts index ff18945..84f280a 100644 --- a/libs/core/public-api.ts +++ b/libs/core/public-api.ts @@ -1,2 +1,3 @@ -export { registerElement } from './src/renderer/registry'; export { AngularReactBrowserModule } from './src/angular-react-browser.module'; +export { ReactWrapperComponent } from './src/components/wrapper-component'; +export { registerElement } from './src/renderer/registry'; diff --git a/libs/core/src/components/wrapper-component.ts b/libs/core/src/components/wrapper-component.ts index 9e2e072..dd2c471 100644 --- a/libs/core/src/components/wrapper-component.ts +++ b/libs/core/src/components/wrapper-component.ts @@ -1,5 +1,5 @@ -import { isReactNode } from "@angular-react/core/src/renderer/react-node"; import { AfterViewInit, ElementRef } from "@angular/core"; +import { isReactNode } from "../renderer/react-node"; const blacklistedAttributesAsProps = [ 'class', @@ -7,7 +7,7 @@ const blacklistedAttributesAsProps = [ ]; const blacklistedAttributeMatchers = [ - /^_ng.*/ + /^_?ng-?.*/ ] diff --git a/libs/core/src/renderer/react-content.ts b/libs/core/src/renderer/react-content.ts index 53381d7..e212d0d 100644 --- a/libs/core/src/renderer/react-content.ts +++ b/libs/core/src/renderer/react-content.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -const DEBUG = false; +const DEBUG = true; export const CHILDREN_TO_APPEND_PROP = 'children-to-append'; export class ReactContent extends React.Component { diff --git a/libs/core/src/renderer/react-node.ts b/libs/core/src/renderer/react-node.ts index 75403e0..dc1a937 100644 --- a/libs/core/src/renderer/react-node.ts +++ b/libs/core/src/renderer/react-node.ts @@ -1,10 +1,12 @@ +/// + import * as React from 'react'; import * as ReactDOM from 'react-dom'; import removeUndefinedProperties from '../utils/object/remove-undefined-properties'; import { CHILDREN_TO_APPEND_PROP } from './react-content'; import { getComponentClass, ReactComponentClass } from "./registry"; -const DEBUG = false; +const DEBUG = true; export function isReactNode(node: any): node is ReactNode { return (node).setRenderPendingCallback !== undefined; @@ -182,12 +184,37 @@ export class ReactNode { this.props[CHILDREN_TO_APPEND_PROP] = this.childrenToAppend; - const clearedProps = removeUndefinedProperties(this.props); + const clearedProps = this.transformProps( + removeUndefinedProperties(this.props) + ); if (DEBUG) { console.warn('ReactNode > renderRecursive > type:', this.toString(), 'props:', this.props, 'children:', children); } return React.createElement(this.type, clearedProps, children.length > 0 ? children : undefined); } + private transformProps(props: object) { + return Object.entries(props).reduce((acc, [key, value]) => { + const [transformKey, transformValue] = this.transformProp(key, value); + return { + ...acc, + [transformKey]: transformValue, + }; + }, {}); + } + + private transformProp(name: string, value: TValue): [string, TValue] { + // prop name is camelCased already + const firstLetter = name[0]; + if (firstLetter === firstLetter.toLowerCase()) { + return [name, value]; + } + + // prop name is PascalCased & is a function - assuming render prop + if (typeof value === 'function') { + return [`on${name}`, value]; + } + } + // This is called by Angular core when projected content is being added. appendChild(projectedContent: HTMLElement) { if (DEBUG) { console.error('ReactNode > appendChild > node:', this.toString(), 'projectedContent:', projectedContent.toString().trim()); } diff --git a/libs/core/src/renderer/renderer.ts b/libs/core/src/renderer/renderer.ts index c4ef6ad..38a05d8 100644 --- a/libs/core/src/renderer/renderer.ts +++ b/libs/core/src/renderer/renderer.ts @@ -1,25 +1,10 @@ // tslint:disable:no-bitwise -import { - Injectable, - RendererType2, - Renderer2, - RendererStyleFlags2 -} from '@angular/core'; -import { BrowserModule, EventManager } from '@angular/platform-browser'; -import { - ɵDomRendererFactory2, - ɵDomSharedStylesHost, - ɵNAMESPACE_URIS -} from '@angular/platform-browser'; -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import { Injectable, Renderer2, RendererStyleFlags2, RendererType2 } from '@angular/core'; +import { EventManager, ɵDomRendererFactory2, ɵDomSharedStylesHost } from '@angular/platform-browser'; +import { isReactNode, ReactNode } from './react-node'; -import { ReactComponentClass, getComponentClass } from './registry'; -import { ReactNode, isReactNode } from './react-node'; - - -const DEBUG = false; +const DEBUG = true; @Injectable() export class AngularReactRendererFactory extends ɵDomRendererFactory2 { @@ -52,7 +37,7 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 { return this.defaultReactRenderer; } - return super.createRenderer(element, type); + return super.createRenderer(element, type); } begin() { } @@ -73,9 +58,9 @@ export class AngularReactRendererFactory extends ɵDomRendererFactory2 { class ReactRenderer implements Renderer2 { data: { [key: string]: any } = Object.create(null); - constructor(private rootRenderer: AngularReactRendererFactory) {} + constructor(private rootRenderer: AngularReactRendererFactory) { } - destroy(): void {} + destroy(): void { } destroyNode(node: ReactNode): void { if (DEBUG) { console.error('Renderer > destroyNode > node:', node.toString()); } @@ -171,7 +156,7 @@ class ReactRenderer implements Renderer2 { if (DEBUG) { console.log('NOT IMPLEMENTED - Renderer > nextSibling > node:', node.toString()); } } - setAttribute(node: ReactNode, name: string, value: string, namespace?: string ): void { + setAttribute(node: ReactNode, name: string, value: string, namespace?: string): void { if (DEBUG) { console.log('Renderer > setAttribute > node:', node.toString(), 'name:', name, 'value:', value, namespace ? 'namespace:' : '', namespace); } node.setProperty(name, value); } @@ -208,7 +193,7 @@ class ReactRenderer implements Renderer2 { } removeStyle(node: ReactNode, style: string, flags: RendererStyleFlags2): void { - if (DEBUG) { console.log( 'Renderer > removeStyle > node:', node.toString(), 'style:', style, 'flags:', flags); } + if (DEBUG) { console.log('Renderer > removeStyle > node:', node.toString(), 'style:', style, 'flags:', flags); } node.removeProperty('style', style); } diff --git a/libs/core/src/types.d.ts b/libs/core/src/types/StringMap.d.ts similarity index 100% rename from libs/core/src/types.d.ts rename to libs/core/src/types/StringMap.d.ts diff --git a/libs/fabric/public-api.ts b/libs/fabric/public-api.ts index 2da0eb2..ffdf06e 100644 --- a/libs/fabric/public-api.ts +++ b/libs/fabric/public-api.ts @@ -1,8 +1,10 @@ -export * from './src/button/button.module'; export * from './src/button/button.component'; -export * from './src/dialog/dialog.module' +export * from './src/button/button.module'; export * from './src/dialog/dialog.component'; +export * from './src/dialog/dialog.module'; export * from './src/icon/icon.component'; export * from './src/icon/icon.module'; export * from './src/image/image.component'; export * from './src/image/image.module'; +export * from './src/panel/panel.component'; +export * from './src/panel/panel.module'; diff --git a/libs/fabric/src/button/button.component.ts b/libs/fabric/src/button/button.component.ts index 105ab27..46f0036 100644 --- a/libs/fabric/src/button/button.component.ts +++ b/libs/fabric/src/button/button.component.ts @@ -4,35 +4,41 @@ // tslint:disable:use-host-property-decorator // tslint:disable:no-output-on-prefix -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output } from '@angular/core'; - -import { IButtonProps, DefaultButton } from 'office-ui-fabric-react/lib/Button'; - +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { IButtonProps } from 'office-ui-fabric-react/lib/Button'; @Component({ selector: 'fab-button', exportAs: 'fabButton', template: ` + [split]="split" + [href]="href" + [menuProps]="menuProps" + [iconProps]="iconProps" + (onClick)="onClick.emit($event)"> + `, styles: [ 'react-renderer', - ':host { display: inline-block; background: red; }' // TODO: this isn't working. Problem with react-renderer. ], changeDetection: ChangeDetectionStrategy.OnPush, host: { 'class': 'fab-button' } }) export class FabButtonComponent { - @Input() disabled = false; - @Input() primary = true; - @Input('label') text = ''; + @Input() disabled?: IButtonProps['disabled']; + @Input() primary?: IButtonProps['primary']; + @Input() checked?: IButtonProps['checked']; + @Input() href?: IButtonProps['href']; + @Input() text?: IButtonProps['text']; + @Input() split?: IButtonProps['split']; + @Input() menuProps?: IButtonProps['menuProps']; + @Input() iconProps?: IButtonProps['iconProps']; + @Input() primaryDisabled?: IButtonProps['primaryDisabled']; @Output() onClick: EventEmitter = new EventEmitter(); diff --git a/libs/fabric/src/dialog/dialog.component.ts b/libs/fabric/src/dialog/dialog.component.ts index 8520b9b..abb0952 100644 --- a/libs/fabric/src/dialog/dialog.component.ts +++ b/libs/fabric/src/dialog/dialog.component.ts @@ -3,23 +3,16 @@ // tslint:disable:no-output-rename // tslint:disable:use-host-property-decorator -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - Output, -} from '@angular/core'; - -import { IDialogProps, Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/components/Dialog'; - +import { ReactWrapperComponent } from '@angular-react/core'; +import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { DialogType, IDialogProps } from 'office-ui-fabric-react/lib/components/Dialog'; @Component({ selector: 'fab-dialog', exportAs: 'fabDialog', template: ` + `, diff --git a/libs/fabric/src/image/image.component.ts b/libs/fabric/src/image/image.component.ts index 2d256cf..14b14dd 100644 --- a/libs/fabric/src/image/image.component.ts +++ b/libs/fabric/src/image/image.component.ts @@ -1,4 +1,4 @@ -import { ReactWrapperComponent } from '@angular-react/core/src/components/wrapper-component'; +import { ReactWrapperComponent } from '@angular-react/core'; import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { IImageProps, ImageLoadState } from 'office-ui-fabric-react/lib/Image'; diff --git a/libs/fabric/src/panel/index.ts b/libs/fabric/src/panel/index.ts new file mode 100644 index 0000000..7e1a213 --- /dev/null +++ b/libs/fabric/src/panel/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/libs/fabric/src/panel/panel.component.ts b/libs/fabric/src/panel/panel.component.ts new file mode 100644 index 0000000..a68c688 --- /dev/null +++ b/libs/fabric/src/panel/panel.component.ts @@ -0,0 +1,145 @@ +// tslint:disable:component-selector +// tslint:disable:no-input-rename +// tslint:disable:no-output-rename +// tslint:disable:use-host-property-decorator +// tslint:disable:no-output-on-prefix + +import { ReactWrapperComponent } from '@angular-react/core'; +import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { IPanelHeaderRenderer, IPanelProps } from 'office-ui-fabric-react/lib/Panel'; + +@Component({ + selector: 'fab-panel', + exportAs: 'fabPanel', + template: ` + + + + + + + + `, + styles: [ + 'react-renderer', + ], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { 'class': 'fab-panel' } +}) +export class FabPanelComponent extends ReactWrapperComponent { + @ViewChild('reactNode') protected reactNodeRef: ElementRef; + + @Input() componentRef?: IPanelProps['componentRef']; + @Input() isOpen?: IPanelProps['isOpen']; + @Input() hasCloseButton?: IPanelProps['hasCloseButton']; + @Input() isLightDismiss?: IPanelProps['isLightDismiss']; + @Input() isHiddenOnDismiss?: IPanelProps['isHiddenOnDismiss']; + @Input() isBlocking?: IPanelProps['isBlocking']; + @Input() isFooterAtBottom?: IPanelProps['isFooterAtBottom']; + @Input() headerText?: IPanelProps['headerText']; + @Input() className?: IPanelProps['className']; + @Input() type?: IPanelProps['type']; + @Input() customWidth?: IPanelProps['customWidth']; + @Input() closeButtonAriaLabel?: IPanelProps['closeButtonAriaLabel']; + @Input() headerClassName?: IPanelProps['headerClassName']; + @Input() elementToFocusOnDismiss?: IPanelProps['elementToFocusOnDismiss']; + @Input() ignoreExternalFocusing?: IPanelProps['ignoreExternalFocusing']; + @Input() forceFocusInsideTrap?: IPanelProps['forceFocusInsideTrap']; + @Input() firstFocusableSelector?: IPanelProps['firstFocusableSelector']; + @Input() focusTrapZoneProps?: IPanelProps['focusTrapZoneProps']; + @Input() layerProps?: IPanelProps['layerProps']; + @Input() componentId?: IPanelProps['componentId']; + + // @Input() headerTemplate?: TemplateRef + + @Output() onLightDismissClick = new EventEmitter(); + @Output() onDismiss = new EventEmitter(); + @Output() onDismissed = new EventEmitter(); + @Output() onRenderNavigation = new EventEmitter(); + // @Output() onRenderHeader = new EventEmitter(); + // @Output() onRenderBody = new EventEmitter(); + // @Output() onRenderFooter = new EventEmitter(); + // @Output() onRenderFooterContent = new EventEmitter(); + + @ContentChild('[fab-panel-header]') headerTemplate?: ElementRef; + + constructor(elementRef: ElementRef) { + super(elementRef); + + // coming from React context - we need to bind to this so we can access the Angular Component properties + this.onRenderHeader = this.onRenderHeader.bind(this); + } + + onRenderHeader(props?: IPanelProps, defaultRender?: IPanelHeaderRenderer, headerTextId?: string | undefined) { + if (!this.headerTemplate) { + return null; + } + + // FIXME: temp + return null; + /* + const tagName = (this.headerTemplate.nativeElement as HTMLElement).tagName; + + return React.createElement( + tagName, + {}, + undefined + ); */ + /* this.headerTemplate.createEmbeddedView({ + props: ren + }) */ + } + +} + +/** + * Counterpart of `IPanelHeaderRenderer`. + */ +export interface IPanelHeaderTemplateContext { + props?: IPanelProps + headerTextId?: string | undefined +} + +@Component({ + selector: 'fab-panel-header', + exportAs: 'fabPanelHeader', + template: ` + + + + `, + styles: ['react-renderer'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { 'class': 'fab-panel-header' } +}) +export class FabPanelHeaderComponent { } diff --git a/libs/fabric/src/panel/panel.module.ts b/libs/fabric/src/panel/panel.module.ts new file mode 100644 index 0000000..215c57b --- /dev/null +++ b/libs/fabric/src/panel/panel.module.ts @@ -0,0 +1,25 @@ +import { registerElement } from '@angular-react/core'; +import { CommonModule } from '@angular/common'; +import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Panel } from 'office-ui-fabric-react/lib/Panel'; +import { FabPanelComponent, FabPanelHeaderComponent } from './panel.component'; + +const components = [ + FabPanelComponent, + FabPanelHeaderComponent, +]; + +@NgModule({ + imports: [CommonModule], + declarations: components, + exports: components, + schemas: [NO_ERRORS_SCHEMA] +}) +export class FabPanelModule { + + constructor() { + // Add any React elements to the registry (used by the renderer). + registerElement('Panel', () => Panel); + } + +} diff --git a/libs/fabric/src/panel/public-api.ts b/libs/fabric/src/panel/public-api.ts new file mode 100644 index 0000000..37880dd --- /dev/null +++ b/libs/fabric/src/panel/public-api.ts @@ -0,0 +1,2 @@ +export * from './panel.component'; +export * from './panel.module'; diff --git a/tsconfig.packages.json b/tsconfig.packages.json index f328492..0fd5274 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -5,15 +5,8 @@ "module": "es2015", "baseUrl": ".", "paths": { - "@angular-react/*": [ - "@angular-react/*" - ] + "@angular-react/*": ["@angular-react/*"] } }, - "exclude": [ - "**/*.spec.ts", - "**/*.e2e-spec.ts", - "node_modules", - "tmp" - ] + "exclude": ["**/*.spec.ts", "**/*.e2e-spec.ts", "node_modules", "tmp"] }