From aa774698a1c07027869c72c64234c1e6b240b71b Mon Sep 17 00:00:00 2001 From: xelaint Date: Fri, 19 Jul 2024 10:39:48 -0600 Subject: [PATCH] feat(design): change daffSizeMixin to a directive (#2925) --- .../cart-view/cart-view.component.spec.ts | 8 -- .../app/core/footer/footer.component.spec.ts | 8 -- .../newsletter/newsletter.component.spec.ts | 9 -- .../product/product.component.spec.ts | 8 -- .../product-grid-view.component.spec.ts | 8 -- .../src/button/button-sizable.directive.ts | 20 ++++ .../src/button/button.component.spec.ts | 8 +- .../button/src/button/button.component.ts | 39 ++++--- libs/design/button/src/public_api.ts | 1 + .../src/container/container.component.spec.ts | 14 +-- .../src/container/container.component.ts | 34 ++---- libs/design/src/core/sizable/public_api.ts | 2 +- libs/design/src/core/sizable/sizable-mixin.ts | 52 --------- .../core/sizable/sizable.directive.spec.ts | 104 +++++++++++++++++ .../src/core/sizable/sizable.directive.ts | 68 +++++++++++ libs/design/src/core/sizable/sizable.spec.ts | 106 ------------------ libs/design/src/core/sizable/sizable.ts | 6 +- 17 files changed, 238 insertions(+), 257 deletions(-) create mode 100644 libs/design/button/src/button/button-sizable.directive.ts delete mode 100644 libs/design/src/core/sizable/sizable-mixin.ts create mode 100644 libs/design/src/core/sizable/sizable.directive.spec.ts create mode 100644 libs/design/src/core/sizable/sizable.directive.ts delete mode 100644 libs/design/src/core/sizable/sizable.spec.ts diff --git a/apps/demo/src/app/cart/pages/cart-view/cart-view.component.spec.ts b/apps/demo/src/app/cart/pages/cart-view/cart-view.component.spec.ts index 493e423bb4..17a77370bd 100644 --- a/apps/demo/src/app/cart/pages/cart-view/cart-view.component.spec.ts +++ b/apps/demo/src/app/cart/pages/cart-view/cart-view.component.spec.ts @@ -126,12 +126,4 @@ describe('DemoCartViewComponent', () => { expect(loadingIcon).toBeNull(); }); }); - - describe('on ', () => { - it('should set size="md"', () => { - const container = fixture.debugElement.query(By.css('daff-container')); - - expect(container.componentInstance.size).toEqual('md'); - }); - }); }); diff --git a/apps/demo/src/app/core/footer/footer.component.spec.ts b/apps/demo/src/app/core/footer/footer.component.spec.ts index 9d80b2c967..b84a91e4a1 100644 --- a/apps/demo/src/app/core/footer/footer.component.spec.ts +++ b/apps/demo/src/app/core/footer/footer.component.spec.ts @@ -41,14 +41,6 @@ describe('FooterComponent', () => { expect(component).toBeTruthy(); }); - describe('on ', () => { - it('should set size="md"', () => { - const container = fixture.debugElement.query(By.css('daff-container')); - - expect(container.componentInstance.size).toEqual('md'); - }); - }); - it('renders a for each leaf in the tree of links', () => { const listItems = fixture.debugElement.queryAll(By.css('daff-list-item')); const numberOfLinks = component.links.reduce((acc,linkset)=>acc+linkset.links.length,0); diff --git a/apps/demo/src/app/newsletter/newsletter.component.spec.ts b/apps/demo/src/app/newsletter/newsletter.component.spec.ts index fa32880a31..b270979358 100644 --- a/apps/demo/src/app/newsletter/newsletter.component.spec.ts +++ b/apps/demo/src/app/newsletter/newsletter.component.spec.ts @@ -4,7 +4,6 @@ import { TestBed, } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; import { DaffContainerModule } from '@daffodil/design/container'; import { @@ -45,14 +44,6 @@ describe('NewsletterComponent', () => { expect(component).toBeTruthy(); }); - describe('on ', () => { - it('should set size="md"', () => { - const container = fixture.debugElement.query(By.css('daff-container')); - - expect(container.componentInstance.size).toEqual('md'); - }); - }); - describe('when intialized', () => { let newsletterElement; diff --git a/apps/demo/src/app/product/components/product/product.component.spec.ts b/apps/demo/src/app/product/components/product/product.component.spec.ts index c2f2d5e2ea..38276abfe6 100644 --- a/apps/demo/src/app/product/components/product/product.component.spec.ts +++ b/apps/demo/src/app/product/components/product/product.component.spec.ts @@ -105,12 +105,4 @@ describe('ProductComponent', () => { expect(router.navigateByUrl).toHaveBeenCalledWith('/404'); }); }); - - describe('on ', () => { - it('should set size="lg"', () => { - const container = fixture.debugElement.query(By.css('daff-container')); - - expect(container.componentInstance.size).toEqual('lg'); - }); - }); }); diff --git a/apps/demo/src/app/product/pages/product-grid-view/product-grid-view.component.spec.ts b/apps/demo/src/app/product/pages/product-grid-view/product-grid-view.component.spec.ts index ddde053f95..2ffb5330fc 100644 --- a/apps/demo/src/app/product/pages/product-grid-view/product-grid-view.component.spec.ts +++ b/apps/demo/src/app/product/pages/product-grid-view/product-grid-view.component.spec.ts @@ -67,14 +67,6 @@ describe('ProductGridViewComponent', () => { }); }); - describe('on ', () => { - it('should set size="lg"', () => { - const container = fixture.debugElement.query(By.css('daff-container')); - - expect(container.componentInstance.size).toEqual('lg'); - }); - }); - describe('when loading$ becomes false', () => { beforeEach(() => { diff --git a/libs/design/button/src/button/button-sizable.directive.ts b/libs/design/button/src/button/button-sizable.directive.ts new file mode 100644 index 0000000000..1aaf903e1a --- /dev/null +++ b/libs/design/button/src/button/button-sizable.directive.ts @@ -0,0 +1,20 @@ +import { Directive } from '@angular/core'; + +import { + DaffSizableDirective, + DaffSizeLargeType, + DaffSizeMediumType, + DaffSizeSmallType, +} from '@daffodil/design'; + +/** + * The DaffSizable {@link DaffSizable } types that the DaffButtonComponent can implement. + */ +export type DaffButtonSize = DaffSizeSmallType | DaffSizeMediumType | DaffSizeLargeType; + +@Directive({ + standalone: true, +}) + +export class DaffButtonSizableDirective extends DaffSizableDirective {} + diff --git a/libs/design/button/src/button/button.component.spec.ts b/libs/design/button/src/button/button.component.spec.ts index 287dbb9c34..cc87515a1e 100644 --- a/libs/design/button/src/button/button.component.spec.ts +++ b/libs/design/button/src/button/button.component.spec.ts @@ -18,10 +18,8 @@ import { DaffLoadingIconComponent, } from '@daffodil/design/loading-icon'; -import { - DaffButtonComponent, - DaffButtonSize, -} from './button.component'; +import { DaffButtonSize } from './button-sizable.directive'; +import { DaffButtonComponent } from './button.component'; @Component({ template: ` @@ -197,7 +195,7 @@ describe('@daffodil/design/button | DaffButtonComponent', () => { }); describe('using the size property of a button', () => { - it('should add the class of the defined size to the host element', () => { + it('should take size as an input', () => { wrapper.size = 'md'; fixture.detectChanges(); diff --git a/libs/design/button/src/button/button.component.ts b/libs/design/button/src/button/button.component.ts index 59f18215b1..719dad96a3 100644 --- a/libs/design/button/src/button/button.component.ts +++ b/libs/design/button/src/button/button.component.ts @@ -17,16 +17,13 @@ import { DaffSuffixable, daffPrefixableMixin, daffSuffixableMixin, - DaffSizable, - DaffSizeSmallType, - DaffSizeMediumType, - DaffSizeLargeType, - daffSizeMixin, DaffStatusable, daffStatusMixin, DaffArticleEncapsulatedDirective, } from '@daffodil/design'; +import { DaffButtonSizableDirective } from './button-sizable.directive'; + /** * List of classes to add to DaffButtonComponent instances based on host attributes to style as different variants. */ @@ -46,15 +43,10 @@ class DaffButtonBase{ constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} } -const _daffButtonBase = daffPrefixableMixin(daffSuffixableMixin(daffColorMixin(daffStatusMixin(daffSizeMixin(DaffButtonBase, 'md'))))); +const _daffButtonBase = daffPrefixableMixin(daffSuffixableMixin(daffColorMixin(daffStatusMixin((DaffButtonBase))))); export type DaffButtonType = 'daff-button' | 'daff-stroked-button' | 'daff-raised-button' | 'daff-flat-button' | 'daff-icon-button' | 'daff-underline-button' | undefined; -/** - * The DaffSizable types that the DaffButtonComponent can implement - */ -export type DaffButtonSize = DaffSizeSmallType | DaffSizeMediumType | DaffSizeLargeType; - enum DaffButtonTypeEnum { Default = 'daff-button', Stroked = 'daff-stroked-button', @@ -86,20 +78,28 @@ enum DaffButtonTypeEnum { styleUrls: ['./button.component.scss'], //todo(damienwebdev): remove once decorators hit stage 3 - https://github.com/microsoft/TypeScript/issues/7342 // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'size', 'status'], - hostDirectives: [{ - directive: DaffArticleEncapsulatedDirective, - }], + inputs: ['color', 'status'], + hostDirectives: [ + { directive: DaffArticleEncapsulatedDirective }, + { + directive: DaffButtonSizableDirective, + inputs: ['size'], + }, + ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DaffButtonComponent extends _daffButtonBase - implements OnInit, DaffPrefixable, DaffSuffixable, DaffColorable, DaffSizable, DaffStatusable { + implements OnInit, DaffPrefixable, DaffSuffixable, DaffColorable, DaffStatusable { private buttonType: DaffButtonType; - constructor(private elementRef: ElementRef, private renderer: Renderer2) { + constructor( + private elementRef: ElementRef, + private renderer: Renderer2, + private size: DaffButtonSizableDirective, + ) { super(elementRef, renderer); for (const attr of BUTTON_HOST_ATTRIBUTES) { @@ -107,6 +107,11 @@ export class DaffButtonComponent (elementRef.nativeElement).classList.add(attr); } } + + /** + * Sets the default size of a button to medium. + */ + this.size.defaultSize = 'md'; } /** diff --git a/libs/design/button/src/public_api.ts b/libs/design/button/src/public_api.ts index 6d5884f78c..955a4ead99 100644 --- a/libs/design/button/src/public_api.ts +++ b/libs/design/button/src/public_api.ts @@ -1,2 +1,3 @@ export { DaffButtonComponent } from './button/button.component'; export { DaffButtonModule } from './button.module'; +export { DaffButtonSizableDirective } from './button/button-sizable.directive'; diff --git a/libs/design/container/src/container/container.component.spec.ts b/libs/design/container/src/container/container.component.spec.ts index f5567e2f7a..5b40295704 100644 --- a/libs/design/container/src/container/container.component.spec.ts +++ b/libs/design/container/src/container/container.component.spec.ts @@ -57,16 +57,16 @@ describe('@daffodil/design/container | DaffContainerComponent', () => { }); describe('setting the size', () => { + it('should take size as an input', () => { + wrapper.size = 'md'; + fixture.detectChanges(); + + expect(de.nativeElement.classList.contains('daff-md')).toEqual(true); + }); + it('should not set a default size', () => { de = fixture.debugElement.query(By.css('daff-container')); expect(de.nativeElement.classList.toString()).toEqual('daff-container'); }); - - it('should add the size class on the host element for the defined size', () => { - wrapper.size = 'xs'; - fixture.detectChanges(); - - expect(de.nativeElement.classList.contains('daff-xs')).toEqual(true); - }); }); }); diff --git a/libs/design/container/src/container/container.component.ts b/libs/design/container/src/container/container.component.ts index d01d27b453..a9fe81427e 100644 --- a/libs/design/container/src/container/container.component.ts +++ b/libs/design/container/src/container/container.component.ts @@ -1,26 +1,10 @@ import { Component, - Input, ChangeDetectionStrategy, HostBinding, - ElementRef, - Renderer2, } from '@angular/core'; -import { - DaffSizable, - DaffSizeAllType, - daffSizeMixin, -} from '@daffodil/design'; - -/** - * An _elementRef and an instance of renderer2 are needed for the Sizeable mixin - */ -class DaffContainerBase{ - constructor(public _elementRef: ElementRef, public _renderer: Renderer2) {} -} - -const _daffContainerBase = daffSizeMixin(DaffContainerBase); +import { DaffSizableDirective } from '@daffodil/design'; /** * @inheritdoc @@ -29,19 +13,17 @@ const _daffContainerBase = daffSizeMixin(DaffContainerBase); selector: 'daff-container', styleUrls: ['./container.component.scss'], template: '', - //todo(damienwebdev): remove once decorators hit stage 3 - https://github.com/microsoft/TypeScript/issues/7342 - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['size'], + hostDirectives: [ + { + directive: DaffSizableDirective, + inputs: ['size'], + }, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DaffContainerComponent extends _daffContainerBase implements DaffSizable { - +export class DaffContainerComponent { /** * @docs-private */ @HostBinding('class.daff-container') class = true; - - constructor(private elementRef: ElementRef, private renderer: Renderer2) { - super(elementRef, renderer); - } } diff --git a/libs/design/src/core/sizable/public_api.ts b/libs/design/src/core/sizable/public_api.ts index 83dccadf1d..8d8a1cc622 100644 --- a/libs/design/src/core/sizable/public_api.ts +++ b/libs/design/src/core/sizable/public_api.ts @@ -8,4 +8,4 @@ export { DaffSizeLargeType, DaffSizeXLargeType, } from './sizable'; -export { daffSizeMixin } from './sizable-mixin'; +export { DaffSizableDirective } from './sizable.directive'; diff --git a/libs/design/src/core/sizable/sizable-mixin.ts b/libs/design/src/core/sizable/sizable-mixin.ts deleted file mode 100644 index aec679495d..0000000000 --- a/libs/design/src/core/sizable/sizable-mixin.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - ElementRef, - Input, - Renderer2, -} from '@angular/core'; - -import { DaffSizeAllType } from './sizable'; -import { Constructor } from '../constructor/constructor'; - -export interface HasElementRef { - _elementRef: ElementRef; - _renderer: Renderer2; -} - -export function -daffSizeMixin = Constructor>(Base: T, defaultSize?: V) { - class DaffSizeMixinClass extends Base { - //TODO move this back to private in Typescript 3.1 - _size: V; - - get size(): V{ - return this._size; - } - set size(value: V) { - // Handles the default size - const incomingSize = value || defaultSize; - - if(incomingSize !== this._size){ //Only run the dom-render if a change occurs - //Remove the old size - if(this._size){ - this._renderer.removeClass(this._elementRef.nativeElement, `daff-${this._size}`); - } - - if(incomingSize){ - this._renderer.addClass(this._elementRef.nativeElement, `daff-${incomingSize}`); - } - - this._size = incomingSize; - } - } - - constructor(...args: any[]) { - super(...args); - this.size = defaultSize; - } - }; - - // TODO: ugly workaround for https://github.com/microsoft/TypeScript/issues/7342#issuecomment-624298133 - Input()(DaffSizeMixinClass.prototype, 'size'); - - return DaffSizeMixinClass; -} diff --git a/libs/design/src/core/sizable/sizable.directive.spec.ts b/libs/design/src/core/sizable/sizable.directive.spec.ts new file mode 100644 index 0000000000..3b33ca2acc --- /dev/null +++ b/libs/design/src/core/sizable/sizable.directive.spec.ts @@ -0,0 +1,104 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffSizeAllType } from './sizable'; +import { DaffSizableDirective } from './sizable.directive'; + +@Component({ + template: ` +
`, +}) + +class WrapperComponent { + size: string; +} + +describe('@daffodil/design | DaffSizableDirective', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + let directive: DaffSizableDirective; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + WrapperComponent, + ], + imports: [ + DaffSizableDirective, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('[daffSizable]')); + + directive = de.injector.get(DaffSizableDirective); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + expect(directive).toBeTruthy(); + }); + + it('should take size as an input', () => { + expect(directive.size).toEqual(wrapper.size); + }); + + it('should add a class of .daff-xs to the host element if size is set to xs', () => { + wrapper.size = 'xs'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-xs': true, + })); + }); + + it('should add a class of .daff-sm to the host element if size is set to sm', () => { + wrapper.size = 'sm'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-sm': true, + })); + }); + + it('should add a class of .daff-md to the host element if size is set to md', () => { + wrapper.size = 'md'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-md': true, + })); + }); + + it('should add a class of .daff-lg to the host element if size is set to lg', () => { + wrapper.size = 'lg'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-lg': true, + })); + }); + + it('should add a class of .daff-xl to the host element if size is set to xl', () => { + wrapper.size = 'xl'; + fixture.detectChanges(); + + expect(directive.class).toEqual(jasmine.objectContaining({ + 'daff-xl': true, + })); + }); +}); diff --git a/libs/design/src/core/sizable/sizable.directive.ts b/libs/design/src/core/sizable/sizable.directive.ts new file mode 100644 index 0000000000..268e0dd3ef --- /dev/null +++ b/libs/design/src/core/sizable/sizable.directive.ts @@ -0,0 +1,68 @@ +import { + Directive, + HostBinding, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; + +import { + DaffSizable, + DaffSizableEnum, + DaffSizeAllType, +} from './sizable'; + +/** + * The `DaffSizableDirective` allows for dynamic sizing of a component by setting + * CSS classes based on the specified size. + * + * ## Example + * + * ```html + *
Sized content
+ * ``` + */ +@Directive({ + selector: '[daffSizable]', + standalone: true, +}) +export class DaffSizableDirective implements DaffSizable, OnChanges, OnInit { + + /** + * Dynamically sets the CSS classes based on the size. + * @docs-private + */ + @HostBinding('class') get class() { + return { + 'daff-xs': this.size === DaffSizableEnum.XSmall, + 'daff-sm': this.size === DaffSizableEnum.Small, + 'daff-md': this.size === DaffSizableEnum.Medium, + 'daff-lg': this.size === DaffSizableEnum.Large, + 'daff-xl': this.size === DaffSizableEnum.XLarge, + }; + } + + /** + * The size of a component. + */ + @Input() size: T; + + /** + * Sets a default size when no size is provided. + */ + public defaultSize: T; + + ngOnChanges(changes: SimpleChanges) { + if(!changes.size?.currentValue) { + this.size = this.defaultSize; + } + } + + ngOnInit() { + if(!this.size) { + this.size = this.defaultSize; + } + } +} + diff --git a/libs/design/src/core/sizable/sizable.spec.ts b/libs/design/src/core/sizable/sizable.spec.ts deleted file mode 100644 index a3d7844a0b..0000000000 --- a/libs/design/src/core/sizable/sizable.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ElementRef } from '@angular/core'; - -import { daffSizeMixin } from './sizable-mixin'; - -class TestingClass { - element: HTMLElement = document.createElement('div'); - - _elementRef = new ElementRef(this.element); - _renderer: any = { - addClass: (el: HTMLElement, className: string) => { - el.classList.add(className); - }, - removeClass: (el: HTMLElement, className: string) => { - el.classList.remove(className); - }, - }; -} - -describe('@daffodil/design | daffSizeMixin', () => { - let instance; - let classWithSize; - - beforeEach(() => { - classWithSize = daffSizeMixin(TestingClass); - instance = new classWithSize(); - }); - - it('should add a size property to an existing class', () => { - expect('size' in instance).toBeTruthy(); - }); - - it('should allow the consuming class to optionally define a default size', () => { - classWithSize = daffSizeMixin(TestingClass, 'sm'); - instance = new classWithSize(); - - expect(instance.size).toEqual('sm'); - expect(instance.element.classList).toContain('daff-sm'); - }); - - describe('when a size is specified', () => { - - it('should set a namespaced size class', () => { - instance.size = 'sm'; - - expect(instance.element.classList).toContain('daff-sm'); - }); - }); - - describe('when a size is not specified', () => { - - it('should default to no size class', () => { - instance.size = undefined; - expect(instance.element.classList.length).toEqual(0); - }); - }); - - describe('when `size` changes', () => { - - beforeEach(() => { - instance.size = 'sm'; - instance.size = 'md'; - }); - - it('should add the new size class', () => { - expect(instance.element.classList).toContain('daff-md'); - }); - - it('should remove the provious size class', () => { - expect(instance.element.classList).not.toContain('daff-sm'); - }); - }); - - describe('when a default size is undefined', () => { - describe('and size is set to null or undefined', () => { - it('should do nothing', () => { - instance.size = null; - expect(instance.element.classList.value).toEqual(''); - - instance.size = undefined; - expect(instance.element.classList.value).toEqual(''); - }); - }); - }); - - describe('when a default size is specified', () => { - - beforeEach(() => { - classWithSize = daffSizeMixin(TestingClass, 'sm'); - instance = new classWithSize(); - }); - - describe('and size is set to null or undefined', () => { - it('should set size to the default size ', () => { - instance.size = null; - - expect(instance.size).toEqual('sm'); - expect(instance.element.classList).toContain('daff-sm'); - - instance.size = undefined; - - expect(instance.size).toEqual('sm'); - expect(instance.element.classList).toContain('daff-sm'); - }); - }); - }); -}); diff --git a/libs/design/src/core/sizable/sizable.ts b/libs/design/src/core/sizable/sizable.ts index 0b52063f41..b29d548fca 100644 --- a/libs/design/src/core/sizable/sizable.ts +++ b/libs/design/src/core/sizable/sizable.ts @@ -1,6 +1,5 @@ /** - * An interface for giving a component the ability to customize sizing for component-specific UI. - * In order to be sizable, a component class must implement this property. + * Interfaces that gives a component the ability to customize sizing for component specific UI. */ export interface DaffSizable { @@ -17,6 +16,9 @@ export type DaffSizeMediumType = 'md'; export type DaffSizeLargeType = 'lg'; export type DaffSizeXLargeType = 'xl'; +/** + * The a type representing all available sizes. + */ export type DaffSizeAllType = DaffSizeXSmallType | DaffSizeSmallType | DaffSizeMediumType | DaffSizeLargeType | DaffSizeXLargeType; export enum DaffSizableEnum {