From 7417fdcb14da08deed26909b65c3b03fa9d767ff Mon Sep 17 00:00:00 2001 From: Dick Smith Date: Mon, 1 May 2017 05:28:22 -0400 Subject: [PATCH] Angular Forms Directive (#88) This change allows for the use of `ngModel` and `formControlName` when using the drop-downs with Angular. There should be no breaking changes to previous functionality or usage for either Angular or Vanilla NativeScript. Build scripts updated. Compatible with Angular AoT and Webpack. Based on `nativescript-angular/value-accessors/selectedIndex-value-accessor.ts` and `nativescript-angular/nativescript-angular/forms.ts` with some changes to conform to project linting. --- README.md | 68 +++++++++++++---- angular/index.ts | 78 ++++++++++++++++++++ demo-ng/app/app.module.ts | 2 + demo-ng/app/dropdown/dropdown.component.html | 36 ++++++--- demo-ng/app/dropdown/dropdown.component.ts | 19 +++-- demo-ng/app/main.ts | 4 - demo-ng/package.json | 18 ++--- drop-down.android.ts | 12 +-- gruntfile.js | 4 + package.json | 4 + tsconfig.aot.json | 30 ++++++++ 11 files changed, 222 insertions(+), 53 deletions(-) create mode 100644 angular/index.ts create mode 100644 tsconfig.aot.json diff --git a/README.md b/README.md index 1405aa7..05f435e 100644 --- a/README.md +++ b/README.md @@ -106,23 +106,48 @@ export function dropDownSelectedIndexChanged(args: SelectedIndexChangedEventData } ``` -## Angular 2 Example +## Angular +##### Migration to 3.0+ + +- Remove: +```typescript +registerElement("DropDown", () => require("nativescript-drop-down/drop-down").DropDown);` +``` +- Import `DropDownModule` in `NgModule`: +```typescript +import { DropDownModule } from “nativescript-drop-down/angular”; +… +@NgModule({ + … + imports: [ + … + DropDownModule, + … + ], + … +}) +``` + +##### Example Usage ```TypeScript // main.ts -import { platformNativeScriptDynamic, NativeScriptModule } from "nativescript-angular/platform"; import { NgModule } from "@angular/core"; +import { NativeScriptModule } from "nativescript-angular/nativescript.module"; +import { platformNativeScriptDynamic } from "nativescript-angular/platform"; +import { DropDownModule } from "nativescript-drop-down/angular"; import { AppComponent } from "./app.component"; -import { registerElement } from "nativescript-angular/element-registry"; - -registerElement("DropDown", () => require("nativescript-drop-down/drop-down").DropDown); @NgModule({ - declarations: [AppComponent], - bootstrap: [AppComponent], - imports: [NativeScriptModule], + declarations: [ AppComponent ], + bootstrap: [ AppComponent ], + imports: [ + NativeScriptModule, + DropDownModule, + ], }) -class AppComponentModule {} +class AppComponentModule { +} platformNativeScriptDynamic().bootstrapModule(AppComponentModule); ``` @@ -130,13 +155,24 @@ platformNativeScriptDynamic().bootstrapModule(AppComponentModule); ```HTML - - - - - + + + + ``` diff --git a/angular/index.ts b/angular/index.ts new file mode 100644 index 0000000..ad0c070 --- /dev/null +++ b/angular/index.ts @@ -0,0 +1,78 @@ +import { AfterViewInit, Directive, ElementRef, HostListener, Inject, NgModule, forwardRef } from "@angular/core"; +import { FormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { BaseValueAccessor, registerElement } from "nativescript-angular"; +import { convertToInt } from "nativescript-angular/common/utils"; +import { View } from "tns-core-modules/ui/core/view"; + +registerElement("DropDown", () => require("../drop-down").DropDown); + +const SELECTED_INDEX_VALUE_ACCESSOR = {provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectedIndexValueAccessor), multi: true}; + +export type SelectableView = {selectedIndex: number} & View; + +/** + * The accessor for setting a selectedIndex and listening to changes that is used by the + * {@link NgModel} directives. + * + * ### Example + * ``` + * + * ``` + */ +@Directive({ + // tslint:disable-next-line:max-line-length directive-selector + selector: "DropDown[ngModel], DropDown[formControlName], dropDown[ngModel], dropDown[formControlName], drop-down[ngModel], drop-down[formControlName]", + providers: [SELECTED_INDEX_VALUE_ACCESSOR] +}) +export class SelectedIndexValueAccessor extends BaseValueAccessor implements AfterViewInit { // tslint:disable-line:max-line-length directive-class-suffix + + private _normalizedValue: number; + private viewInitialized: boolean; + + constructor(@Inject(ElementRef) elementRef: ElementRef) { + super(elementRef.nativeElement); + } + + @HostListener("selectedIndexChange", ["$event"]) + public selectedIndexChangeListener(event: any) { + this.onChange(event.value); + } + + // tslint:disable-next-line:no-empty + public onTouched = () => { }; + + public writeValue(value: any): void { + if (value === undefined || value === null || value === "") { + this._normalizedValue = null; + } + else { + this._normalizedValue = convertToInt(value); + } + + if (this.viewInitialized) { + this.view.selectedIndex = this._normalizedValue; + } + } + + public ngAfterViewInit() { + this.viewInitialized = true; + this.view.selectedIndex = this._normalizedValue; + } + + public registerOnTouched(fn: () => void): void { this.onTouched = fn; } +} + +@NgModule({ + declarations: [ SelectedIndexValueAccessor ], + providers: [], + imports: [ + FormsModule + ], + exports: [ + FormsModule, + SelectedIndexValueAccessor + ] +}) +export class DropDownModule { +} diff --git a/demo-ng/app/app.module.ts b/demo-ng/app/app.module.ts index 565ad54..f43976a 100644 --- a/demo-ng/app/app.module.ts +++ b/demo-ng/app/app.module.ts @@ -4,6 +4,7 @@ import { AppRoutingModule } from "./app.routing"; import { AppComponent } from "./app.component"; import { DropDownComponent } from "./dropdown/dropdown.component"; +import { DropDownModule } from 'nativescript-drop-down/angular'; @NgModule({ bootstrap: [ @@ -11,6 +12,7 @@ import { DropDownComponent } from "./dropdown/dropdown.component"; ], imports: [ NativeScriptModule, + DropDownModule, AppRoutingModule ], declarations: [ diff --git a/demo-ng/app/dropdown/dropdown.component.html b/demo-ng/app/dropdown/dropdown.component.html index 35489d7..3c395c7 100644 --- a/demo-ng/app/dropdown/dropdown.component.html +++ b/demo-ng/app/dropdown/dropdown.component.html @@ -1,13 +1,29 @@ - - + - - - - - - + + + + + diff --git a/demo-ng/app/dropdown/dropdown.component.ts b/demo-ng/app/dropdown/dropdown.component.ts index 16520c5..bb6bb57 100644 --- a/demo-ng/app/dropdown/dropdown.component.ts +++ b/demo-ng/app/dropdown/dropdown.component.ts @@ -8,27 +8,30 @@ import { SelectedIndexChangedEventData, ValueList } from "nativescript-drop-down }) export class DropDownComponent implements OnInit { public selectedIndex: number = null; - public hint = "My Hint"; + public hint = "My Hint"; public items: ValueList; - public cssClass: string = "default"; + public cssClass: string = "default"; public ngOnInit() { this.items = new ValueList(); - for (let loop = 0; loop < 200; loop++) { - this.items.push({ value: `I${loop}`, display: `Item ${loop}`}); + for ( let loop = 0; loop < 200; loop++ ) { + this.items.push({ + value: `I${loop}`, + display: `Item ${loop}`, + }); } } public onchange(args: SelectedIndexChangedEventData) { - console.log(`Drop Down selected index changed from ${args.oldIndex} to ${args.newIndex}. New value is '${this.items.getValue(args.newIndex)}'`); - this.selectedIndex = args.newIndex; + console.log(`Drop Down selected index changed from ${args.oldIndex} to ${args.newIndex}. New value is "${this.items.getValue( + args.newIndex)}"`); } public onopen() { console.log("Drop Down opened."); } - + public changeStyles() { this.cssClass = "changed-styles"; - } + } } diff --git a/demo-ng/app/main.ts b/demo-ng/app/main.ts index 22f21ea..1057aef 100644 --- a/demo-ng/app/main.ts +++ b/demo-ng/app/main.ts @@ -1,10 +1,6 @@ // this import should be first in order to load some required settings (like globals and reflect-metadata) import { platformNativeScriptDynamic } from "nativescript-angular/platform"; -import { registerElement } from "nativescript-angular/element-registry"; - import { AppModule } from "./app.module"; -registerElement("DropDown", () => require("nativescript-drop-down/drop-down").DropDown); - platformNativeScriptDynamic().bootstrapModule(AppModule); diff --git a/demo-ng/package.json b/demo-ng/package.json index b1fab86..63cc3c1 100644 --- a/demo-ng/package.json +++ b/demo-ng/package.json @@ -15,15 +15,15 @@ "debug-ios": "npm uninstall nativescript-drop-down && tns debug ios --emulator" }, "dependencies": { - "@angular/animations": "4.0.0", - "@angular/common": "4.0.0", - "@angular/compiler": "4.0.0", - "@angular/core": "4.0.0", - "@angular/forms": "4.0.0", - "@angular/http": "4.0.0", - "@angular/platform-browser": "4.0.0", - "@angular/platform-browser-dynamic": "4.0.0", - "@angular/router": "4.0.0", + "@angular/animations": "^4.0.3", + "@angular/common": "^4.0.3", + "@angular/compiler": "^4.0.3", + "@angular/core": "^4.0.3", + "@angular/forms": "^4.0.3", + "@angular/http": "^4.0.3", + "@angular/platform-browser": "^4.0.3", + "@angular/platform-browser-dynamic": "^4.0.3", + "@angular/router": "^4.0.3", "nativescript-angular": "rc", "nativescript-drop-down": "file:../bin/dist", "reflect-metadata": "~0.1.8", diff --git a/drop-down.android.ts b/drop-down.android.ts index 3045df2..7bccc7e 100644 --- a/drop-down.android.ts +++ b/drop-down.android.ts @@ -73,12 +73,6 @@ export class DropDown extends DropDownBase { spinner.setOnTouchListener(touchListener); (spinner as any).touchListener = touchListener; - // When used in templates the selectedIndex changed event is fired before the native widget is init. - // So here we must set the inital value (if any) - if (!types.isNullOrUndefined(this.selectedIndex)) { - this.android.setSelection(this.selectedIndex + 1); // +1 for the hint first element - } - return spinner; } @@ -89,6 +83,12 @@ export class DropDown extends DropDownBase { nativeView.adapter.owner = new WeakRef(this); nativeView.itemSelectedListener.owner = new WeakRef(this); nativeView.touchListener.owner = new WeakRef(this); + + // When used in templates the selectedIndex changed event is fired before the native widget is init. + // So here we must set the inital value (if any) + if (!types.isNullOrUndefined(this.selectedIndex)) { + this.android.setSelection(this.selectedIndex + 1); // +1 for the hint first element + } } public disposeNativeView() { diff --git a/gruntfile.js b/gruntfile.js index f058a2b..f626cee 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -47,6 +47,9 @@ tsCompile: { cmd: "node ./node_modules/typescript/bin/tsc --project tsconfig.json --outDir " + localConfig.outDir }, + ngCompile: { + cmd: "node ./node_modules/.bin/ngc --project tsconfig.aot.json --outDir " + localConfig.outDir + }, tslint: { cmd: "node ./node_modules/tslint/bin/tslint --project tsconfig.json" }, @@ -65,6 +68,7 @@ "exec:tslint", "clean:build", "exec:tsCompile", + "exec:ngCompile", "copy" ]); grunt.registerTask("publish", [ diff --git a/package.json b/package.json index 86bbc75..e8d2aa0 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,10 @@ }, "devDependencies": { + "@angular/compiler-cli": "^4.0.3", + "@angular/core": "^4.0.3", + "@angular/forms": "^4.0.3", + "nativescript-angular": "rc", "typescript": "~2.2.2", "tslint": "^4.5.1", "tns-core-modules": "rc", diff --git a/tsconfig.aot.json b/tsconfig.aot.json new file mode 100644 index 0000000..db6f6c6 --- /dev/null +++ b/tsconfig.aot.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "noEmitOnError": true, + "noEmitHelpers": true, + "sourceMap": false, + "removeComments": true, + "declaration": true, + "experimentalDecorators": true, + "target": "es5", + "module": "commonjs", + "outDir": "bin/dist", + "lib": ["es6", "dom", "es2015.iterable"], + "rootDir": ".", + "baseUrl": ".", + "paths": { + "*": [ + "./node_modules/tns-core-modules/*", + "./node_modules/*" + ] + }, + "emitDecoratorMetadata": true, + "moduleResolution": "node" + }, + "files": [ + "angular/index.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true + } +} \ No newline at end of file