We have a template-based form that uses the various Angular Material controls (matInput, mat-select, etc). We want to create a 'read-only' type view for some users. My immediate thought was to simply check a user's privileges and then loop through all form controls and set them to disable if need be.
This appears to work for text input type controls but is not working for selects and checkboxes. Plus this grays-out the controls which isn't necessarily what we want.
Any ideas on how to best accomplish this? Perhaps somehow disable keyboard and mouse input within the form might be what we want but I have no idea how to go about this.
I think it is possible, but you will have to check all control types, for example MatInput exposes readonly property, it allows us to create simple directive which will switch readonly state for entire form:
import {Directive, ContentChildren, QueryList, AfterContentInit, Input} from '#angular/core';
import {NgModel} from '#angular/forms';
#Directive({
selector: 'form[readonly]'
})
export class FormReadonlyDirective implements AfterContentInit
{
#ContentChildren(NgModel)
public inputs:QueryList<NgModel>;
private controls:any[];
private _readonly:boolean = false;
public ngAfterContentInit():void
{
this.controls = this.getControls();
this.inputs.changes.subscribe(() =>
{
this.setReadonly(this.readonly);
});
this.setReadonly(this.readonly);
}
#Input()
public get readonly():boolean
{
return this._readonly;
}
public set readonly(value:boolean)
{
this._readonly = value != null && value !== false && `${value}` !== 'false';
this.setReadonly(value);
}
private setReadonly(value:boolean):void
{
if(this.controls != null)
{
this.controls.forEach(control =>
{
//todo: check
control.readonly = value;
});
}
}
private getControls():any[]
{
return this.inputs.map(control => control.valueAccessor);
}
}
Not tested, but you get the idea.
Related
There is some components in Ionic that do not provide an event that is emitted when focus is lost.
For example ion-input provides ionBlur. On the other hand there is other elements like ion-content where I need to detect an outside click, but without knowing which event to use.
Is there a way to achieve that without being limited to the proposed events in the documentation?
I found this article that shows a way to use a custom directive to detect an outside click:
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '#angular/core';
#Directive({
selector: '[clickOutside]'
})
export class ClickOutsideDirective {
constructor(private _elementRef : ElementRef) {
}
#Output()
public clickOutside = new EventEmitter();
#HostListener('document:click', ['$event.target'])
public onClick(targetElement) {
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside) {
this.clickOutside.emit(null);
}
}
}
The directive can then be used this way, after declaring it in the concerned module:
<!-- HTML Template -->
<ion-content (clickOutside)="handleOutsideClick()"><!-- ... --></ion-content>
<!-- Typescript code -->
handleOutsideClick() {
//Handle My outside Click
}
Yeah, It's been 7 months since asked.
Stucked with the same issue; this solved the issue
TS
#ViewChild('content') content: ElementRef
#HostListener('document:click', ['$event'])
andClickEvent(event) {
if (!this.content.nativeElement.contains(event.target)) {
if (!this.navCtrl.isTransitioning() && this.navCtrl.getActive()) {
this.close()
}
}
}
HTML
<ion-content #content>
I’m struggling at modeling a certain use case of mine with Draft.js.
I’m highlighting certain words/phrases in the editor with a composite decorator by using a regex strategy.
What I’m trying to archive is: if a user clicks on a decorated word, I want to toggle it’s state and store it somehow.
How would I do this? Any clues?
At a higher level, it’s basically a way to ignore certain decorated words, even if they match the regex strategy
I thought that maybe entities could help me do the job here, they would allow me to store such meta information (ex: ignore) on the decorated word, right?
When I faced the similar issue I used the store of the rendered component for storing a condition.
You used a functional component here:
renderAnnotation(type){
return (props)=> {
return (<span className={type} onClick={this.ignoreDecoratedWord.bind(this, props)}>{props.children}</span>);
};
}
You can change it and use standard react component:
renderAnnotation(type){
return (props)=> {
return (<HighlightedWord type={type} {...props} />);
};
}
The HighlightedWord component is:
class HighlightedWord extends React.Component {
constructor() {
super();
this.state = { enable: true }
}
toggleStatus = () => {
this.setState({ enable: !this.state.enable });
}
render() {
return (
<span className={this.state.enable ? this.props.type : ''} onClick={this.toggleStatus}>{this.props.children}</span>
)
}
}
We toggle the highlighting status after the click event.
Check this demo.
I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)
In angular2 I want to trigger Validators for some controls when a another control is changed. Is there some way that I can just tell the form to re-validate? Better still, can I request validation of specific fields?
Example:
Given Checkbox X and input P.
Input P has a validator that behaves differently based on the model value of X.
When X is checked/unchecked I need to invoke the validator on P. The Validator on P will look at the model to determine the state of X and will validate P accordingly.
Here's some code:
constructor(builder: FormBuilder) {
this.formData = { num: '', checkbox: false };
this.formGp = builder.group({
numberFld: [this.formData.num, myValidators.numericRange],
checkboxFld: [this.formData.checkbox],
});
}
this.formGp.controls['checkboxFld'].valueChanges.observer({
next: (value) => {
// I want to be able to do something like the following line:
this.formGp.controls['numberFld'].validator(this.formGp.controls['numberFld']);
}
});
Anybody have a solution? Thanks!
I don't know if you are still looking for an answer, so here is my suggestions:
Have a look at this: Angular 2 - AbstractControl
I think what you could do is following:
this.formGp.controls['checkboxFld'].valueChanges.observer({
next: (value) => {
this.formGp.controls['numberFld'].updateValueAndValidity();
}
});
This should trigger and run the validators. Furthermore the state gets updated as well. Now you should be able to consult the checkbox value within your validator logic.
Validaton-Guide
FormControl Documentation
with my ControlGroup I do this because I have errors divs checking if touched
for (var i in this.form.controls) {
this.form.controls[i].markAsTouched();
}
(this.form is my ControlGroup)
With the help of this blog
blog link
I have came across a solution with the combine of Nightking answer
Object.keys(this.orderForm.controls).forEach(field => {
const control = this.orderForm.get(field);
control.updateValueAndValidity();
});
this.orderForm is the form group
This did the trick for me
this.myForm.markAllAsTouched();
There are more elegant ways of modeling this behavior - for example, putting your state into a ReplaySubject and observing that, and then using async validators observing the state - but the pseudo-coded approach below should work. You simply observe the value changes in the checkbox, update the model as appropriate, then force a re-validation of the numberFld with the updateValueAndValidity cal.
constructor(builder: FormBuilder) {
this.formData = { num: '', checkbox: false };
const numberFld = builder.control(this.formData.num, myValidators.numericRange);
const checkbox = builder.control(this.formData.checkbox);
checkbox.valueChanges.map(mapToBoolean).subscribe((bool) => {
this.formData.checked = bool;
numberFld.updateValueAndValidity(); //triggers numberFld validation
});
this.formGp = builder.group({
numberFld: numberFld,
checkboxFld: checkbox
});
}
You can trigger validation in this way:
this.myform.get('myfield').updateValueAndValidity();
static minMaxRange(min: number, max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (Validators.min(min)(control)) { // if min not valid
return Validators.min(min)(control);
} else {
return Validators.max(max)(control);
}
};
}
Here is another similar way that also uses markAsDirty and updateValueAndValidity, particularly good if you use angular material where markAsTouched is not enough.
export function forceValidation(form: AbstractControl) {
if (form instanceof FormGroup || form instanceof FormArray) {
for (const inner in form.controls) {
const control = form.get(inner);
control && forceValidation(control);
}
} else {
form.markAsDirty();
form.markAsTouched();
form.updateValueAndValidity();
}
}
We have a form with few fields marked as readOnly.
The issue is that the user is able to focus or navigate to these readOnly fields using mouse or keyboard tab, and we want to disallow this.
One way of not allowing this is to mark all such fields as 'disabled'. But when marked disabled, then though fields can not be focussed, but then these disabled fields also do not get submitted to the server which is not what is expected.
Thus, how can we prevent a focus at readOnly fields?
PS: The reason behind disallowing focus at readOnly fields is to provide better navigation through keyboards, so that by using tab key user navigates or jumps across only those fields which he can edit and all readOnly fields get ignored.
You could add a listeners to the base Field class that listens for focus events then if the field is readOnly to focus the next component.
listeners: {
focus: function(field)
{
if (field.readOnly)
{
field.nextSibling().focus();
}
}
}
Just override getSubmitData method and assign some other property to let overridden method know you want to submit regardless (say, forceSubmit):
Ext.define('My.form.Field', {
override: 'Ext.form.field.Field',
getSubmitData: function() {
var me = this,
data = null;
if ( me.disabled && me.readOnly && me.forceSubmit ) {
data = {};
data[me.getName()] = '' + me.getValue();
}
else {
return me.callParent();
}
return data;
}
});
Then you can require this class in your code and set the fields you need to be disabled, readOnly and have forceSubmit:
my field = new Ext.form.field.Text({
disabled: true,
readOnly: true,
forceSubmit: true,
value: 'foo'
});
That should do the trick.