Im creating a unit test for my confirmation modal that uses MatDialog. my first test is a basic test that the component should be created. here is my code for the spec file.
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { PocitConfirmationModalComponent } from './confirmation-modal.component';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material';
import { CommonModule } from '#angular/common';
import { PortalModule } from '#angular/cdk/portal';
import { MaterialModule } from 'src/app/core/material/material.module';
import { BrowserDynamicTestingModule } from '#angular/platform-browser-dynamic/testing';
class MatDialogRefStub {
close(param: string) {}
}
describe('PocitConfirmationModalComponent', () => {
let component: PocitConfirmationModalComponent;
let fixture: ComponentFixture<PocitConfirmationModalComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
MaterialModule,
PortalModule
],
declarations: [PocitConfirmationModalComponent],
providers: [
{ provide: MatDialogRef, useClass: MatDialogRefStub },
{ provide: MAT_DIALOG_DATA, useValue: {} },
]
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [ PocitConfirmationModalComponent ],
}
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PocitConfirmationModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
this is the component file that I want to test.
import { Component, OnInit, Inject, ViewEncapsulation } from '#angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material';
import { ComponentPortal } from '#angular/cdk/portal';
#Component({
selector: 'pocit-confirmation-modal',
templateUrl: './confirmation-modal.component.html',
styleUrls: ['./confirmation-modal.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class PocitConfirmationModalComponent implements OnInit {
portal: ComponentPortal<any>;
constructor(
public dialogRef: MatDialogRef<PocitConfirmationModalComponent>,
#Inject(MAT_DIALOG_DATA) public data: any,
) {
this.portal = new ComponentPortal(this.data.component);
}
ngOnInit() {
}
action(type: string) {
this.dialogRef.close(type);
}
}
after all of these I got this error after running the test.
Error: No component factory found for undefined. Did you add it to #NgModule.entryComponents?
I already added it to entryComponents but still got this error.
//your public data:any model, eg:
const model: any = {
ActionButton: 'Delete',
SupportingText: 'Are you sure?',
};
then in providers:
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: model
}
]
Related
This is my code in angular, the functionality is working all fine but the test cases are getting failed. Please tell me what i am doing wrong in the code?
The error I am getting
HeadlessChrome 83.0.4103 (Windows 10.0.0) AboutComponent should create FAILED
TypeError: Cannot read property 'getAboutInfo' of undefined
at **
at AboutComponent.ngOnInit (http://localhost:9876/karma_webpack/src/app/about/about.component.ts:44:28)
at callHook (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:3937:1)
at callHooks (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:3901:1)
at executeInitAndCheckHooks (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:3842:1)
at refreshView (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11795:1)
at renderComponentOrTemplate (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:11903:1)
at tickRootContext (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:13379:1)
at detectChangesInRootView (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:13413:1)
at RootViewRef.detectChanges (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/core.js:15093:22)
at ComponentFixture._tick (http://localhost:9876/karma_webpack/node_modules/#angular/core/ivy_ngcc/fesm2015/testing.js:323:1)
import { async, ComponentFixture, TestBed} from '#angular/core/testing';
import { AboutComponent } from './about.component';
import { AboutService } from './about.service';
import { HttpClientTestingModule, HttpTestingController } from '#angular/common/http/testing';
import { Observable, of } from 'rxjs';
import { I18nService } from 'src/utils/i18n.service';
import { MatDialogRef, MAT_DIALOG_DATA } from '#angular/material/dialog';
import { AppModule } from './../app.module';
describe('AboutComponent', () => {
let component: AboutComponent;
let fixture: ComponentFixture<AboutComponent>;
let dialogSpy: jasmine.Spy;
let app: any;
const mockDialogRef = {
close: jasmine.createSpy('close')
};
let service: any;
const data = '20/04/2019';
let getAboutInfoSpy: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AboutComponent],
imports: [HttpClientTestingModule , AppModule],
providers: [{ provide: AboutService, useValue: service },
I18nService,
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: mockDialogRef}]
}).compileComponents();
}));
beforeEach(async () => {
fixture = TestBed.createComponent(AboutComponent);
component = fixture.componentInstance;
await fixture.whenStable();
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('infoList should be empty array', () => {
expect(app['dataList'].length).toBe(0);
});
it('when OnInit invoked through service data will return to infoList ', async(() => {
service = fixture.debugElement.injector.get(AboutService);
spyOn(service, 'getAboutInfo').and.returnValue(of(data));
app.ngOnInit();
expect(app['dataList'].length).toBe(3);
}));
it('onCancel should close the dialog', async( () => {
component.closePopup();
expect(mockDialogRef.close).toHaveBeenCalled();
}));
});
import { Component, OnInit, Inject } from '#angular/core';
import { AboutService } from './about.service';
import { Subscription } from 'rxjs';
import { MatDialogRef} from '#angular/material/dialog';
import { I18nService } from 'src/utils/i18n.service';
#Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss']
})
export class AboutComponent implements OnInit {
private aboutServiceSubscription: Subscription;
dataList: any;
locales: any = {};
translator: any;
constructor(
private dialogRef: MatDialogRef<AboutComponent>,
public aboutService: AboutService,
private i18Service: I18nService) {}
ngOnInit() {
this.translator = this.i18Service.getTranslator();
this.translator.translateObject.subscribe((item: any) => {
this.locales = item;
});
this.aboutServiceSubscription = this.aboutService.getAboutInfo().subscribe((data: any) => {
if (data) {
data = data.split('/');
this.dataList = data;
}
});
}
/**
* Closes the poup
* #memberof AboutComponent
*/
closePopup() {
this.dialogRef.close();
}
}
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class AboutService {
constructor(private http: HttpClient) {
}
getAboutInfo() {
return this.http.get('/assets/aboutInfo.txt', {responseType: 'text'})
}
}
The error message indicates that you did not mock AboutService correctly, you passed an undefined to useValue, so the AboutService obtained through the Injector is undefined. You can't use spyOn to a undefined value.
Here is a working example, with irrelevant code removed:
about.component.ts:
import { Component, OnInit } from '#angular/core';
import { AboutService } from './about.service';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-about',
})
export class AboutComponent implements OnInit {
private aboutServiceSubscription: Subscription;
dataList: any;
locales: any = {};
translator: any;
constructor(public aboutService: AboutService) {}
ngOnInit() {
this.aboutServiceSubscription = this.aboutService
.getAboutInfo()
.subscribe((data: any) => {
if (data) {
data = data.split('/');
this.dataList = data;
}
});
}
}
about.service.ts:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root',
})
export class AboutService {
constructor(private http: HttpClient) {}
getAboutInfo() {
return this.http.get('/assets/aboutInfo.txt', { responseType: 'text' });
}
}
about.component.spec.ts:
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { of } from 'rxjs';
import { AboutComponent } from './about.component';
import { AboutService } from './about.service';
fdescribe('62700708', () => {
let component: AboutComponent;
let fixture: ComponentFixture<AboutComponent>;
let aboutServiceSpy: jasmine.SpyObj<AboutService>;
const data = '20/04/2019';
beforeEach(() => {
aboutServiceSpy = jasmine.createSpyObj('AboutService', ['getAboutInfo']);
aboutServiceSpy.getAboutInfo.and.returnValue(of(data));
TestBed.configureTestingModule({
declarations: [AboutComponent],
imports: [HttpClientTestingModule],
providers: [{ provide: AboutService, useValue: aboutServiceSpy }],
}).compileComponents();
});
beforeEach(async () => {
fixture = TestBed.createComponent(AboutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('when OnInit invoked through service data will return to infoList ', () => {
expect(aboutServiceSpy.getAboutInfo).toHaveBeenCalledTimes(1);
expect(component.dataList).toEqual(['20', '04', '2019']);
});
});
unit test result:
This is my Angular App. My app will get data from API (temporarily in JSON file) and show in many another sibling component. So I decide to create a category.service.ts that I get and store data in. I using APP_INITIALIZER to run this service first when my app started. But there is a problem that: This service is running first, AppComponent runs before service get data done. So my view have empty of data.
If I click button routing to this component, everything run perfect. But when I go to this component by url path or F5(refresh page), nothing is shown
category.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class CategoryService {
DATA_CATEGORIES = 'assets/categories.json';
private _categories = [];
constructor(private http: HttpClient) {
}
get categories() {
return this._categories;
}
Init(): Promise<any> {
return new Promise<void>(resolve => {
this.http.get(this.DATA_CATEGORIES).subscribe(data => {
this._categories = Array.from(Object.keys(data), k => data[k]);
console.log("load data...");
});
resolve();
});
}
}
app.module.ts
export function initializeCategoryService(catService: CategoryService) {
return (): Promise<any> => {
return catService.Init();
}
}
#NgModule({
declarations: [
AppComponent,
HomeComponent,
StoriesFilterPipe,
ViewStoryComponent,
ViewCatComponent,
FrontEndComponent,
SearchComponent,
BackEndComponent,
CrudStoryFormComponent,
CrudStoryComponent,
JwPaginationComponent,
CrudCatComponent,
CrudCatFormComponent,
CrudCatSearchResultComponent,
CatListComponent
],
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
],
providers: [
StoryService,
CategoryService,
{
provide: APP_INITIALIZER, useFactory: initializeCategoryService, deps: [CategoryService], multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
What I will suggest is to use Observable
like in your category service
import { Observable, Subject } from 'rxjs';
export class CategoryService {
private loadDataSub = new Subject<any>();
loadDataObservable$ = this.loadDataSub.asObservable();
emitLoadDataSuccess() {
this.loadDataSub.next();
}
Init(): Promise<any> {
return new Promise<void>(resolve => {
this.http.get(this.DATA_CATEGORIES).subscribe(data => {
this._categories = Array.from(Object.keys(data), k => data[k]);
console.log("load data...");
this.emitLoadDataSuccess(); // here we are emitting event
});
resolve();
});
}
}
And In your component
export class AppComponent implements OnInit {
constructor(private categoryService: CategoryService) {
this.categoryService.loadDataObservable$.subscribe(() => {
// here you can get data, this will only trigger when data is loaded from API
});
}
}
This is common case - i.e. you shows page while data is not avaliable yet - at slow and bad connections for instance, and it can do even more - connection was broken and data was nto recieved.
So, your page should be able to show not only data recieved, but also two another states: loading and error.
(So the advise is "add loader").
// data.service.ts
import { Injectable } from "#angular/core";
import { HttpClient, HttpClientModule } from "#angular/common/http";
#Injectable()
export class DataService {
private _categories = [];
constructor(private http: HttpClient) {}
get categories() {
return this._categories;
}
getData(): Promise<any[]> {
return new Promise<any[]>(resolve => {
this.http.get('https://api.myjson.com/bins/18qku4').subscribe(data => {
this._categories = Array.from(Object.keys(data), k => data[k]);
console.log("load data...");
resolve(this._categories);
});
});
}
}
// app.module.ts
import { NgModule, APP_INITIALIZER } from "#angular/core";
import { BrowserModule } from "#angular/platform-browser";
import { FormsModule } from "#angular/forms";
import { RouterModule } from "#angular/router";
import { ListDataComponent } from "./list-data/list-data.component";
import { AppComponent } from "./app.component";
import { DataService } from "./data.service";
import { HttpClientModule } from "#angular/common/http";
import {DetailComponent} from './detail/detail.component'
#NgModule({
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot([
{ path: "", component: ListDataComponent },
{ path: "detail", component: DetailComponent }
])
],
declarations: [AppComponent, ListDataComponent,DetailComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
//list-data.component.ts
import { Component, OnInit } from "#angular/core";
import { DataService } from "../data.service";
#Component({
selector: "app-list-data",
templateUrl: "./list-data.component.html",
styleUrls: ["./list-data.component.css"],
providers: [DataService],
})
export class ListDataComponent implements OnInit {
categories = [];
constructor(service: DataService) {
service.getData().then(data => {
debugger;
this.categories = data;
});
}
ngOnInit() {}
}
There are alternatives to resolve this issue:
One is you can use a loader which you can display until the service call finishes.
Second is you can use *ngIf="categories?.length" which will keep your component hides until your service call finishes.
I hope it will resolve your issue.
I want to store array of items in a service (called cart service) and show it in component (cart.component.ts).
but the items are chosen each alone by the component bgview.component.ts that is a children of component single.component.ts that receive the items through http requests.
when i open the cart component i receive an empty array.
can i store these items in the service without losing them when navigating.
i've tried the following steps:
The component that i want to send items from :
export class BgviewComponent implements OnInit {
#Input() item: Item;
constructor(private cartservice: CartService) { }
ngOnInit() {
}
onAddToCart(){
this.cartservice.items.push(this.item);
}
}
the service :
#Injectable({
providedIn: 'root',
})
export class CartService {
public items: Item[]=[];
}
the receiving component:
export class CartComponent implements OnInit {
items: Item[]=[];
constructor(private cartservice:CartService) {
}
ngOnInit() {
this.items = this.cartservice.items;
console.log(this.items) //gives empty array
}
}
routing way:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { HomeModule } from './pages/home/home.module';
import { ContactusModule } from './pages/contactus/contactus.module';
import { SingleModule } from './pages/single/single.module';
import { LoginModule } from './pages/login/login.module';
import { SearchpModule } from './pages/searchp/searchp.module';
import { AdminModule } from './pages/admin/admin.module';
import { FindpModule } from './pages/findp/findp.module';
import { AuthGuardService } from './services/auth.service';
import { CartModule } from './pages/cart/cart.module';
const routes: Routes = [
{
path: '',
loadChildren: () => HomeModule,
},
{
path: 'contact',
loadChildren: () => ContactusModule,
},
{
path: 'single/:id',
loadChildren: () => SingleModule,
},
{
path: 'login',
loadChildren: () => LoginModule,
},
{
path: 'searchp/:id',
loadChildren: () => SearchpModule,
},
{
path: 'login/admin',
loadChildren: () => AdminModule, canActivate: [AuthGuardService]
},
{
path: 'cart',
loadChildren: () => CartModule,
},
{
path: 'findp/:str',
loadChildren: () => FindpModule,
},
]
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
cart module:
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { CartComponent } from './cart.component';
import { CartRoutingModule } from './cart-routing.module';
import { CartService } from 'src/app/services/cart.service';
#NgModule({
declarations: [ CartComponent],
imports: [
CommonModule,
CartRoutingModule,
],
providers:[
]
})
export class CartModule { }
single module:
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { SingleComponent } from './single.component';
import { SharedModule } from 'src/app/shared/shared.module';
import { SingleRoutingModule } from './single-routing.module';
import { BgviewComponent } from './bgview/bgview.component';
import { HttpService } from 'src/app/services/http.service';
import { CartService } from 'src/app/services/cart.service';
#NgModule({
declarations: [SingleComponent, BgviewComponent],
imports: [
CommonModule,
SharedModule,
SingleRoutingModule,
],
providers:[
HttpService,
]
})
export class SingleModule { }
please help me i'm stuck here.
Also make sure that onAddToCart method is called.
You can simply add some log message inside method to check it.
Make sure that the onAddToCArt method is called.
I want to test a function with condition that comes from a service in ngOnInit. I tried many way but no success. i have all kinds of mistakes.
my component
export class MainSectionComponent implements OnInit {
propertiesFrDb: PropertyPost[];
constructor(
private getPropertiesFrDbService: GetPropertiesFrDbService,
private propertyWarehouseService: PropertyWarehouseService,
private router: Router,
config: NgbCarouselConfig,
private userService: UserService,
private sharedFunctionService: SharedFunctionService,
private returnResponseAfterUserLoginService: ReturnResponseAfterUserLoginService,
private localStorageService: LocalStorageServiceService,
private dialogLoginService: DialogLoginService,
#Inject(PLATFORM_ID) private platformId: Object
) {
// this.isBrowser = isPlatformBrowser(platformIds);
}
ngOnInit() {
this.getPropertiesFrDb();
}
getPropertiesFrDb() {
if (this.propertyWarehouseService.currentValuesProperties) {
this.propertyWarehouseService.getPropertyHome$.subscribe(
prop => {
console.log(prop);
return this.propertiesFrDb = prop
}
)
} else {
this.getPropertiesFrDbService.getHomeProperties()
.subscribe(property => {
// console.log(property['results']);
this.propertyWarehouseService.setPropertiesHome(property['results'])
return this.propertiesFrDb = property['results']
},
)
}
}
I want to test this.getPropertiesFrDb() in ngOnInit
i would like to test the case with this.propertyWarehouseService.currentValuesProperties !== ''and checked that this.getPropertiesFrDbService.getHomeProperties() is called and checked the value of propertiesFrDb
and my spec.ts file
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '#angular/core/testing';
import { MainSectionComponent } from './home-properties.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '#angular/core';
import { MaterialModule } from 'src/app/material/material.module';
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { RouterTestingModule } from '#angular/router/testing';
import { GetPropertiesFrDbService } from 'src/app/services/getPropertiesFromDb/get-properties-fr-db.service';
import { MOCKPROPERTIES, MockPropertyWarehouseService } from 'src/app/mocks/property-post';
import { NgxPaginationModule, PaginatePipe } from 'ngx-pagination';
import { PropertyWarehouseService } from 'src/app/services/propertyWarehouse/property-warehouse.service';
import { BsDropdownModule } from 'ngx-bootstrap';
import { NgbModule } from '#ng-bootstrap/ng-bootstrap';
import { StorageServiceModule } from 'angular-webstorage-service';
import { of } from 'rxjs/internal/observable/of';
fdescribe('MainSectionComponent', () => {
let component: MainSectionComponent;
let fixture: ComponentFixture<MainSectionComponent>;
const PROPERTYMODEL = MOCKPROPERTIES;
const spyPropertyWarehouseService = jasmine.createSpyObj('spypropertyWarehouseService', ['currentValuesProperties', 'getPropertyHome$']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
NgxPaginationModule,
BsDropdownModule.forRoot(),
NgbModule,
StorageServiceModule,
],
declarations: [
MainSectionComponent,
],
providers: [
{
provide: PropertyWarehouseService,
useValue: spyPropertyWarehouseService
}
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MainSectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', (() => {
// console.log('properties', component);
expect(component).toBeTruthy();
}));
it('Should get propertiesFrDb from GetPropertiesFrDbService', async(() => {
spyPropertyWarehouseService.currentValuesProperties.and.returnValue(PROPERTYMODEL);
spyPropertyWarehouseService.getPropertyHome$.and.returnValue(of(PROPERTYMODEL));
expect(component.propertiesFrDb).toBe(PROPERTYMODEL);
console.log('spy',spyPropertyWarehouseService);
}));
});
Try creating a stub as below:
export class PropertyWarehouseServiceStub{
currentValuesProperties = '';
getPropertyHome$ = new BaheviorSubject<any>('someObj');
setPropertiesHome(){ }
}
export class GetPropertiesFrDbServiceStub{
getHomeProperties(){
return of({results: 'val'})
}
}
in component file make the service public in constructor so that we can override some of its behaviors:
constructor(...,
public propertyWarehouseService: PropertyWarehouseService,
public getPropertiesFrDbService: GetPropertiesFrDbService,
....)
and in spec file as :
providers: [
{
provide: PropertyWarehouseService,
useClass: PropertyWarehouseServiceStub
},{
provide: GetPropertiesFrDbService,
useClass: GetPropertiesFrDbServiceStub
}
],
......
....
..
it('should call getPropertiesFrDb() in ngOnInit',()=>{
spyOn(component,'getPropertiesFrDb').and.callThrough();
component.ngOnInit();
expect(component.getPropertiesFrDb).toHaveBeenCalled();
})
it('inside getPropertiesFrDb() should call getPropertiesFrDbService.getHomeProperties() when "propertyWarehouseService.currentValuesProperties" is empty,()=>{
spyOn(component.getPropertiesFrDbService,'getHomeProperties').and.callThrough();
spyOn(component.propertyWarehouseService,'setPropertiesHome').and.callThrough();
component.getPropertiesFrDb();
expect(component.getPropertiesFrDbService.getHomeProperties).toHaveBeenCalled();
expect(component.propertyWarehouseService.setPropertiesHome).toHaveBeenCalledWith('val');
expect(component.propertiesFrDb).toBe('someObj');
})
it('inside getPropertiesFrDb() should not call getPropertiesFrDbService.getHomeProperties() when "propertyWarehouseService.currentValuesProperties" is NOT empty,()=>{
component.propertyWarehouseService.currentValuesProperties = 'Not empty';
spyOn(component.getPropertiesFrDbService,'getHomeProperties').and.callThrough();
spyOn(component.propertyWarehouseService,'setPropertiesHome').and.callThrough();
component.getPropertiesFrDb();
expect(component.getPropertiesFrDbService.getHomeProperties).not.toHaveBeenCalled();
expect(component.propertyWarehouseService.setPropertiesHome).not.toHaveBeenCalledWith('val');
expect(component.propertiesFrDb).toBe('val');
})
You can refer to this intro article written by me on Karma-jasmine which contains more article references for several test use cases.
This one is very much similar to what you are looking for. I am planning to write some more articles, in case you wanna follow.
Also, I have no clue on why you are using return as below inside getPropertiesFrDb()
return this.propertiesFrDb = prop
which is of no use because no value of this function has been assigned to any variable inside ngOnInit.
I need to wrap a mat-slide-toggle component on my own, I wrote:
mytoggle.component.ts
import { Component, OnInit, Input, forwardRef, ViewChild, ElementRef } from '#angular/core';
import {MatSlideToggle, MatSlideToggleChange} from '#angular/material/slide-toggle';
import { NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'ng7-common-ng7-slide',
templateUrl: 'ng7-slide.component.html',
styles: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => Ng7SlideComponent),
multi: true
}
]
})
export class Ng7SlideComponent extends MatSlideToggle {
}
And mytoggle.component.html:
<mat-slide-toggle
[checked]="checked"
[disabled]="disabled">
{{label}}
</mat-slide-toggle>
and in my app I'm using like this:
app.component.html
<form class="example-form" [formGroup]="formGroup" (ngSubmit)="onFormSubmit(formGroup.value)" ngNativeValidate>
<!-- THIS WORKS <mat-slide-toggle formControlName="slideToggle">Enable Wifi</mat-slide-toggle> -->
<ng7-common-ng7-slide formControlName="slideToggle" label="test me!">
</ng7-common-ng7-slide>
<button mat-rasied-button type="submit">Save Settings</button>
</form>
app.component.ts
import { Component } from '#angular/core';
import { FormGroup, FormControl, FormBuilder } from '#angular/forms';
import { MatSlideToggleChange } from '#angular/material/slide-toggle';
#Component({
selector: 'home-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
formGroup: FormGroup;
constructor(formBuilder: FormBuilder) {
this.formGroup = formBuilder.group({
slideToggle: false
});
}
onFormSubmit(formValue: any) {
alert(JSON.stringify(formValue, null, 2));
}
}
So the formValue on onFormSubmit method always alerts "slideToggle":false no matter is it is checked or not, when I use mat-slide-toggle it alerts true or false according with the toggle state correctly.
Are there anything else to do? I just need to extend the component and all event.
After some research I got something that works well..
I imported an abstract class that implements the basic value acessors methods:
https://stackoverflow.com/a/45480791/2161180
import { ControlValueAccessor } from '#angular/forms';
export abstract class AbstractValueAccessor implements ControlValueAccessor {
innerValue: any = '';
get value(): any { return this.innerValue; }
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChange(v);
}
}
writeValue(value: any) {
this.innerValue = value;
this.onChange(value);
}
onChange = (_) => {};
onTouched = () => {};
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
Then I created my component extending it:
import { NG_VALUE_ACCESSOR } from '#angular/forms';
import { Component, Input, forwardRef } from '#angular/core';
import { AbstractValueAccessor } from '../abstract.component';
#Component({
selector: 'my-switch',
templateUrl: './my-switch.component.html',
styleUrls: ['./my-switch.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MySwitchComponent)
}
]
})
export class MySwitchComponent extends AbstractValueAccessor {
#Input() label: string;
#Input() checked: boolean;
#Input() disabled: boolean;
}
html:
<mat-slide-toggle
[(ngModel)]="value"
[checked]="checked"
[disabled]="disabled">
{{label}}
</mat-slide-toggle>
module:
import { FormsModule } from '#angular/forms';
import { MatSlideToggleModule } from '#angular/material';
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { MySwitchComponent } from './my-switch.component';
#NgModule({
declarations: [MySwitchComponent],
imports: [
CommonModule,
MatSlideToggleModule,
FormsModule
],
exports: [
MySwitchComponent
]
})
export class MySwitchModule { }
And to use it:
<form [formGroup]="fb">
<my-switch formControlName="inputSwitch" label="Toggle-me!"></my-switch>
<strong> Value: </strong> {{inputSwitch}}
</form>
or
<my-switch [(ngModel)]="inputSwitchNgModel" label="Toggle-me!"></my-switch>
<strong> Value: </strong> {{inputSwitchNgModel}}