Repository: mozilla-services/react-jsonschema-form (js)

Issue:
ui:emptyValue=null does not work with number inputs
### Prerequisites

- [x] I have read the [documentation](https://react-jsonschema-form.readthedocs.io/);
- [x] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers.
- [x] Ideally, I'm providing a [sample JSFiddle](https://jsfiddle.net/n1k0/f2y3fq7L/6/) or a [shared playground link](https://mozilla-services.github.io/react-jsonschema-form/) demonstrating the issue.

### Description

Using ui:emptyValue=`null` with a type=`["number", "null"]` property doesn't actually give you null back. This appears to be an issue with when `util.js:asNumber(null)` ends up using `new Number(null)` which yields 0.

### Steps to Reproduce

Make the age field empty. With the default widgets and current state, this is impossible, but it would be possible if `asNumber` is fixed. I have a custom Widget that passes null up thru `onChange` that reproduces this as well. 

[Playground Link](https://mozilla-services.github.io/react-jsonschema-form/#eyJmb3JtRGF0YSI6eyJmaXJzdE5hbWUiOiJDaHVjayIsImxhc3ROYW1lIjoiTm9ycmlzIiwiYWdlIjowLCJiaW8iOm51bGwsInBhc3N3b3JkIjoibm9uZWVkIn0sInNjaGVtYSI6eyJ0aXRsZSI6IkEgcmVnaXN0cmF0aW9uIGZvcm0gKG51bGxhYmxlKSIsImRlc2NyaXB0aW9uIjoiQSBzaW1wbGUgZm9ybSBleGFtcGxlIHVzaW5nIG51bGxhYmxlIHR5cGVzIiwidHlwZSI6Im9iamVjdCIsInJlcXVpcmVkIjpbImZpcnN0TmFtZSIsImxhc3ROYW1lIl0sInByb3BlcnRpZXMiOnsiZmlyc3ROYW1lIjp7InR5cGUiOiJzdHJpbmciLCJ0aXRsZSI6IkZpcnN0IG5hbWUiLCJkZWZhdWx0IjoiQ2h1Y2sifSwibGFzdE5hbWUiOnsidHlwZSI6InN0cmluZyIsInRpdGxlIjoiTGFzdCBuYW1lIn0sImFnZSI6eyJ0eXBlIjpbImludGVnZXIiLCJudWxsIl0sInRpdGxlIjoiQWdlIn0sImJpbyI6eyJ0eXBlIjpbInN0cmluZyIsIm51bGwiXSwidGl0bGUiOiJCaW8ifSwicGFzc3dvcmQiOnsidHlwZSI6InN0cmluZyIsInRpdGxlIjoiUGFzc3dvcmQiLCJtaW5MZW5ndGgiOjN9LCJ0ZWxlcGhvbmUiOnsidHlwZSI6InN0cmluZyIsInRpdGxlIjoiVGVsZXBob25lIiwibWluTGVuZ3RoIjoxMH19fSwidWlTY2hlbWEiOnsiZmlyc3ROYW1lIjp7InVpOmF1dG9mb2N1cyI6dHJ1ZSwidWk6ZW1wdHlWYWx1ZSI6IiJ9LCJhZ2UiOnsidWk6d2lkZ2V0IjoidXBkb3duIiwidWk6dGl0bGUiOiJBZ2Ugb2YgcGVyc29uIiwidWk6ZGVzY3JpcHRpb24iOiIoZWFydGhpYW4geWVhcikiLCJ1aTplbXB0eVZhbHVlIjpudWxsfSwiYmlvIjp7InVpOndpZGdldCI6InRleHRhcmVhIiwidWk6cGxhY2Vob2xkZXIiOiJMZWF2aW5nIHRoaXMgZmllbGQgZW1wdHkgd2lsbCBjYXVzZSBmb3JtRGF0YSBwcm9wZXJ0eSB0byBiZSBgbnVsbGAiLCJ1aTplbXB0eVZhbHVlIjpudWxsfSwicGFzc3dvcmQiOnsidWk6d2lkZ2V0IjoicGFzc3dvcmQiLCJ1aTpoZWxwIjoiSGludDogTWFrZSBpdCBzdHJvbmchIn0sImRhdGUiOnsidWk6d2lkZ2V0IjoiYWx0LWRhdGV0aW1lIn0sInRlbGVwaG9uZSI6eyJ1aTpvcHRpb25zIjp7ImlucHV0VHlwZSI6InRlbCJ9fX19)

#### Expected behavior

For the value in formData to be `null`.

#### Actual behavior

The value in formData is `0`.

### Version

1.4.1

Pull request:
Make numbers nullable by allowing null values to be echoed from asNumber()
### Reasons for making this change

To allow ui:emptyValue=null for number fields. Fixes #1268 

### Checklist

* [ ] **I'm updating documentation**
  - [ ] I've [checked the rendering](https://react-jsonschema-form.readthedocs.io/en/latest/#contributing) of the Markdown text I've added
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [x] I've updated [docs](https://react-jsonschema-form.readthedocs.io/) if needed
  - [x] I've run `npm run cs-format` on my branch to conform my code to [prettier](https://github.com/prettier/prettier) coding style
* [x] **I'm adding a new feature**
  - [x] I've updated the playground with an example use of the feature

Patch:
diff --git a/playground/samples/nullable.js b/playground/samples/nullable.js
index 1068ac974..2ccc62de9 100644
--- a/playground/samples/nullable.js
+++ b/playground/samples/nullable.js
@@ -15,7 +15,7 @@ module.exports = {
         title: "Last name",
       },
       age: {
-        type: "integer",
+        type: ["integer", "null"],
         title: "Age",
       },
       bio: {
@@ -43,6 +43,7 @@ module.exports = {
       "ui:widget": "updown",
       "ui:title": "Age of person",
       "ui:description": "(earthian year)",
+      "ui:emptyValue": null,
     },
     bio: {
       "ui:widget": "textarea",
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 31dc31580..6f583ba9b 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -7,6 +7,7 @@ import {
   isSelect,
   optionsList,
   getDefaultRegistry,
+  hasWidget,
 } from "../../utils";
 
 function StringField(props) {
@@ -29,7 +30,10 @@ function StringField(props) {
   const { title, format } = schema;
   const { widgets, formContext } = registry;
   const enumOptions = isSelect(schema) && optionsList(schema);
-  const defaultWidget = format || (enumOptions ? "select" : "text");
+  let defaultWidget = enumOptions ? "select" : "text";
+  if (format && hasWidget(schema, format, widgets)) {
+    defaultWidget = format;
+  }
   const { widget = defaultWidget, placeholder = "", ...options } = getUiOptions(
     uiSchema
   );
diff --git a/src/utils.js b/src/utils.js
index a4b2c8af1..22f08e186 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -123,6 +123,22 @@ export function getWidget(schema, widget, registeredWidgets = {}) {
   throw new Error(`No widget "${widget}" for type "${type}"`);
 }
 
+export function hasWidget(schema, widget, registeredWidgets = {}) {
+  try {
+    getWidget(schema, widget, registeredWidgets);
+    return true;
+  } catch (e) {
+    if (
+      e.message &&
+      (e.message.startsWith("No widget") ||
+        e.message.startsWith("Unsupported widget"))
+    ) {
+      return false;
+    }
+    throw e;
+  }
+}
+
 function computeDefaults(schema, parentDefaults, definitions = {}) {
   // Compute the defaults recursively: give highest priority to deepest nodes.
   let defaults = parentDefaults;
@@ -256,6 +272,9 @@ export function asNumber(value) {
   if (value === "") {
     return undefined;
   }
+  if (value === null) {
+    return null;
+  }
   if (/\.$/.test(value)) {
     // "3." can't really be considered a number even if it parses in js. The
     // user is most likely entering a float.
diff --git a/src/validate.js b/src/validate.js
index f20afc79e..526bceef9 100644
--- a/src/validate.js
+++ b/src/validate.js
@@ -14,6 +14,7 @@ function createAjvInstance() {
     allErrors: true,
     multipleOfPrecision: 8,
     schemaId: "auto",
+    unknownFormats: "ignore",
   });
 
   // add custom formats
@@ -215,13 +216,7 @@ export default function validateFormData(
     typeof validationError.message === "string" &&
     validationError.message.includes("no schema with key or ref ");
 
-  const unknownFormat =
-    validationError &&
-    validationError.message &&
-    typeof validationError.message === "string" &&
-    validationError.message.includes("unknown format");
-
-  if (noProperMetaSchema || unknownFormat) {
+  if (noProperMetaSchema) {
     errors = [
       ...errors,
       {
@@ -235,7 +230,7 @@ export default function validateFormData(
 
   let errorSchema = toErrorSchema(errors);
 
-  if (noProperMetaSchema || unknownFormat) {
+  if (noProperMetaSchema) {
     errorSchema = {
       ...errorSchema,
       ...{


Test patch:
diff --git a/test/Form_test.js b/test/Form_test.js
index 388ed1e66..45cc89e7e 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -1984,13 +1984,13 @@ describe("Form", () => {
       const formProps = {
         liveValidate: true,
         formData: {
-          areaCode: 123,
+          areaCode: "123455",
         },
         schema: {
           type: "object",
           properties: {
             areaCode: {
-              type: "number",
+              type: "string",
               format: "area-code",
             },
           },
@@ -2007,22 +2007,20 @@ describe("Form", () => {
 
       const { comp } = createFormComponent(formProps);
 
-      expect(comp.state.errorSchema).eql({
-        $schema: {
-          __errors: [
-            'unknown format "area-code" is used in schema at path "#/properties/areaCode"',
-          ],
-        },
-      });
+      expect(comp.state.errorSchema).eql({});
 
       setProps(comp, {
         ...formProps,
         customFormats: {
-          "area-code": /\d{3}/,
+          "area-code": /^\d{3}$/,
         },
       });
 
-      expect(comp.state.errorSchema).eql({});
+      expect(comp.state.errorSchema).eql({
+        areaCode: {
+          __errors: ['should match format "area-code"'],
+        },
+      });
     });
   });
 
diff --git a/test/utils_test.js b/test/utils_test.js
index 2f1ecc369..5acfcc229 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -342,6 +342,10 @@ describe("utils", () => {
     it("should return undefined if the input is empty", () => {
       expect(asNumber("")).eql(undefined);
     });
+
+    it("should return null if the input is null", () => {
+      expect(asNumber(null)).eql(null);
+    });
   });
 
   describe("orderProperties()", () => {
diff --git a/test/validate_test.js b/test/validate_test.js
index 3ea9d40fe..696ed0631 100644
--- a/test/validate_test.js
+++ b/test/validate_test.js
@@ -174,15 +174,9 @@ describe("Validation", () => {
         },
       };
 
-      it("should return a validation error if unknown string format is used", () => {
+      it("should not return a validation error if unknown string format is used", () => {
         const result = validateFormData({ phone: "800.555.2368" }, schema);
-        const errMessage =
-          'unknown format "phone-us" is used in schema at path "#/properties/phone"';
-
-        expect(result.errors[0].stack).include(errMessage);
-        expect(result.errorSchema).to.eql({
-          $schema: { __errors: [errMessage] },
-        });
+        expect(result.errors.length).eql(0);
       });
 
       it("should return a validation error about formData", () => {