Angular: How to mark a validator as dirty manually - javascript

I have a component I would like to mark as dirty when the "Next" button in my stepper component is clicked.
Currently inside my stepper.component.ts I have a function displayValidation, which is called when my Next button is clicked and manually marks all form inputs as touched (I have also double checked each's status to make sure touched == true when the Next button is clicked):
displayValidation(field: FormlyFieldConfig) {
if (field?.fieldGroup){
field.fieldGroup.forEach((curFieldGroup) => {
curFieldGroup.formControl.markAsTouched();
this.setFieldTypeInvalid(curFieldGroup.type, curFieldGroup.formControl.status);
});
}
}
This successfully changes all my components to touched, however for number input component, it does nothing visually (expecting a red outline/dirty) despite touched now equalling true, and validation existing for the component (called currency component).
Here is my currency.component.ts in question:
import { Component } from '#angular/core';
import { CurrencyPipe} from '#angular/common';
import { FieldType } from '#ngx-formly/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
#Component({
selector: 'app-formly-field-currency',
templateUrl: './currency.component.html'
})
export class FormlyFieldCurrencyComponent extends FieldType {
constructor(private currencyPipe: CurrencyPipe) {
super();
}
currencyGroup = new FormGroup({
currencyForm: new FormControl('', [
Validators.required
])
});
get currencyAmount(){
return this.currencyGroup.get('currencyForm');
}
}
In my corresponding currency.component.html, I have formGroup and formControl validation that are fired when the currency component is clicked manually, but I would like it to fire in the above stepper.component.ts such that it is visually dirty when the Next button is clicked:
currency.component.html:
<div class="input-group mb-3">
<form [formGroup]="currencyGroup">
<div class="input-group mt-2 flex-nowrap">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
</div>
<input matInput
[class.is-invalid] = "currencyGroup.get('currencyForm').invalid && currencyGroup.get('currencyForm').touched"
type="number"
class="form-control"
[formControl]="formControl"
formControlName="currencyForm" #input
[formlyAttributes]="field"
>
</div>
<div
class="mx-auto"
*ngIf="(currencyAmount.invalid && currencyAmount.touched) || currencyAmount.dirty">
<small *ngIf="currencyAmount.errors?.required" class="text-danger">
Currency amount is required.
</small>
</div>
</form>
</div>
I understand this is a lot here, however I feel as if I am so close, so any help would be greatly appreciated.
Thanks!

Related

How do I add a conditional form field through content projection in Angular?

