How to make an injection optional in an angular component - javascript

I have a angular component which uses a form group and can be used to show a readonly view based on an input #Input() isEditModeActive: boolean;. The component works fine when the parent component has form .
#Component({
selector: 'bifrost-organization-financial',
templateUrl: './organization-financial.component.html',
})
export class OrganizationFinancialComponent implements OnInit {
readonly MAX_NAME_LENGTH: number = 50;
readonly MAX_IBAN_LENGTH: number = 255;
readonly MAX_OTHER_NUMBER_LENGTH: number = 50;
readonly MAX_BIC_LENGTH: number = 255;
currencies: Currency[] = currencies;
bankAccount: BankAccount;
bankAccountTypes: BankAccountType[] = bankAccountTypes;
organizationFinancialFormGroup: FormGroup = new FormGroup({});
#Input() errorCodes: ErrorCode[];
#Input() customer: Organization;
#Input() isEditModeActive: boolean;
constructor(public readonly controlContainer: ControlContainer) {}
ngOnInit(): void {
this.organizationFinancialFormGroup = this.controlContainer
.control as FormGroup;
if (this.customer.bankAccounts.length === 0) {
this.customer.bankAccounts = [new BankAccount()];
}
this.bankAccount = this.customer.bankAccounts[0];
this.organizationFinancialFormGroup.addControl(
'currency',
new FormControl(this.customer.currency, [])
);
this.organizationFinancialFormGroup.addControl(
'name',
new FormControl(this.bankAccount.name, [
Validators.maxLength(this.MAX_NAME_LENGTH),
])
);
this.organizationFinancialFormGroup.addControl(
'type',
new FormControl(this.bankAccount.type, [Validators.required])
);
let bankAccountNumber: String = '';
if (this.bankAccount.type === 'SEPA') {
bankAccountNumber = this.bankAccount.number;
}
this.organizationFinancialFormGroup.addControl(
'iban',
new FormControl(bankAccountNumber, [
Validators.maxLength(this.MAX_IBAN_LENGTH),
ibanValidator(),
])
);
bankAccountNumber = '';
if (this.bankAccount.type === 'OTHER') {
bankAccountNumber = this.bankAccount.number;
}
this.organizationFinancialFormGroup.addControl(
'otherBankAccountNumber',
new FormControl(bankAccountNumber, [
Validators.maxLength(this.MAX_OTHER_NUMBER_LENGTH),
Validators.pattern(/^[A-Za-z0-9\s]*$/),
])
);
this.organizationFinancialFormGroup.addControl(
'bic',
new FormControl(this.bankAccount.bic, [
Validators.maxLength(this.MAX_BIC_LENGTH),
])
);
}
}
I want to reuse the component only for the read only view , but in this case the parent component does not have any FormGroup, But the code breaks with the below error :
NullInjectorError: R3InjectorError(CustomersModule)[ControlContainer -> ControlContainer -> ControlContainer -> ControlContainer]:
NullInjectorError: No provider for ControlContainer!
Is it possible to reuse this component from a parent component without a formGroup ?

