gitana / alpaca

Alpaca provides the easiest way to generate interactive HTML5 forms for web and mobile applications. It uses JSON Schema and simple Handlebars templates to generate great looking, dynamic user interfaces on top of Twitter Bootstrap, jQuery UI, jQuery Mobile and HTML5.
http://www.alpacajs.org
Other
1.29k stars 371 forks source link

How to add errors to fields after server-side validation? #579

Open apaokin opened 6 years ago

apaokin commented 6 years ago

I have this code. I just want to add errors here: promise.fail(function(arg) { //ADDING SOME ERRORS }); 1)I wish to find out how to add errors to specific field 2) I may want to remove them in future 3) Is there built-in support for rendering all or specific errors in one div, for example?


var attachable_type =   "<%=@attach_to[:class_name]%>";
var attachable_id =   "<%=@attach_to[:ids].first%>";

$("#create_comment").alpaca({
    "schema": {
        "title": "<%=t('comments.write_comment')%>",
        "type": "object",
        "properties": {
          "comment":{
            "type": "object",
            "properties":{
              "text": {
                  "title": "<%=Comments::Comment.human_attribute_name(:text)%>"
              },
              "attachable_type":{
                "default": attachable_type
              },
              "attachable_id":{
                "default": attachable_id
              }
            }
          }
        }
    },
        "options":{
            "fields":{
        "comment":{
          "fields":{
            "text": {
                    "type": "textarea",
                    "rows": 5,
                    "label": "",
                    "wordlimit": 250
            },
            "attachable_type":{
              "type": "hidden"
            },
            "attachable_id":{
              "type": "hidden"
            }
          }
        },
            },
      "form": {
            "attributes": {
                "method": "post",
                "action": "<%=comments_path %>"
            },
            "buttons": {
                "submit": {
                    "title": "<%= t 'buttons.save'%>",
                    "click": function(e) {
                        var promise = this.ajaxSubmit();
                        promise.done(function() {
                            alert("Success");
                        });
                        promise.fail(function(arg) {
                          alert("FAIL");
                        });
                        promise.always(function() {
                            //alert("Completed");
                        });
                    }
                }
            }
        }
        }
});

Thanks.

WhileTrueEndWhile commented 6 years ago

You could define a custom validator that sends the data to the server: See http://www.alpacajs.org/docs/api/validation.html

Something like this...

    "options": {
        "validator": function(callback) {
            const value = this.getValue();
            const request = new XmlHttpRequest();
            request.load = function() {
                if (request.status >= 200 && request.status <= 299) {
                    callback({ "status": true });
                } else {
                    callback({
                        "status": false,
                        "message": request.responseText
                    });
                }
            }
            request.open(...);
            request.send(JSON.stringify(value));
        }
    }
apaokin commented 6 years ago

Thank you for answer. I can modify my question: "How to validate on button click only"? I found an example here, but validation on button click and validation on field changing are used. Also I want to submit and get errors using only one request

WhileTrueEndWhile commented 6 years ago

Okay, did you mean this issue: https://github.com/gitana/alpaca/issues/563? The problem is that this trick only works for primitive data types. Therefore, these properties must be set recursively. If you're worried now, worry about architecture. In my opinion, you should always encapsulate Alpaca and build your own classes around it, so that transformations can be defined in general and are not made for every application. Although this intervention is separately ugly. But the recursive code should also be done quickly (not tested yet):

function setValidate(control, validate) {
    control.options.validate = validate;
    Object.keys(control.childrenByPropertyId).forEach(k => {
        setValidate(control.childrenByPropertyId[k], validate);
    });
}

This function could then be used as follows (as in the above linked issue):

    "options": {
        "validator": function(callback) {
            const value = this.getValue();
            const request = new XmlHttpRequest();
            request.load = function() {
                if (request.status >= 200 && request.status <= 299) {
                    callback({ "status": true });
                } else {
                    callback({
                        "status": false,
                        "message": request.responseText
                    });
                }
            }
            request.open(...);
            request.send(JSON.stringify(value));
        },
        "form": {
            "buttons": {
                "submit": {
                    "click": function() {
                        setValidate(this.topControl, true);
                        this.validate(true);
                        this.refreshValidationState(true);
                        if (this.isValid(true)) {
                            window.alert(JSON.stringify(this.getValue()));   
                        }
                    }
                }
            }
        }
    },
    "postRender": function() {
        setValidate(this.topControl, false);
        this.validate(true);
        this.refreshValidationState(true);
    }
}

I hope this improvised code works halfway :)