I am trying to add a conditional field through content projection.
The projected input field is toggling correctly in accordance to its sibling checkbox field. When the checkbox is "checked" the conditional field displays. When the checkbox is "unchecked", the input field disappears.
However, when I move the checkbox into its "checked" state, I'm receiving an error:
ERROR Error: Cannot find control with name: 'detail'
I want the projected detail field to be apart of the _formGroup in the child component, not the parent component. How can I achieve this?
The error seems to suggest that the formControlName="detail" is not visible to the _formGroup in the child component. How can I rectify this?
Here is the parent component with the outer parent Form Group.
Parent Component
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup } from '#angular/forms';
#Component({
selector: 'ch-parent-comp',
templateUrl: './parent-comp.component.html',
styleUrls: ['./parent-comp.component.scss']
})
export class ParentComponent implements OnInit {
formGroup: FormGroup;
constructor() {
this.formGroup = new FormGroup({
//detail: new FormControl('') --> Adding this will solve the issue, but I don't want this field here. I want it in the child
});
}
ngOnInit(): void {
}
}
Parent Component HTML Template
<form>
<ch-check-box-group [parentFormGroup]="formGroup" controlName="new_property" controlLabel="New Property" [hasDetail]="true" [detailRequired]="true">
<!-- Projected Input Field -->
<ng-template contentHandle>
<mat-form-field>
<mat-label>Purchase Date</mat-label>
<input type="text" matInput name="detail" formControlName="detail" id="">
</mat-form-field>
</ng-template>
</ch-check-box-group>
</form>
Child Component(ch-check-box-group)
export interface CheckboxGroupForm {
value: FormControl<boolean>,
detail?: FormControl<any>
}
#Component({
selector: 'ch-check-box-group',
templateUrl: './check-box-group.component.html',
styleUrls: ['./check-box-group.component.scss']
})
export class CheckBoxGroupComponent implements OnInit, OnDestroy {
private _ks: Subject<void> = new Subject<void>();
_formGroup: FormGroup
_showDetailInput: boolean = false;
#Input() controlLabel!: string;
#Input() controlName!: string;
#Input() parentFormGroup!: FormGroup;
#Input() hasDetail: boolean = false;
#Input() detailRequired: boolean = false;
#ContentChild(ContentHandleDirective) content!: ContentHandleDirective;
constructor() {
this._formGroup = new FormGroup<CheckboxGroupForm>({
value : new FormControl(false, {nonNullable: true}),
});
}
ngOnInit(): void {
if(this.hasDetail){
this._formGroup.addControl('detail', new FormControl(''));
}
this.parentFormGroup.addControl(this.controlName, this._formGroup);
this._formGroup.setParent(this.parentFormGroup);
this._formGroup.valueChanges
.pipe(takeUntil(this._ks))
.subscribe((change) => {
// Toggle visibility of detail input
if(this.hasDetail && this._showDetailInput != change.value){
this._showDetailInput = change.value;
}
// Toggle Validation(if necessary) of detail input
if(this.hasDetail && this.detailRequired && change.value){
setTimeout(() => {
this._formGroup.get('detail')?.addValidators(Validators.required);
});
} else {
this._formGroup.get('detail')?.clearValidators();
if(this._showDetailInput != change.value){
this._formGroup.get('detail')?.updateValueAndValidity();
}
}
console.log(this._formGroup.controls)
});
}
ngOnDestroy(){
this._ks.next();
this._ks.complete();
}
}
Child Component Template
<div class="row">
<form class="col-12" [formGroup]="_formGroup">
<mat-checkbox formControlName="value" i18n>{{ controlLabel }}</mat-checkbox>
<button mat-icon-button color="primary">
<mat-icon>help</mat-icon>
</button>
<div class="conditional-input-box-container" *ngIf="_showDetailInput">
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>
</form>
</div>
To solve this, pass the child components _formGroup to the [ngTemplateOutletContext] like so:
<div class="row">
<form class="col-12" [formGroup]="_formGroup">
<mat-checkbox formControlName="value" i18n>{{ controlLabel }}</mat-checkbox>
<button mat-icon-button color="primary">
<mat-icon>help</mat-icon>
</button>
<div class="conditional-input-box-container" *ngIf="_showDetailInput">
<ng-container [ngTemplateOutlet]="content.templateRef" [ngTemplateOutletContext]="{ $implicit: _formGroup}"></ng-container>
</div>
</form>
</div>
Then use that new variable in the parent component like this:
<form>
<ch-check-box-group [parentFormGroup]="formGroup" controlName="new_property" controlLabel="New Property" [hasDetail]="true" [detailRequired]="true">
<!-- Projected Input Field -->
<ng-template contentHandle let-_formGroup>
<div [formGroup]="_formGroup">
<mat-form-field>
<mat-label>Purchase Date</mat-label>
<input type="text" matInput name="detail" formControlName="detail" id="">
</mat-form-field>
</div>
</ng-template>
</ch-check-box-group>
</form>

ng-select - when dropdown is opened it is scrolled far-down by default

I am using ng-select for dropdown list (multiselect).
ng-select has native problem so when all items are auto-selected on dropdown init, it will be auto-scrolled far-down to last item.
It is working when no items are preselected on init but I need them all to be preselected on init.
Is there a chance to avoid this behavior?
Could this approach work?
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
#Component({
selector: 'myForm',
templateUrl: './myForm.component.html',
styleUrls: ['./myForm.component.scss']
})
export class AlarmComponent implements OnInit {
myForm: FormGroup;
dropdownitems: [];
constructor(private formBuilder: FormBuilder) {}
ngOnInit(): void {this.initializeForm();}
initializeForm() {this.myForm = this.formBuilder.group({myDropdown: [''] });
html-file
<div class="card text-center>
<form [formGroup]=" myForm" (ngSubmit)="onSubmit()" class="col-xl-12">
<ng-select [items]= dropdownitems" formControlName="alarm" > </ng-select>
<!-- bindValue="" bindLabel="" (change)="onChanged($event)"are optional -->
<button class="btn btn-primary" type="submit">submit</button>
</form>
</div>
If this doesn't fit your needs, please add the html- and the ts-template (complete) to your question. Or a link to somewhere where they can be found.
Take care and good luck.

Angular 7, elementRef not correct

i have created component autocomplete and i was using it just fin till i had to user 2 of those component in 1 view, i saw that dropdown in template is referenced to previous instance of my autocomplete component
#Component({
selector: 'app-autocomplete',
templateUrl: './autocomplete.component.html',
styles: []
})
export class AutocompleteComponent implements OnInit, OnChanges {
delayId;
#ViewChild('dropdown')
dropdown;
#ViewChild('element')
input;
....
}
and template
<div class="form-group" style="position: relative;">
<label for="{{buttonId}}">{{buttonCaption}}</label>
<input #element (keyup)="sendKeyStrokes(element.value)"
type="text" class="form-control"
id="{{buttonId}}"
aria-describedby=""
placeholder="{{buttonCaption}}">
<ng-content></ng-content>
<div #dropdown <-- **this is wrong** class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<span *ngFor="let option of options"
class="dropdown-item"
(click)="optionClick(option)">
{{option.caption}}
</span>
</div>
</div>
can i create reference # dynamically ? or someone can point me to right direction

Angular: Hiding/showing elements by ngIf or Toggling a class?

I'm wondering what the best way to approach this problem is and I'm very new to TypeScript and Angular. Using Angular 5.
Anyway. I have a list of elements on a page via a table.
This is the code that controls said list.
<tbody>
<tr class="text-center" *ngFor="let topic of topics">
<td *ngIf="!editTopicMode">{{ topic.name }}</td>
<td id="{{topic.id}}" *ngIf="editTopicMode">
<form>
<div class="form-group">
<input class="form-control" type="text" name="name" value="{{topic.name}}" />
</div>
</form>
</td>
<td>
<div *ngIf="!editTopicMode" class="btn-group btn-group-sm">
<button class="btn btn-link" (click)="editTopicBtnClick(topic.id)">
<i class="fa fa-pencil fa-2x" aria-hidden="true"></i>
</button>
<button class="btn btn-link">
<i class="fa fa-trash fa-2x" aria-hidden="true"></i>
</button>
</div>
<div *ngIf="editTopicMode" class="btn-group-sm">
<button class="ml-2 btn btn-sm btn-outline-secondary" (click)="cancelEditMode()">Cancel</button>
<button class="ml-2 btn btn-sm btn-outline-primary">Save</button>
</div>
</td>
</tr>
</tbody>
What I'm aiming to do is that if a user clicks on the pencil(edit) icon, then the adjacent div changes from just a regular td to an input and the edit/delete buttons to change to a cancelEdit/save edits button group. (I know that I need to change the html a bit because the buttons aren't in the form element currently, but I'm not there on the wiring it up part).
I've thought of two ways to do this. 1) with ngIf's so that I can conserve the elements that are rendered and the edit/cancel buttons toggle the editMode; or 2) use ngClass and toggle display:none css classes for the button clicked.
Right now, when you click the edit button, regardless of which edit button you click, it flips all the columns to inputs, rather than just the row the user wants to edit.
Here's my component ts:
import { Component, OnInit, TemplateRef, ElementRef, ViewChild, Inject } from '#angular/core';
import { Topic } from '../models/topic';
import { TopicService } from '../services/topicService/topics.service';
import { AlertifyService } from '../services/alertify/alertify.service';
import { ActivatedRoute } from '#angular/router';
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'app-topics',
templateUrl: './topics.component.html',
styleUrls: ['./topics.component.css']
})
export class TopicComponent implements OnInit {
#ViewChild('topicId') topicId: ElementRef;
topics: Topic[];
newTopic: Topic = {
id: 0,
name: '',
};
editTopicMode = false;
constructor(
#Inject(DOCUMENT) document,
private topicsService: TopicService,
private alertify: AlertifyService,
private route: ActivatedRoute
) { }
ngOnInit() {
//this.route.data.subscribe(data => {
// this.topics = data['topics'];
//})
this.getTopics();
}
getTopics() {
this.topicsService.getAllTopics()
.subscribe(data => {
this.topics = data;
}, error => {
this.alertify.error(error);
});
}
addTopic() {
this.topicsService.createTopic(this.newTopic)
.subscribe((topic: Topic) => {
this.topics.push(topic);
this.alertify.success(this.newTopic.name + ' added as a new topic.');
this.newTopic.name = '';
},
(err: any) => {
this.alertify.error(err);
}
)
}
editTopicBtnClick(event) {
console.log(event);
this.editTopicMode = true;
console.log(document.getElementById(event));
}
cancelEditMode() {
this.editTopicMode = !this.editTopicMode;
}
}
Any thoughts on the best (most efficient) way to make this happen?
You've done all the hard work already.
For single item editing, all that's left is: change editTopicMode to something like editTopicId.
Then you can:
Set it to topic.id on edit enabled, and null for example on edit closed
Change your *ngIf to editTopicId === topic.id (or !== as needed)
And that should be all.
If you want to enable multiple editing, just add a property called isInEditMode to each topic.
Your *ngIf check becomes topic.isInEditMode
No isInEditMode property at all is just like false, because undefined is a falsy value
Set topic.isInEditMode to true on editing enabled, false on editing closed
Using *ngIf is fine just make sure that you set the *ngIf variable to point to somethign specific to the particular row of the *ngFor
this example could be cleaner but you could accomplish it as simply as
<button (click)="topic.edit = true" *ngIf="topic.edit === false">edit</button>
<button (click)="topic.edit = false" *ngIf="topic.edit === true">cancel</button>