You can try to add #Optional() decorator when injecting ControlContainer like that:
constructor(#Optional() public readonly controlContainer: ControlContainer) {}
Then you also probably need if statement in ngOnInit
ngOnInit(): void {
if (!!this.controlContainer) {
// do your stuff here
} else {
// readonly mode stuff here
}
}

Related

Angular creating filter with pipe and map

I am fairly new to angular and I am trying to create a filter for a value.
In my component - I have => myData$: Observable<MyInterface>
and the interface is as follows
export class FoundValues {
customerName: string;
startDate: string;
endDate: string;
includes(values: string) : boolean {
value = value.toLowerCase();
return this.valueIncludes(this.customerName, value);
}
private valueIncludes(includedValue, value){
if (value) {
const value = value.toLowerCase().includes(includedValue);
return result;
} else {
return false;
}
}
}
export interface MyInterface {
found_values : Array<FoundValues>;
}
In my component ngOnInit(), I am trying to create a logic filter but not getting it as it return a type FoundValues[] and it's complaining that it's not the expected Observable return type.
export class MyComponent implements OnInit{
myData$ = Observable<MyInterface>;
myControl = new FormControl();
ngOnInit(): void{
this.filterData =
this.myControl.valueChanges.pipe(map(value=>this._filter(value)));
}
private _filter(value:string): .....{
--- need logic here ----
}
}
How can I create the filter so that if I type a customer name in my form it shows only the matching customer name?
You can use the combineLatest RxJS operator for filtering through as shown in the following code snippet,
export class MyComponent implements OnInit {
myData$ = Observable < MyInterface > ;
mySearchQuery$: Observable < any > ;
searchString = new FormControl('');
filterData: Observable < any >
constructor() {
mySearchQuery$ = this.searchString.valueChanges.startsWith('');
}
ngOnInit(): void {
this.filterData = this.searchQuery$.combineLatest(this.myData$).map(([queryString, listOfCustomers]) => {
return listOfCustomers.filter(customer => customer.name.toLowerCase().indexOf(queryString.toLowerCase()) !== -1)
})
}
}
The combineLatest RxJS operator takes in the observables myData$ and mySearchQuery and returns the observable filterData containing the emitted values that match the searchString.
usual design in angular would be different
https://stackblitz.com/edit/angular7-rxjs-pgvqo5?file=src/app/app.component.ts
interface Entity {
name: string;
//...other properties
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name = new FormControl('');
data$: Observable<Array<Entity>> = of([
{ name: 'Jhon' },
{ name: 'Jane' },
{ name: 'Apple' },
{ name: 'Cherry' },
]);
filtered$: Observable<Array<Entity>>;
ngOnInit() {
// this can be moved to a util lib/file
const startWithPipe = pipe(
map(
([query, data]: [query: string, data: Array<Entity>]): Array<Entity> =>
data.filter((entity) =>
query ? entity.name.toLowerCase().startsWith(query) : true
)
)
);
this.filtered$ = this.name.valueChanges.pipe(
startWith(''),
debounceTime<string>(300),
withLatestFrom(this.data$),
startWithPipe
);
}

disable in Angular Material custom field component not working

I have a custom slide toggle component created using Angular Material. I followed this guide: https://material.angular.io/guide/creating-a-custom-form-field-control
Everything seems to be working fine except when I dynamically disable the custom component like this:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disabled]="true"
></custom-slide-toggle>
The component is disabled but I get the console warning It looks like you're using the disabled attribute with a reactive form directive. ....
To solve it I tried setting disabled the recommended way like this: activeUser: new FormControl([{value:false, disabled: true}]) in the parent component but the custom component was not disabled.
I also tried the same thing but in the custom component itself but didn't have any effect on making the field disabled or not.
UPDATE:
I tried adding a formGroup binding to my custom component per #DKidwell suggestion but I still get the same warning It looks like you're using the disabled attribute.... I added the formGroup using FormBuilder to more closely match the Angular Material example.
UPDATE 2:
The solution I found was to create a custom directive in conjunction with adding the FormGroup binding per #DKidwell's answer. The custom directive I created was based on this post: https://netbasal.com/disabling-form-controls-when-working-with-reactive-forms-in-angular-549dd7b42110
I implemented the custom directive like so and removed the [disabled] decorator:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disableControl]="isEditable"
></custom-slide-toggle>
Here's my custom component typescript:
#Component({
selector: 'custom-slide-toggle',
templateUrl: './custom-slide-toggle.component.html',
styleUrls: ['./custom-slide-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomSlideToggleComponent)
},
{
provide: MatFormFieldControl,
useExisting: CustomSlideToggleComponent
}
],
host: {
'[id]': 'id'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSlideToggleComponent implements OnInit, OnDestroy, DoCheck, ControlValueAccessor, MatFormFieldControl<boolean> {
private static nextId = 0;
private _placeholder: string;
private _disabled = false;
private _required = false;
private _readlonly = false;
public stateChanges = new Subject<void>();
public errorState = false;
public focused = false;
public ngControl: NgControl;
public toggleFormGroup: FormGroup;
#HostBinding() public id = `custom-slide-toggle-${CustomSlideToggleComponent.nextId++}`;
#HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
#HostBinding('attr.aria-describedby') describedBy = '';
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
#Input() public toolTip: string = '';
#Input() public isChecked: boolean;
#Input() public toggleText: string = '';
#Input() public tabNumber: number = null;
#Input() public toggleId: string = '';
#Input() public toggleClass: string;
#Input()
public get disabled(): boolean {
return this._disabled;
}
public set disabled(value: boolean) {
console.log('set disabled trigged');
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.toggleFormGroup.disable() : this.toggleFormGroup.enable();
this.stateChanges.next();
}
#Input()
public get required() {
return this._required;
}
public set required(req: boolean) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
#Input()
public get readonly(): boolean {
return this._readlonly;
}
public set readonly(value: boolean) {
this._readlonly = coerceBooleanProperty(value);
this._readlonly ?
this.toggleFormGroup.get('toggleFormControl').disable() :
this.toggleFormGroup.get('toggleFormControl').enable();
this.stateChanges.next();
}
#Input()
public get value(): boolean {
let n = this.toggleFormGroup.value;
if (n.toggleFormControl !== null){
return n.toggleFormControl.value;
}
return null;
}
public set value(val: boolean) {
this.toggleFormGroup.setValue({toggleFormControl: val});
this.stateChanges.next();
this.onTouched();
}
#Input()
public get placeholder(): string {
return this._placeholder;
}
public set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
#Output() selectionChange: EventEmitter<boolean> = new EventEmitter<boolean>();
public constructor(private injector: Injector, fb: FormBuilder) {
this.toggleFormGroup = fb.group({
'toggleFormControl': ''
});
}
ngOnInit(): void {
this.ngControl = this.injector.get(NgControl);
if (this.ngControl != null) { this.ngControl.valueAccessor = this; }
}
ngOnDestroy(): void {
this.stateChanges.complete();
}
ngDoCheck(): void {
if(this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public toggleClick($event: MatSlideToggleChange) {
this.onChange($event.checked);
this.selectionChange.emit($event.checked);
}
public onChanged = (val: boolean) => {};
public onTouched = () => {};
writeValue(value: any) {
console.log('writeValue triggered, incoming value is: ' + value);
if (value !== this.inputControl.value) {
this.inputControl.setValue(value);
this.onChanged(value);
this.stateChanges.next();
}
}
get empty() {
if (this.inputControl?.pristine || this.inputControl?.untouched) return true;
else return false;
}
onBlur() {
this.onTouched();
}
onChange(val: boolean) {
this.writeValue(val);
}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
Here is my custom component's html:
<mat-slide-toggle
[formControl]="inputControl"
[id]="toggleId"
[class]="toggleClass"
color="primary"
labelPosition="after"
[checked]="isChecked"
[disabled]="disabled"
[required]="required"
(change)="toggleClick($event)"
[tabIndex]="tabNumber"
[matTooltip]="toolTip"
>{{toggleText}}</mat-slide-toggle>
How do I dynamically disable my custom component without getting the warning?
You need to add a formGroup binding to your custom component,
<div [formGroup]="yourFormGroup">
<mat-slide-toggle ...>
{{toggleText}}
</mat-slide-toggle>
</div>
You will also need to define that formGroup in your component,
FormGroup yourFormGroup = new FormGroup({
inputControl: new FormControl()
});
Once that is setup properly you shouldn't need to bind to the [disabled] property in your custom control's template.

how can i find paramter of previous route in angular

i want find the params in previous route in angular typescript .
i use this code :
private previousUrl: string = undefined;
private currentUrl: string = undefined;
constructor(private router: Router) {
this.currentUrl = this.router.url;
router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.previousUrl = event.url;
this.currentUrl = this.currentUrl;
}
});
}
but i can not access to the params of this url :
http://localhost:4200/claims-manager/200/edit
i want ti access 200 . how can i find params in url ????
You can do it in your component file but It is a best practice to do it in a service (using rxjs) to pass data and call it in your component file
In your service
export class myService {
constructor() { }
private param = new BehaviorSubject("");
sharedParam = this.param.asObservable();
paramToPass(param:string) {
this.param.next(param)}
}
In your component class that set param
export class ComponentSetParam {
param: string
constructor(private myService: Service)
this.myService.setParam(this.param);
}
in your appModule
#NgModule({
declarations: [YourComponents]
imports: [ AppRoutingModule, YourModules...],
providers: [ShareService],
})
export class AppModule {}
Component that you want to pass data
export class ComponentGetParam {
paramFromService: string
constructor(private myService: Service) {
this.shareService.sharedData.subscribe(data : string => {
this.paramFromService = data;
})
}
}
Try this:
readonly _destroy$: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
constructor(
private activatedRoute: ActivatedRoute,
) {
this.activatedRoute.parent.paramMap
.pipe(
distinctUntilChanged(),
takeUntil(this._destroy$)
)
.subscribe((params: ParamMap) => {
const id = params.get('id');
});
}
ngOnDestroy() {
this._destroy$.next(true);
this._destroy$.complete();
}
Where 'id' is a name, that you use in the routing, e.g.
path: '/claims-manager/:id/'
Demo You can do it in service
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
#Injectable()
export class ShareService {
constructor() { }
private paramSource = new BehaviorSubject("");
sharedData = this.paramSource.asObservable();
setParam(param:string) { this.paramSource.next(param)}
}
in constructors
constructor(private shareService: ShareService)
in component in ngOnDestroy set this like this.shareService.setParam(param);
in appmodule
providers:[ShareService ]
in new component in ngOnInit or in constructor get like
this.shareService.sharedData.subscribe(data=> { console.log(data); })

