-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch to stateless directives (#220)
# Pull Request ## 🤨 Rationale This PR addresses [TASK 1741886](https://ni.visualstudio.com/DevCentral/_workitems/edit/1741886) and [TASK 1746789](https://ni.visualstudio.com/DevCentral/_workitems/edit/1746789). The major changes include: - Removing [barrel](https://basarat.gitbook.io/typescript/main-1/barrel) files - After removing barrel files from nimble-angular the library is able to be used in other ViewEngine libraries without resulting in a `Maximum call stack size exceeded` from the angular compiler. - Some possible reasons removing the barrel files helped is eliminating [import cycles](angular/angular#14649) or preventing the same [symbol from being exposed from multiple paths](psykolm22/angular-google-place#29 (comment)). A couple attempts at bisecting did not reveal the exact culprit so barrel files were removed wholesale in this PR. - Change to stateless directives - After switching to ViewEngine builds the `@HostBinding` directives binding to properties that were specific to the nimble components would fail (ie expended state of tree). It seems like that would be [expected generally](angular/angular#13776). Maybe a change in ivy allows for that kind of binding but it is not ViewEngine compatible. - Regardless, this change makes directives stateless instead. This is preferred generally because our custom elements themselves should be managing their state and be the source of truth, not the Angular directive which is just acting as a proxy to the underlying web component. ## 👩💻 Implementation - Barrel files were removed - A similar change (but unrelated to the top-level re-factor) was removing the idea of shared control value accessors and instead moving the existing control value accessors to be next to their component. There isn't a strong reason either way but I think it helps keep the components isolated and helps with repo organization even though it may result in some code duplication (very minimal with the current control value accessor patterns). - Stateless directives - Removed existing `@HostBinding` calls and replaced with accessors setting values through the Renderer2 interface as a step to avoid breaking [Angular Universal](https://www.willtaylor.blog/angular-universal-gotchas/) support and ## 🧪 Testing - Barrel files were removed - Verified against a local build of systemlink-lib-angular with the [changes for nimble-tree-view](https://ni.visualstudio.com/DevCentral/_git/Skyline/pullrequest/228468) applied. Verified that the header was able to perform a production build but not beyond that (ie verifying systemlink-lib-angular using nimble-angular could be used in SystemLinkShared). - Stateless directives - Since we are now managing the definition and state on the directives ourselves (proxying to the web component) it seems necessary to test those code paths instead of relying on the behavior of `@HostBinding`. I modified the nimble-angular boolean directive test to cover the cases I think we should cover and updated the testing docs. - One behavior I found writing the tests is that when binding to templates directly (ie as strings in html `<my-elem something="7">`) angular will [assign to the directive property](angular/angular#6919) a string value even if the TypeScript type is something else, like number. See the comments in `template-value-helpers.ts`. Making sure to test this behavior is captured in the test docs updates. ## ✅ Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed.
- Loading branch information
Showing
48 changed files
with
560 additions
and
196 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
angular-workspace/projects/example-client-app/src/environments/.eslintrc.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module.exports = { | ||
overrides: [ | ||
{ | ||
files: [ | ||
'environment.*.ts' | ||
], | ||
rules: { | ||
// The environment.*.ts files are used in fileReplacements and are not part of the normal tsconfig. | ||
// Because they do not participate in tsconfig they don't get the strictNullChecks compiler setting | ||
// so we disable rules requiring stictNullChecks for these files. | ||
'@typescript-eslint/no-unnecessary-condition': 'off', | ||
'@typescript-eslint/strict-boolean-expressions': 'off' | ||
} | ||
} | ||
] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 0 additions & 2 deletions
2
angular-workspace/projects/ni/nimble-angular/src/directives/button/index.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
203 changes: 197 additions & 6 deletions
203
...ce/projects/ni/nimble-angular/src/directives/button/tests/nimble-button.directive.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,205 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { Component, ElementRef, ViewChild } from '@angular/core'; | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { Button } from '@ni/nimble-components/dist/esm/button'; | ||
import { ButtonAppearance } from '@ni/nimble-components/dist/esm/button/types'; | ||
import { BooleanAttribute } from '../../utilities/template-value-helpers'; | ||
import { NimbleButtonDirective } from '../nimble-button.directive'; | ||
import { NimbleButtonModule } from '../nimble-button.module'; | ||
|
||
describe('Nimble button', () => { | ||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [NimbleButtonModule] | ||
describe('module', () => { | ||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [NimbleButtonModule] | ||
}); | ||
}); | ||
|
||
it('defines custom element', () => { | ||
expect(customElements.get('nimble-button')).not.toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe('with no values in template', () => { | ||
@Component({ | ||
template: ` | ||
<nimble-button #button></nimble-button> | ||
` | ||
}) | ||
class TestHostComponent { | ||
@ViewChild('button', { read: NimbleButtonDirective }) public directive: NimbleButtonDirective; | ||
@ViewChild('button', { read: ElementRef }) public elementRef: ElementRef<Button>; | ||
} | ||
|
||
let fixture: ComponentFixture<TestHostComponent>; | ||
let directive: NimbleButtonDirective; | ||
let nativeElement: Button; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
declarations: [TestHostComponent], | ||
imports: [NimbleButtonModule] | ||
}).compileComponents(); | ||
fixture = TestBed.createComponent(TestHostComponent); | ||
fixture.detectChanges(); | ||
directive = fixture.componentInstance.directive; | ||
nativeElement = fixture.componentInstance.elementRef.nativeElement; | ||
}); | ||
|
||
it('has expected defaults for disabled', () => { | ||
expect(directive.disabled).toBeFalse(); | ||
expect(nativeElement.disabled).toBeFalse(); | ||
}); | ||
|
||
it('has expected defaults for appearance', () => { | ||
expect(directive.appearance).toBe(ButtonAppearance.Outline); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Outline); | ||
}); | ||
}); | ||
|
||
describe('with template string values', () => { | ||
@Component({ | ||
template: ` | ||
<nimble-button #button | ||
disabled | ||
appearance="${ButtonAppearance.Ghost}"> | ||
</nimble-button>` | ||
}) | ||
class TestHostComponent { | ||
@ViewChild('button', { read: NimbleButtonDirective }) public directive: NimbleButtonDirective; | ||
@ViewChild('button', { read: ElementRef }) public elementRef: ElementRef<Button>; | ||
} | ||
|
||
let fixture: ComponentFixture<TestHostComponent>; | ||
let directive: NimbleButtonDirective; | ||
let nativeElement: Button; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
declarations: [TestHostComponent], | ||
imports: [NimbleButtonModule] | ||
}).compileComponents(); | ||
fixture = TestBed.createComponent(TestHostComponent); | ||
fixture.detectChanges(); | ||
directive = fixture.componentInstance.directive; | ||
nativeElement = fixture.componentInstance.elementRef.nativeElement; | ||
}); | ||
|
||
it('will use template string values for disabled', () => { | ||
expect(directive.disabled).toBeTrue(); | ||
expect(nativeElement.disabled).toBeTrue(); | ||
}); | ||
|
||
it('will use template string values for appearance', () => { | ||
expect(directive.appearance).toBe(ButtonAppearance.Ghost); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Ghost); | ||
}); | ||
}); | ||
|
||
describe('with property bound values', () => { | ||
@Component({ | ||
template: ` | ||
<nimble-button #button | ||
[disabled]="disabled" | ||
[appearance]="appearance"> | ||
</nimble-button> | ||
` | ||
}) | ||
class TestHostComponent { | ||
@ViewChild('button', { read: NimbleButtonDirective }) public directive: NimbleButtonDirective; | ||
@ViewChild('button', { read: ElementRef }) public elementRef: ElementRef<Button>; | ||
public disabled = false; | ||
public appearance = ButtonAppearance.Outline; | ||
} | ||
|
||
let fixture: ComponentFixture<TestHostComponent>; | ||
let directive: NimbleButtonDirective; | ||
let nativeElement: Button; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
declarations: [TestHostComponent], | ||
imports: [NimbleButtonModule] | ||
}).compileComponents(); | ||
fixture = TestBed.createComponent(TestHostComponent); | ||
fixture.detectChanges(); | ||
directive = fixture.componentInstance.directive; | ||
nativeElement = fixture.componentInstance.elementRef.nativeElement; | ||
}); | ||
|
||
it('can be configured with property binding for disabled', async () => { | ||
expect(directive.disabled).toBeFalse(); | ||
expect(nativeElement.disabled).toBeFalse(); | ||
|
||
fixture.componentInstance.disabled = true; | ||
fixture.detectChanges(); | ||
|
||
expect(directive.disabled).toBeTrue(); | ||
expect(nativeElement.disabled).toBeTrue(); | ||
}); | ||
|
||
it('can be configured with property binding for appearance', async () => { | ||
expect(directive.appearance).toBe(ButtonAppearance.Outline); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Outline); | ||
|
||
fixture.componentInstance.appearance = ButtonAppearance.Ghost; | ||
fixture.detectChanges(); | ||
|
||
expect(directive.appearance).toBe(ButtonAppearance.Ghost); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Ghost); | ||
}); | ||
}); | ||
|
||
it('custom element is defined', () => { | ||
expect(customElements.get('nimble-button')).not.toBeUndefined(); | ||
describe('with attribute bound values', () => { | ||
@Component({ | ||
template: ` | ||
<nimble-button #button | ||
[attr.disabled]="disabled" | ||
[attr.appearance]="appearance"> | ||
</nimble-button> | ||
` | ||
}) | ||
class TestHostComponent { | ||
@ViewChild('button', { read: NimbleButtonDirective }) public directive: NimbleButtonDirective; | ||
@ViewChild('button', { read: ElementRef }) public elementRef: ElementRef<Button>; | ||
public disabled: BooleanAttribute = null; | ||
public appearance: ButtonAppearance = ButtonAppearance.Outline; | ||
} | ||
|
||
let fixture: ComponentFixture<TestHostComponent>; | ||
let directive: NimbleButtonDirective; | ||
let nativeElement: Button; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
declarations: [TestHostComponent], | ||
imports: [NimbleButtonModule] | ||
}).compileComponents(); | ||
fixture = TestBed.createComponent(TestHostComponent); | ||
fixture.detectChanges(); | ||
directive = fixture.componentInstance.directive; | ||
nativeElement = fixture.componentInstance.elementRef.nativeElement; | ||
}); | ||
|
||
it('can be configured with attribute binding for disabled', () => { | ||
expect(directive.disabled).toBeFalse(); | ||
expect(nativeElement.disabled).toBeFalse(); | ||
|
||
fixture.componentInstance.disabled = ''; | ||
fixture.detectChanges(); | ||
|
||
expect(directive.disabled).toBeTrue(); | ||
expect(nativeElement.disabled).toBeTrue(); | ||
}); | ||
|
||
it('can be configured with attribute binding for appearance', () => { | ||
expect(directive.appearance).toBe(ButtonAppearance.Outline); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Outline); | ||
|
||
fixture.componentInstance.appearance = ButtonAppearance.Ghost; | ||
fixture.detectChanges(); | ||
|
||
expect(directive.appearance).toBe(ButtonAppearance.Ghost); | ||
expect(nativeElement.appearance).toBe(ButtonAppearance.Ghost); | ||
}); | ||
}); | ||
}); |
3 changes: 0 additions & 3 deletions
3
angular-workspace/projects/ni/nimble-angular/src/directives/checkbox/index.ts
This file was deleted.
Oops, something went wrong.
3 changes: 0 additions & 3 deletions
3
angular-workspace/projects/ni/nimble-angular/src/directives/control-value-accessor/index.ts
This file was deleted.
Oops, something went wrong.
11 changes: 0 additions & 11 deletions
11
...ble-angular/src/directives/control-value-accessor/nimble-control-value-accessor.module.ts
This file was deleted.
Oops, something went wrong.
2 changes: 0 additions & 2 deletions
2
angular-workspace/projects/ni/nimble-angular/src/directives/drawer/index.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.