How to show "show more" button when text has ellipsis?

I have searched on Google and here on SO before posting this question.
I have found several solution to my problem, but none of them fits my needs.
Here is my code: Plunker
<div *ngIf="description.length > 200" class="ui mini compact buttons expand">
<button class="ui button" (click)="showMore($event)">Show more</button>
</div>
The "show more" button appears only if text length exceeds 200 characters.
As you can see it seems to be a nice solution.
showMore(event: any) {
$(event.target).text((i, text) => { return text === "Show more" ? "Show less" : "Show more"; });
$(event.target).parent().prev().find('.detail-value').toggleClass('text-ellipsis');
}
Anyway I could have a text that is not 200 characters long and that doesn't fit the SPAN element, then it has the ellipsis but the "show more" button doesn't appear.
How can I make my solution work in any case? Do you know a workaround or a best solution to solve that?
Edit with a possible solution:
//our root app component
import {Component, NgModule, VERSION, OnInit} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import {ElementRef,ViewChild} from '#angular/core';
#Component({
selector: 'my-app',
template: `
<div class="ui segment detail-container" (window:resize)="checkOverflow(span)">
<span class="title-container" role="heading">User details</span>
<div class="detail-group">
<div class="detail-element">
<span class="detail-label">Name</span>
<span class="detail-value">John</span>
</div>
<div class="detail-element">
<span class="detail-label">Surname</span>
<span class="detail-value">Smith</span>
</div>
</div>
<div class="detail-group">
<div class="detail-element">
<span class="detail-label">Description</span>
<span #span class="detail-value text-ellipsis">{{description}}</span>
</div>
<div class="ui mini compact buttons expand">
<button *ngIf="checkOverflow(span) && showMoreFlag" class="ui button" (click)="showMore($event)">Show more</button>
<button *ngIf="!showMoreFlag" class="ui button" (click)="showMore($event)">Show less</button>
</div>
</div>
</div>
`,
styleUrls: ['src/app.css']
})
export class App implements OnInit {
description: string = 'Lorem ipsum dolor sit a ';
showMoreFlag:boolean = true;
constructor() {
}
ngOnInit(): void {
this.overflowOcurs = this.checkOverflow(this.el.nativeElement);
}
showMore(event: any) {
this.showMoreFlag = !this.showMoreFlag;
$(event.target).parent().prev().find('.detail-value').toggleClass('text-ellipsis');
}
checkOverflow (element) {
if (element.offsetHeight < element.scrollHeight ||
element.offsetWidth < element.scrollWidth) {
return true;
} else {
return false;
}
}
}
#NgModule({
imports: [ BrowserModule ],
declarations: [ App ],
bootstrap: [ App ]
})
export class AppModule {}
Plunker working properly:
https://plnkr.co/edit/HCd6ds5RBYvlcmUtdvKr
I Recommend using the "ng2-truncate".
With this component, you can truncate your codes with length or word count or something else.
I hope this component help you.
Plunker
npm

Categories

Resources