pipe operator not behaving as expected RXJS

Please look at my component below the purpose to is to listen on changes to an input, which it does and then emit the value to the parent component. I created a pipe to only emit every so often and therby minimize the calls to the api, for some reason even though I can see through various console.log statements that it goes in the pipe, it emits the value on every change. What is it that I am missing:
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, KeyValueDiffers, DoCheck, KeyValueDiffer} from '#angular/core';
import {BehaviorSubject, Observable, of} from "rxjs";
import {debounceTime, distinctUntilChanged, map, skip, switchMap, takeUntil, tap} from "rxjs/operators";
#Component({
selector: 'core-ui-typeahead-filter',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './typeahead-filter.component.html',
})
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new Observable<string>();
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
handleTypeahead = (text$: Observable<string>) =>
text$.pipe(
distinctUntilChanged(),
debounceTime(500),
).subscribe((value) => {
this.onTypeahead.emit(of(value))
})
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.text$);
}
});
}
}
}
More background: There is an ngModel on the input linked to detectChange when it changes then the ngDoCheck is called and executes. Everything is done in observables so in the parent I can subscribe to the incoming events.
EDIT -------------------------------------------------------------------
Tried the following solution based on my understanding of #ggradnig answer, sadly it skips over my pipe something seems wrong with it, really not sure what:
handleTypeahead = (text$: Observable<string>) => {
this.test.subscribe(this.text$);
this.test.pipe(
distinctUntilChanged(),
debounceTime(500),
// switchMap(value => text$)
).subscribe((value) => {
tap(console.log('im inside the subscription',value))
this.onTypeahead.emit(value)
})
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.test);
}
});
}
}
}
You can do the following -
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new BehaviorSubject<string>('');
serachTerm$: Observable<string>;
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
// handleTypeahead = (text$: Observable<string>) =>
// text$.pipe(
// distinctUntilChanged(),
// debounceTime(500),
// ).subscribe((value) => {
// this.onTypeahead.emit(of(value))
// })
ngOnInit() {
this.serachTerm$ = this.text$
.pipe(
distinctUntilChanged(),
debounceTime(500),
//filter(), //use filter operator if your logic wants to ignore certain string like empty/null
tap(s => this.onTypeahead.emit(s))
);
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$.next(item.currentValue);
}
});
}
}
}
Now, at the bottom of your template put the following line -
<ng-container *ngIf="searchTerm$ | async"></ng-container>
Having this line will keep your component code free form managing the subscription [i.e. need not to subscribe/unsubscribe]; async pipe will take care of it.

