So I have a form that I need to add validators to and some of the controls are required only if a certain condition is matched by another control. What is a good way to do this. I originally made a custom validator function that I passed in a parameter to the function to determine if it should be required, but it keeps the original value of the parameter not matter if I update other controls in the form.
public static required(bookType: BookType, controlKey: string) {
return (control: AbstractControl): ValidationErrors | null => {
if(this.isRequired(bookType,controlKey)){
return !control.value? {required: true} : null
}
return null;
}
}
the form book type is originally DIGITAL and I change the book type to PRINT it stays DIGITAL.
This feels like it should stay a form-control validator since I am validating one value, not the group.
What would be the best way to make this work?
You need to implement a cross fields validator. So you will be able to play with values of these fields inside the validator function. Details: https://angular.io/guide/form-validation#cross-field-validation
const deliveryAddressRequiredValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const bookType = control.get('bookType');
const deliveryAddress = control.get('deliveryAddress');
if (bookType && deliveryAddress && bookType.value === 'PRINT' && Validators.required(deliveryAddress)) {
return { deliveryAddressRequired: true };
}
// no validation for book type DIGITAL
return null;
};
Usage:
this.form = this.formBuilder.group({
bookType: ['', [Validators.required]],
deliveryAddress: [''],
}, {validators: deliveryAddressRequiredValidator});
To display error in the template use: form.errors?.deliveryAddressRequiredValidator
Related
Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx
I would like to pass a variable as a parameter in a custom validator like this
newSimulation: new FormControl('', [uniqNameValidator(this.options)])
Then use it in my custom validator
export function uniqNameValidator(list: any): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const simulationFlatList = list.map(val => val.closingPeriodSimulationName)
return simulationFlatList.indexOf(control.value) > -1 ? { message: "simulation exists" } : null;
}
}
The issue with this is that this.options is always empty. I initialize it to [] but when user interacts with the form ( first field ) I update it to an array of string, I think that the custom validator does not recheck the value of this.options ?
In this case how to pass a variable in custom validator ?
this may work, bind the function to component newSimulation: new FormControl('', [uniqNameValidator.bind(this)]) then in the function you can access this.options
I get input data in this form:
[
{ name: "Producent", checked: true },
{ name: "Handel", checked: true },
{ name: "Simple", checked: true }
];
Only the checked values from true to false and vice versa can change. This is assigned to the checkedTypeOfClient variable. Later, I'd like to filter out all my clients (the currentClientList array) based on the checkedTypeOfClient variable.
Here are the properties of the Client class:
export class Client {
clientId: number;
name: string ;
district: string;
province: string;
zip: string;
city: string;
// tslint:disable-next-line: variable-name
full_Address: string;
latitude: number;
longitude: number;
segment: string;
ph: string;
bh: number;
canal: string;
isSimple: string;
}
The complication of this task is that the filtration goes like this. The values Producent and Handel are values that can be placed in the canal column that are in the Client Class, and the Simple value is a value that is also in the Client class in the isSimple column and can take the value "YES" or "NO"
for now, what I was able to do is extract what values Producent , Handel, Simple are marked and grind the Simple field to "TAK" or NIE "
filterClients() {
console.log(this.checkedTypeOfClient);
const filter={
Producent:this.checkedTypeOfClient.find(x=>x.name=="Producent").checked,
Handel:this.checkedTypeOfClient.find(x=>x.name=="Handel").checked,
Simple:this.checkedTypeOfClient.find(x=>x.name=="Simple").checked
}
let simpleFilter = this.returnFilterSimpleValue(filter.Simple);
this.currentClientList = this.baseClientList;
}
returnFilterSimpleValue(value : boolean) : string {
switch (value) {
case true:
return "TAK";
case false:
return "NIE";
}
If(Producent = True){
this.currentClientList(client.producent)
}
If(Handel= True){
this.currentClientList(client.handel)
}
If(Handel= True || Producent = True){
this.currentClientList(client.handel) && this.currentClientList(client.producent)
}
The question is how to filter it?
I'm not sure that I'm fully understanding your question. However, here's how I'd solve it.
First of all, I'd like to introduce you to union types (a feature of typescript that allows you to combine multiple types, but can also be used for literal values.
In your case that would be useful for:
isSimple: "YES" | "NO" meaning that it can only have either values. Also, why not make it a boolean?
canal: "Handel" | "Producent" meaning that it can only have either the "Handel" or "Producent" string value.
Secondly you can simple use filter array method to filter the objects that have a certain property values, which can also be chained.
Is this what you wanted to do or did I miss something?
I'm not sure either if I get the question right. As i understand it, you have two different types of clients: CanalClient and SimpleClient.
with the superclass Client sharing the common attributes.
export class CanalClient extends Client {
canal: string;
}
export class Simpleclientextends Client {
isSimple: string; // as Ruben asked - why is this not a simple boolean?
}
In the filter Operation, you can then check the class with instanceof CanalClient etc. to have type safety.
Actually, in an if block- this is type guarded:
if(client instanceof CanalClient) {
console.log( client.canal) // Typescript will know this is of type Canalclient!
}
I tried to keep it as simple as I could by reusing your code as per my understanding. Try this:
filterClients() {
console.log(this.checkedTypeOfClient);
const filter={
Producent:this.checkedTypeOfClient.find(x=>x.name=="Producent").checked,
Handel:this.checkedTypeOfClient.find(x=>x.name=="Handel").checked,
Simple:this.checkedTypeOfClient.find(x=>x.name=="Simple").checked
}
let simpleFilter = this.returnFilterSimpleValue(filter.Simple);
this.currentClientList = this.baseClientList.filter(c => {
if (c.canal=='Producent' && filter.Producent) {
return true;
}
if (c.canal=='Handel' && filter.Handel) {
return true;
}
});
this.currentClientList.foEach(c => { c.isSimple = this.returnFilterSimpleValue(c.isSimple) });
}
I need value :
If(Producent = True){
this.currentClientList(client.producent)
}
If(Handel= True){
this.currentClientList(client.handel)
}
If(Handel= True || Producent = True){
this.currentClientList(client.handel) && this.currentClientList(client.producent)
}
We are developing components and when using them, we would like to use the same mechanism like for DOM nodes to conditionally define attributes. So for preventing attributes to show up at all, we set the value to null and its not existing in the final HTML output. Great!
<button [attr.disabled]="condition ? true : null"></button>
Now, when using our own components, this does not work. When we set null, we actually get null in the components #Input as the value. Any by default set value will be overwritten.
...
#Component({
selector: 'myElement',
templateUrl: './my-element.component.html'
})
export class MyElementComponent {
#Input() type: string = 'default';
...
<myElment [type]="condition ? 'something' : null"></myElement>
So, whenever we read the type in the component, we get null instead of the 'default' value which was set.
I tried to find a way to get the original default value, but did not find it. It is existing in the ngBaseDef when accessed in constructor time, but this is not working in production. I expected ngOnChanges to give me the real (default) value in the first change that is done and therefore be able to prevent that null is set, but the previousValue is undefined.
We came up with some ways to solve this:
defining a default object and setting for every input the default value when its null
addressing the DOM element in the template again, instead of setting null
<myElement #myelem [type]="condition ? 'something' : myelem.type"></myElement>
defining set / get for every input to prevent null setting
_type: string = 'default';
#Input()
set type(v: string) {if (v !== null) this._type = v;}
get type() { return this._type; }
but are curious, if there are maybe others who have similar issues and how it got fixed. Also I would appreciate any other idea which is maybe more elegant.
Thanks!
There is no standard angular way, because many times you would want null or undefined as value. Your ideas are not bad solutions. I got a couple more
I suppose you can also use the ngOnChanges hook for this:
#Input()
type: string = 'defaultType';
ngOnChanges(changes: SimpleChanges): void {
// == null to also match undefined
if (this.type == null) {
this.type = 'defaultType';
}
}
Or using Observables:
private readonly _type$ = new BehaviorSubject('defaultType');
readonly type$ = this._type$.pipe(
map((type) => type == null ? 'defaultType' : type)
);
#Input()
set type(type: string) {
this._type$.next(type);
}
Or create your own decorator playground
function Default(value: any) {
return function(target: any, key: string | symbol) {
const valueAccessor = '__' + key.toString() + '__';
Object.defineProperty(target, key, {
get: function () {
return this[valueAccessor] != null ? this[valueAccessor] : value
},
set: function (next) {
if (!Object.prototype.hasOwnProperty.call(this, valueAccessor)) {
Object.defineProperty(this, valueAccessor, {
writable: true,
enumerable: false
});
}
this[valueAccessor] = next;
},
enumerable: true
});
};
}
which you can use like this:
#Input()
#Default('defaultType')
type!: string;
Just one more option (perhaps simpler if you don't want to implement your own custom #annotation) based off Poul Krujit solution:
const DEFAULT_VALUE = 'default';
export class MyElementComponent {
typeWrapped = DEFAULT_VALUE;
#Input()
set type(selected: string) {
// this makes sure only truthy values get assigned
// so [type]="null" or [type]="undefined" still go to the default.
if (selected) {
this.typeWrapped = selected;
} else {
this.typeWrapped = DEFAULT_VALUE;
}
}
get type() {
return this.typeWrapped;
}
}
If you need to do this for multiple inputs, you can also use a custom pipe instead of manually defining the getter/setter and default for each input. The pipe can contain the logic and defaultArg to return the defaultArg if the input is null.
i.e.
// pipe
#Pipe({name: 'ifNotNullElse'})
export class IfNotNullElsePipe implements PipeTransform {
transform(value: string, defaultVal?: string): string {
return value !== null ? value : defaultVal;
}
}
<!-- myElem template -->
<p>Type Input: {{ type | ifNotNullElse: 'default' }}</p>
<p>Another Input: {{ anotherType | ifNotNullElse: 'anotherDefault' }}</p>
I noticed that type="url" gets ignored in Angular 8 when using Reactive Form. I am trying to validate the URL/URI using RFC2396 since the form gets sent to a .NET Core API.
I think .NET Core uses Uri.CheckSchemaName with RFC2396 and I wanted to do the same validation in Angular and avoid having two different RegEx to validate the URL/URI.
RFC2396
Here is my Custom Angular Validator
import { ValidatorFn, AbstractControl } from '#angular/forms';
export function urlValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const uriRegEx = RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?', 'g');
const result = uriRegEx.test(control.value);
if (!result) {
return { 'invalid-url': true };
}
return null;
};
}
The result is always true no matter what control.value is.
Disclaimer: Did my best looking for an answer and avoid the infamous duplicated.
Stackblitz Code