Update:

This seems to work, but arrays are very buggy (commented out):

function setValidateOnConfig(config, validate) {
    if (config.schema === undefined) config.schema = {};
    if (config.options === undefined) config.options = {};
    setValidateOnConfigImpl(config.schema, config.options, validate);
}

function setValidateOnConfigImpl(schema, options, validate) {
    options.validate = false;
    if (schema.properties !== undefined) {
        if (options.fields === undefined) options.fields = {};
        Object.keys(schema.properties).forEach(k => {
            setValidateOnConfigImpl(schema.properties[k], options.fields[k]);
        });
    } /* else if (schema.items !== undefined) {
        if (options.items === undefined) options.items = {};
        setValidateOnConfigImpl(schema.items, options.items, validate);
    } */
}

function setValidateOnControl(control, validate) {
    control.options.validate = validate;
    if (control.childrenByPropertyId !== undefined && Object.keys(control.childrenByPropertyId).length > 0) {
        Object.keys(control.childrenByPropertyId).forEach(k => {
            setValidateOnControl(control.childrenByPropertyId[k], validate);
        });
    } /* else if (control.children !== undefined && control.children.length > 0) {
        control.children.forEach(c => {
            setValidateOnControl(c, validate);
        });
    } */
}

const config = {
    "schema": {
        "type": "object",
        "properties": {
            "number": {
                "type": "number"
            }
        }
    },
    "options": {
        "fields": {
            "number": {
                "label": "Number"
            }
        },
        "validator": function(callback) {
            if (this.options.validate) {
                callback({
                    "status": false,
                    "message": "... Server Response ..."
                });
            } else {
                callback({ "status": this.validate(true) });
            }
        },
        "form": {
            "buttons": {
                "submit": {
                    "click": function() {
                        setValidateOnControl(this.topControl, true);
                        this.validate(true);
                        this.refreshValidationState(true);
                        window.setTimeout(() => {
                            if (this.isValid(true)) {
                                window.alert(JSON.stringify(this.getValue()));   
                            }
                        }, 0);
                    }
                }
            }
        }
    }
}

setValidateOnConfig(config, false);
$("#field1").alpaca(config);

Note:

A validator can also be defined for each individual field and does not have to be defined for all fields.

Example:

options.items.fields.myField.validate = false
options.items.fields.myField.validator = function (callback) { ... }

So you can avoid the recursion and send only the single field to the server. Then you have to overwrite the validate attribute in (e.g.) the button click event handler for one field only.

If you only have the possibility to send a post-request and only if it fails to handle an error, this is of course not so good. Then the validator must send a (data-changing) post-request, which it should not do under any circumstances. That cannot be solved. With this longing, you can only use damage limitation and make sure that the validator is only called when the button has been clicked, which now works at least for the first time. The validator is then called up for each change. So you would have to set validate back to false, which is a bit paradoxical.

The desired functionality is to be welcomed. I hope there is an even more elegant solution.

apaokin commented 6 years ago

Thank you for answer. I decided to show errors after server-side validation in the special div.

streof commented 5 years ago

Great answer from @WhileTrueEndWhile ! In addition, if you also want to perform custom validation on conditional dependencies (e.g number that depends on condition), you can use an observable such as subscribe inside the postRender block:

...
    "postRender": function(control) {
        var condition = control.childrenByPropertyId["condition"];
        var number = control.childrenByPropertyId["number"];

        condition.subscribe(number, function(val) {
            // See answer @WhileTrueEndWhile
            this.validate(true);
            this.refreshValidationState(true);
        });
    }

Helpful links:

P.S. A big thanks to all contributors for this great project! :tada: