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}}
Related
I have a global Errorhandler in which I process Client- and Server-Errors.
To provide a feedback for the user I want to open a modal which returns the error-message.
Therefore I've implemented a modal:
import {Component} from '#angular/core';
import {BsModalRef, BsModalService} from 'ngx-bootstrap';
import {Button} from '../../layout-models/button.model';
#Component({
selector: 'error-modal',
templateUrl: './error-modal.component.html',
styleUrls: ['./error-modal.component.scss']
})
export class ErrorModalComponent {
title: string;
buttonTitle = 'OK';
type: 'error';
button: Button;
protected modalRef: BsModalRef;
constructor(protected modalService: BsModalService) {}
public show(title: string, message: string) {
this.title = title;
this.modalRef = this.modalService.show(
message,
Object.assign({}, { class: `modal-banner ${this.type}`})
);
}
hide() {
if (this.modalRef) {
this.modalRef.hide();
}
}
}
In my Notification-Service:
import {Injectable, NgZone} from '#angular/core';
import { ErrorModalComponent } from '../error-modal.component';
#Injectable({
providedIn: 'root'
})
export class NotificationService {
public errorModalComponent: ErrorModalComponent;
showError(title: string, message: string): void {
this.errorModalComponent.show(title, message);
}
}
Which leads to
Uncaught TypeError: Cannot read property 'show' of undefined
I feel like I am doing some fundamental mistake - the main purpose of this is to have a centralized modal. Is this possible or do I need to use the ModalComponent in every Component in which I want to show the error-handling-modal?
I wouldn't use ngx-modal I would use NgbModal
What yazantahhan means is something like this:
import {Injectable} from "#angular/core";
import {NgbModal, NgbModalRef} from "#ng-bootstrap/ng-bootstrap";
#Injectable()
export class ErrorModalService {
title: string;
buttonTitle = "OK";
type: "error";
protected modalRef: NgbModalRef;
constructor(protected modalService: NgbModal) {}
public show(title: string, message: string) {
this.title = title;
this.modalRef = this.modalService.open(
message
);
}
hide() {
if (this.modalRef) {
this.modalRef.close();
}
}
}
Then inject and use it like this:
import { Component } from "#angular/core";
import {ErrorModalService} from "./ErrorModalService";
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent {
title = "testAngular";
constructor(
private errorModalService: ErrorModalService,
) {}
showError() {
this.errorModalService.show("title", "message");
}
}
Don't forget to provide the service in your module
import { BrowserModule } from "#angular/platform-browser";
import { NgModule } from "#angular/core";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import {ErrorModalService} from "./ErrorModalService";
import {BsModalService} from "ngx-bootstrap/modal";
import {NgbModule} from "#ng-bootstrap/ng-bootstrap";
#NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
],
providers: [
ErrorModalService,
],
bootstrap: [AppComponent],
})
export class AppModule { }
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
}
]
This is my component:
import { Component, OnInit, Input, Output, EventEmitter, ChangeDetectionStrategy, OnDestroy } from '#angular/core';
import { FormBuilder, FormGroup , Validators } from '#angular/forms';
import { Subscription } from 'rxjs';
import { Values } from '../_models/values';
#Component({
selector: 'some',
templateUrl: './my.component.html',
styleUrls: ['./my.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Mycomponent implements OnInit, OnDestroy {
#Input()
values: Array<string>;
#Output()
selectedValues = new EventEmitter<Values>();
private myForm: FormGroup;
#Input()
errorMsg: string;
private selectSubscription: Subscription;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.myForm = this.fb.group({
'selectAll': [false],
'values': [this.values, Validators.required]
});
this.selectSubscription = this.myForm.get('selectAll').valueChanges.subscribe(value => {
this.changeSelection(value);
});
}
submit(): void {
console.log('called');
console.log(this.myForm.value.values);
const theSelectedValues = {
vals: this.myForm.value.values
};
this.selectedValues.emit(theSelectedValues);
}
private changeSelection(selectAll: boolean): void {
if (selectAll) {
const valuesSelect = this.myForm.controls['values'];
valuesSelect.disable();
} else {
this.myForm.controls['values'].enable();
}
}
ngOnDestroy() {
this.selectSubscription.unsubscribe();
}
}
The template:
<form [formGroup]="myForm" (ngSubmit)="submit()">
<fieldset>
<mat-checkbox formControlName="all">Select all</mat-checkbox>
</fieldset>
<fieldset>
<select id="chooser" formControlName="values" multiple>
<option *ngFor="let val of values?.urls" [value]="val">{{val}}</option>
</select>
</fieldset>
<button mat-button [disabled]="myForm.invalid">Go!</button>
</form>
<div *ngIf="errorMsg">{{errorMsg}}</div>
The test
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { Mycomponent } from './my.component';
import { By } from '#angular/platform-browser';
import { FormBuilder } from '#angular/forms';
import { NO_ERRORS_SCHEMA } from '#angular/core';
describe('Mycomponent', () => {
let component: Mycomponent;
let fixture: ComponentFixture<Mycomponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
schemas: [NO_ERRORS_SCHEMA],
declarations: [Mycomponent],
providers: [
FormBuilder ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(Mycomponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit selected values', () => {
spyOn(component.selectedValues, 'emit');
component.values = ['abc', 'de'];
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('option')).length).toBe(2); // is 0
const formDE = fixture.debugElement.query(By.css('form'));
formDE.triggerEventHandler('ngSubmit', {});
// urls: null
expect(component.selectedValues.emit).toHaveBeenCalledWith({ vals: ['abc', 'de'] });
});
});
The test fails because
a)
component.values = ['abc', 'de'];
Does not lead to the form having two option elements
and b)
expect(component.selectedValues.emit).toHaveBeenCalledWith({ vals: ['abc', 'de'] });
Is called, but with { vals: null }
The code works, the app itself works fine, just the test is failing.
How do I set up the form properly, the #Input element?
I have looked at some blog posts but have not been able to adapt them to my code.
This is because you are using Onpush strategy. When onPush is used change detection is propagating down from the parent component not the component itself.
My suggestion is to wrap your test component inside a host component. There is a pending issue regarding this on Angular's gitHub page you can look it up for further reading.
I am working on my first Angular app and dealing with the HttpClientModule in my component and getting errors.
following the docs Angular -HttpClient I installed the HttpClientModule in the app.module.ts as instructed, then in my emails.component.ts I have the following:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-emails',
templateUrl: './emails.component.html',
styleUrls: ['./emails.component.scss'],
results: string[]
})
export class EmailsComponent implements OnInit {
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('/api/email_list.json').subscribe(data => {
this.results = data['results'];
});
}
}
which is giving me the following error in my console:
ERROR in src/app/components/emails/emails.component.ts(7,3): error TS2345: Argument of type '{ selector: string; templateUrl: string; styleUrls: string[]; results: any; }' is not assignable to parameter of type 'Component'.
Object literal may only specify known properties, and 'results' does not exist in type 'Component'.
src/app/components/emails/emails.component.ts(7,12): error TS2693: 'string' only refers to a type, but is being used as a value here.
src/app/components/emails/emails.component.ts(7,19): error TS1109: Expression expected.
src/app/components/emails/emails.component.ts(12,29): error TS2304: Cannot find name 'HttpClient'.
src/app/components/emails/emails.component.ts(16,12): error TS2339: Property 'results' does not exist on type 'EmailsComponent'.
app.module.ts:
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { HttpClientModule } from '#angular/common/http';
import { AppComponent } from './app.component';
import { EmailsComponent } from './components/emails/emails.component';
#NgModule({
declarations: [
AppComponent,
EmailsComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
you're component should be:
first you need to import HttpClient
second results: string[] should be inside the class not at decoration component exactly as I did here :
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-emails',
templateUrl: './emails.component.html',
styleUrls: ['./emails.component.scss']
})
export class EmailsComponent implements OnInit {
results: string[];
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('/api/email_list.json').subscribe(data => {
this.results = data['results'];
});
}
}
Please read the description carefully, they explaining you need to load HttpClient.
In your app module you forgot to provide.
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { HttpClientModule } from '#angular/common/http';
import { AppComponent } from './app.component';
import { EmailsComponent } from './components/emails/emails.component';
#NgModule({
declarations: [
AppComponent,
EmailsComponent
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [HttpClient],
bootstrap: [AppComponent]
})
export class AppModule { }
Then in your component:
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'app-emails',
templateUrl: './emails.component.html',
styleUrls: ['./emails.component.scss']
})
export class EmailsComponent implements OnInit {
results: string[]
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('/api/email_list.json').subscribe(data => {
this.results = data['results'];
});
}
}
I have this error when i use ngModel in input boxes.
In .html page
<form>
<div *ngIf="editTitle" class="form-group">
<input type="input" class="form-control" name="title" required
placeholder="title" [(ngModel)]="video.title">
</div>
<h3 *ngIf="!editTitle" (click)="onTitleClick()">{{video.title}}</h3>
</form>
This is the details.components.html file and
the last one is the details.components.ts file.
In .ts page
import { Component, OnInit } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
#Component({
selector: 'video-detail',
templateUrl: './video-detail.component.html',
styleUrls: ['./video-detail.component.css'],
inputs: ['video']
})
export class VideoDetailComponent implements OnInit {
private editTitle: boolean = false;
constructor() { }
ngOnInit() {
}
ngOnChanges(){
this.editTitle = false;
}
onTitleClick(){
this.editTitle = true;
}
}
app.component.html
<form><div *ngIf="editTitle" class="form-group">
<input type="input" class="form-control" name="title" required
placeholder="title" [(ngModel)]="video.title"></div>
<h3 *ngIf="!editTitle" (click)="onTitleClick()">{{video.title}}</h3>
</form>
app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { AppComponent } from './app.component';
#NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
video: any = {};
private editTitle: boolean = false;
constructor() { }
ngOnInit() { }
ngOnChanges(){
this.editTitle = false;
}
onTitleClick(){
this.editTitle = true;
}
}
Seems are you are getting the input in a wrong way.Try getting the input like this:
export class VideoDetailComponent implements OnInit {
#Input() video:object;
constructor() {
}
ngOnInit() {
}
}
You also have to import the 'Input' module from '#angular/core' like below:
import { Component, OnInit,Input } from '#angular/core';