Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
280 views
in Technique[技术] by (71.8m points)

javascript - Angular2 forms : validator with interrelated fields

Given a form where one can enter either a city name or its latitude and longitude. The form would validate if city name is filled OR if both latitude AND longitude are filled. Latitude and longitude, if filled, must be numbers.

I could create a FormGroup with those three fields and do one custom validators...

function fatValidator(group: FormGroup) { 
    // if cityName is present : is valid
    // else if lat and lng are numbers : is valid
    // else : is not valid
}

builder.group({
    cityName: [''],
    lat: [''],
    lng: ['']
},
{
    validators: fatValidator
});

...but I would like to take advantage of validators composition (e.g testing latitude and longitude to be valid numbers at the fields level in one validator and test the interrelation at the group level in another validator).

I have tested several options but I am stuck with the fact that a group is valid if all its fields are valid. The following construction seems not to be the proper way to approach the problem :

function isNumber(control: FormControl) { ... }
function areAllFilled(group: FormGroup) { ... }
function oneIsFilledAtLeast(group: FormGroup) { ... }

builder.group({
    cityName: [''],
    localisation: builder.group({
        lat: ['', Validators.compose([Validators.minLength(1), isNumber])],
        lng: ['', Validators.compose([Validators.minLength(1), isNumber])]
    },
    {
        validators: areAllFilled
    })
},
{
    validators: oneIsFilledAtLeast
});

How would you do that with Angular2 Is it even possible ?

EDIT

Here is an example of how the fatValidator could be implemented. As you can see it is not reusable and harder to test than composed validators :

function fatValidator (group: FormGroup) {
    const coordinatesValidatorFunc = Validators.compose([
        Validators.required, 
        CustomValidators.isNumber
    ]);
    const cityNameControl = group.controls.cityName;
    const latControl = group.controls.lat;
    const lngControl = group.controls.lng;

    const cityNameValidationResult = Validators.required(cityNameControl);
    const latValidationResult = coordinatesValidatorFunc(latControl);
    const lngValidationResult = coordinatesValidatorFunc(lngControl);

    const isCityNameValid = !cityNameValidationResult;
    const isLatValid = !latValidationResult;
    const isLngValid = !lngValidationResult;

    if (isCityNameValid) {
        return null;
    }

    if (isLatValid && isLngValid) {
        return null;
    }

    if (!isCityNameValid && !isLatValid && !isLngValid) {
        return { cityNameOrCoordinatesRequired: true, latAndLngMustBeNumbers: true };
    }

    return Object.assign({}, 
        { cityName: cityNameValidationResult }, 
        { lat: latValidationResult }, 
        { lng: lngValidationResult }
    );
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Using the final release or new of Angular, I have written a reusable method to add a Conditional Required- or other Validation -to a given set of Controls.

export class CustomValidators {
    static controlsHaveValueCheck(controlKeys: Array<string>, formGroup: FormGroup): Array<boolean> {
        return controlKeys.map((item) => {
            // reset any errors already set (ON ALL GIVEN KEYS).
            formGroup.controls[item].setErrors(null);

            // Checks for empty string and empty array.
            let hasValue = (formGroup.controls[item].value instanceof Array) ? formGroup.controls[item].value.length > 0 :
                !(formGroup.controls[item].value === "");
            return (hasValue) ? false : true;
        });
    }


    static conditionalAnyRequired(controlKeys: Array<string>): ValidatorFn {
        return (control: FormControl): {[key: string]: any} => {
            let formGroup = control.root;
            if (formGroup instanceof FormGroup) {

                // Only check if all FormControls are siblings(& present on the nearest FormGroup)
                if (controlKeys.every((item) => {
                        return formGroup.contains(item);
                    })) {
                    let result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);

                    // If any item is valid return null, if all are invalid return required error.
                    return (result.some((item) => {
                        return item === false;
                    })) ? null : {required: true};
                }
            }
            return null;
        }
    }
}

This can be used in your code like this:

this.form = new FormGroup({
    'cityName': new FormControl('', 
        CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])),
    'lat': new FormControl('', 
        Validators.compose([Validators.minLength(1),
            CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng']))),
    'lng': new FormControl('', 
        Validators.compose([Validators.minLength(1),
            CustomValidators.conditionalAnyRequired(['cityName', 'lat', 'lng'])))
})

This would make any of 'city', 'lat' or 'lng' required.

Additionally, if you wanted either 'city' or 'lat' and 'lng' to be required you can include an additional validator such as this:

static conditionalOnRequired(conditionalControlKey: string, controlKeys: Array<string>): ValidatorFn {
    return (control: FormControl): {[key: string]: any} => {
        let formGroup = control.root;
        if (formGroup instanceof FormGroup) {
            if (controlKeys.every((item) => {
                    return formGroup.contains(item);
                }) && formGroup.contains(conditionalControlKey)) {
                let firstControlHasValue = (formGroup.controls[conditionalControlKey].value instanceof Array) ? formGroup.controls[conditionalControlKey].value.length > 0 :
                        !(formGroup.controls[conditionalControlKey].value === ""),
                    result = CustomValidators.controlsHaveValueCheck(controlKeys, formGroup);
                formGroup.controls[conditionalControlKey].setErrors(null); // Also reset the conditional Control...
                if (firstControlHasValue && formGroup.controls[conditionalControlKey].value !== false) {// also checks for false (for unchecked checkbox value)...
                    return (result.every((invalid) => {
                        return invalid === false;
                    })) ? null : {required: true};
                }
            }
        }
        return null;
    }
}

This method will make a set of form controls 'required' based on the value of the conditionalControlKey, i.e. if conditionalControlKey has a value all other controls in controlKeys Array are not required, otherwise the all are required.

I hope this isn't too convoluted for anyone to follow- I am sure these code snippets can be improved, but I feel they aptly demonstrate one way of going about this.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

56.8k users

...