Angular2: Send data from one component to other and act with the data

I'm learning Angular2. In order to that, I have 2 components, on the click of one of the components, the other component should be notified and act with that.
This is my code so far:
export class JsonTextInput {
#Output() renderNewJson: EventEmitter<Object> = new EventEmitter()
json: string = '';
process () {
this.renderNewJson.next(this.json)
}
}
The process function is being called on the click on the first component.
On the second component I have this code:
export class JsonRendered {
#Input() jsonObject: Object
ngOnChanges () {
console.log(1)
console.log(this.jsonObject)
}
}
The ngOnChanges is never runned, I dont get how to pass the info from one component to other
EDIT
There is an app component which is parent of those 2 components. None of both is parent of the other
This is how my clasess look now:
export class JsonRendered {
private jsonObject: Object
constructor (private jsonChangeService: JsonChangeService) {
this.jsonChangeService = jsonChangeService
this.jsonObject = jsonChangeService.jsonObject
jsonChangeService.stateChange.subscribe(json => { this.jsonObject = json; console.log('Change made!') })
}
}
export class JsonTextInput {
json: string = '';
constructor (private jsonChangeService: JsonChangeService) {
this.jsonChangeService = jsonChangeService
}
process () {
this.jsonChangeService.jsonChange(this.json)
}
}
And the service
import {Injectable, EventEmitter} from '#angular/core';
#Injectable()
export default class JsonChangeService {
public jsonObject: Object;
stateChange: EventEmitter<Object> = new EventEmitter<Object>();
constructor(){
this.jsonObject = {};
}
jsonChange(obj) {
console.log('sending', obj)
this.jsonObject = obj
this.stateChange.next(this.jsonObject)
}
}
Create a service like so...
import {Injectable, EventEmitter} from 'angular2/core';
#Injectable()
export class MyService {
private searchParams: string[];
stateChange: EventEmitter<any> = new EventEmitter<any>();
constructor(){
this.searchParams = [{}];
}
change(value) {
this.searchParams = value;
this.stateChange.next(this.searchParams);
}
}
Then in your component...
import {Component} from 'angular2/core';
import {MyService} from './myService';
#Component({
selector: 'my-directive',
pipes: [keyValueFilterPipe],
templateUrl: "./src/someTemplate.html",
providers: [MyService]
})
export class MyDirective {
public searchParams: string[];
constructor(private myService: MyService) {
this.myService = myService;
myService.stateChange.subscribe(value => { this.searchParams = value; console.log('Change made!') })
}
change(){
this.myService.change(this.searchParams);
}
}
You have to subscribe to the eventemitter, then update your variable. The change event in the service would get fired of from something like...
(click)="change()"

Categories

Resources