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

Issue:
Allow passing arbitrary data to fields/widgets through root registry
We'd like to pass some data to all our custom fields/widgets from the Form root to make some fields/widgets context aware. I think the `registry` object could be utilised for this.

This would make it possible to implement any context aware fields/widgets. This would be useful for example for passing translations (at least for now, since there is no support for translations yet).

Now we pass registry fields and widgets like so:

```
<Form
  fields={fields}
  widgets={widgets}
/>
```

What we'd like to have is this:

```
<Form
  registry={
      fields: fields,
      widgets: widgets,
      someArbitraryObject: someArbitraryObject}
/>
```

Pull request description:
Context property
Closes #278

Patch:
diff --git a/README.md b/README.md
index 996e967c1..6a1638a57 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
      - [Custom field components](#custom-field-components)
         - [Field props](#field-props)
         - [The registry object](#the-registry-object)
+        - [The formContext object](#the-formcontext-object)
      - [Custom SchemaField](#custom-schemafield)
      - [Custom titles](#custom-titles)
      - [Custom descriptions](#custom-descriptions)
@@ -592,6 +593,7 @@ The following props are passed to a custom field template component:
 - `required`: A boolean value stating if the field is required;
 - `readonly`: A boolean value stating if the field is read-only;
 - `displayLabel`: A boolean value stating if the label should be rendered or not. This is useful for nested fields in arrays where you don't want to clutter the UI.
+- `formContext`: The `formContext` object that you passed to Form.
 
 > Note: you can only define a single field template for a form. If you need many, it's probably time to look for [custom fields](#custom-field-components) instead.
 
@@ -643,6 +645,7 @@ The following props are passed to custom widget components:
 - `readonly`: `true` if the widget is read-only;
 - `onChange`: The value change event handler; call it with the new value everytime it changes;
 - `options`: A map of options passed as a prop to the component (see [Custom widget options](#custom-widget-options)).
+- `formContext`: The `formContext` object that you passed to Form.
 
 > Note: Prior to v0.35.0, the `options` prop contained the list of options (`label` and `value`) for `enum` fields. Since v0.35.0, it now exposes this list as the `enumOptions` property within the `options` object.
 
@@ -769,7 +772,7 @@ render((
 ), document.getElementById("app"));
 ```
 
-> Note: Registered fields can be reused accross the entire schema.
+> Note: Registered fields can be reused across the entire schema.
 
 #### Field props
 
@@ -781,6 +784,7 @@ A field component will always be passed the following props:
  - `formData`: The data for this field;
  - `errorSchema`: The tree of errors for this field and its children;
  - `registry`: A [registry](#the-registry-object) object (read next).
+ - `formContext`: A [formContext](#the-formcontext-object) object (read next next).
 
 #### The `registry` object
 
@@ -789,9 +793,14 @@ The `registry` is an object containing the registered custom fields and widgets
  - `fields`: The [custom registered fields](#custom-field-components). By default this object contains the standard `SchemaField`, `TitleField` and `DescriptionField` components;
  - `widgets`: The [custom registered widgets](#custom-widget-components), if any;
  - `definitions`: The root schema [definitions](#schema-definitions-and-references), if any.
+ - `formContext`: The [formContext](#the-formcontext-object) object.
 
 The registry is passed down the component tree, so you can access it from your custom field and `SchemaField` components.
 
+#### The `formContext` object
+
+You can provide a `formContext` object to the Form, which is passed down to all fields and widgets (including [TitleField](#custom-titles) and [DescriptionField](#custom-descriptions)). Useful for implementing context aware fields and widgets.
+
 ### Custom SchemaField
 
 **Warning:** This is a powerful feature as you can override the whole form behavior and easily mess it up. Handle with care.
diff --git a/src/components/Form.js b/src/components/Form.js
index 0f33e8e2e..d687ef595 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -132,6 +132,7 @@ export default class Form extends Component {
       FieldTemplate: this.props.FieldTemplate,
       widgets: this.props.widgets || {},
       definitions: this.props.schema.definitions || {},
+      formContext: this.props.formContext || {},
     };
   }
 
@@ -211,7 +212,6 @@ if (process.env.NODE_ENV !== "production") {
     noValidate: PropTypes.bool,
     liveValidate: PropTypes.bool,
     safeRenderCompletion: PropTypes.bool,
+    formContext: PropTypes.object,
   };
 }
-
-export default Form;
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index e090615d3..1c08cbac7 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -410,7 +410,8 @@ if (process.env.NODE_ENV !== "production") {
       ])).isRequired,
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
-    })
+      formContext: PropTypes.object.isRequired
+    }),
   };
 }
 
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index dc53f68b3..a001185ac 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -32,7 +32,7 @@ function BooleanField(props) {
     onChange
   } = props;
   const {title} = schema;
-  const {widgets} = registry;
+  const {widgets, formContext} = registry;
   const widget = uiSchema["ui:widget"];
   const commonProps = {
     schema,
@@ -43,6 +43,8 @@ function BooleanField(props) {
     required,
     disabled,
     readonly,
+    registry,
+    formContext,
   };
   if (widget) {
     const Widget = getAlternativeWidget(schema, widget, widgets);
@@ -68,6 +70,7 @@ if (process.env.NODE_ENV !== "production") {
       ])).isRequired,
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
+      formContext: PropTypes.object.isRequired,
     })
   };
 }
diff --git a/src/components/fields/DescriptionField.js b/src/components/fields/DescriptionField.js
index 4d63845f1..f9848a968 100644
--- a/src/components/fields/DescriptionField.js
+++ b/src/components/fields/DescriptionField.js
@@ -18,7 +18,7 @@ if (process.env.NODE_ENV !== "production") {
     description: PropTypes.oneOfType([
       PropTypes.string,
       PropTypes.element,
-    ]),
+    ])
   };
 }
 
diff --git a/src/components/fields/NumberField.js b/src/components/fields/NumberField.js
index 7c9e11800..f1b84c6c7 100644
--- a/src/components/fields/NumberField.js
+++ b/src/components/fields/NumberField.js
@@ -18,6 +18,7 @@ if (process.env.NODE_ENV !== "production") {
     onChange: PropTypes.func.isRequired,
     formData: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     required: PropTypes.bool,
+    formContext: PropTypes.object.isRequired,
   };
 }
 
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 5682db0f2..27485da5d 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -93,7 +93,7 @@ class ObjectField extends Component {
       disabled,
       readonly
     } = this.props;
-    const {definitions, fields} = this.props.registry;
+    const {definitions, fields, formContext} = this.props.registry;
     const {SchemaField, TitleField, DescriptionField} = fields;
     const schema = retrieveSchema(this.props.schema, definitions);
     const title = schema.title || name;
@@ -117,12 +117,13 @@ class ObjectField extends Component {
         {title ? <TitleField
                    id={`${idSchema.$id}__title`}
                    title={title}
-                   required={required} /> : null}
+                   required={required}
+                   formContext={formContext}/> : null}
         {schema.description ?
           <DescriptionField
             id={`${idSchema.$id}__description`}
             description={schema.description}
-          /> : null}
+            formContext={formContext} /> : null}
         {
         orderedProperties.map((name, index) => {
           return (
@@ -163,6 +164,7 @@ if (process.env.NODE_ENV !== "production") {
       ])).isRequired,
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
+      formContext: PropTypes.object.isRequired,
     })
   };
 }
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 2ede56349..f09a206e2 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -115,6 +115,7 @@ if (process.env.NODE_ENV !== "production") {
     required: PropTypes.bool,
     readonly: PropTypes.bool,
     displayLabel: PropTypes.bool,
+    formContext: PropTypes.object,
   };
 }
 
@@ -127,7 +128,7 @@ DefaultTemplate.defaultProps = {
 
 function SchemaField(props) {
   const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
-  const {definitions, fields, FieldTemplate = DefaultTemplate} = registry;
+  const {definitions, fields, formContext, FieldTemplate = DefaultTemplate} = registry;
   const schema = retrieveSchema(props.schema, definitions);
   const FieldComponent = getFieldComponent(schema, uiSchema, fields);
   const {DescriptionField} = fields;
@@ -156,7 +157,8 @@ function SchemaField(props) {
     <FieldComponent {...props}
       schema={schema}
       disabled={disabled}
-      readonly={readonly} />
+      readonly={readonly}
+      formContext={formContext} />
   );
 
   const {type} = schema;
@@ -175,7 +177,9 @@ function SchemaField(props) {
   ].join(" ").trim();
 
   const fieldProps = {
-    description: <DescriptionField id={id + "__description"} description={description} />,
+    description: <DescriptionField id={id + "__description"}
+                                   description={description}
+                                   formContext={formContext} />,
     help: <Help help={help} />,
     errors: <ErrorList errors={errors} />,
     id,
@@ -185,6 +189,7 @@ function SchemaField(props) {
     readonly,
     displayLabel,
     classNames,
+    formContext,
   };
 
   return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
@@ -214,6 +219,7 @@ if (process.env.NODE_ENV !== "production") {
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
       FieldTemplate: PropTypes.func,
+      formContext: PropTypes.object.isRequired,
     })
   };
 }
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 73290984e..4542adf58 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -24,7 +24,7 @@ function StringField(props) {
     onChange
   } = props;
   const {title} = schema;
-  const {widgets} = registry;
+  const {widgets, formContext} = registry;
   const widget = uiSchema["ui:widget"] || schema.format;
   const placeholder = uiSchema["ui:placeholder"] || "";
   const commonProps = {
@@ -36,6 +36,7 @@ function StringField(props) {
     required,
     disabled,
     readonly,
+    formContext,
   };
   if (Array.isArray(schema.enum)) {
     const enumOptions = optionsList(schema);
@@ -69,7 +70,9 @@ if (process.env.NODE_ENV !== "production") {
       ])).isRequired,
       fields: PropTypes.objectOf(PropTypes.func).isRequired,
       definitions: PropTypes.object.isRequired,
+      formContext: PropTypes.object.isRequired,
     }),
+    formContext: PropTypes.object.isRequired,
     required: PropTypes.bool,
     disabled: PropTypes.bool,
     readonly: PropTypes.bool,
diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js
index d82d5ab59..7c5544409 100644
--- a/src/components/widgets/BaseInput.js
+++ b/src/components/widgets/BaseInput.js
@@ -10,6 +10,7 @@ function BaseInput(props) {
     onChange,
     options,  // eslint-disable-line
     schema,   // eslint-disable-line
+    formContext,  // eslint-disable-line
     ...inputProps
   } = props;
   return (
diff --git a/src/utils.js b/src/utils.js
index b4fa48d60..36372dc64 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -82,6 +82,7 @@ export function getDefaultRegistry() {
     },
     widgets: {},
     definitions: {},
+    formContext: {}
   };
 }
 


Test patch:
diff --git a/test/FormContext_test.js b/test/FormContext_test.js
new file mode 100644
index 000000000..705a6095d
--- /dev/null
+++ b/test/FormContext_test.js
@@ -0,0 +1,97 @@
+import React from "react";
+import { expect } from "chai";
+
+import { createFormComponent, createSandbox } from "./test_utils";
+
+describe("FormContext", () => {
+  let sandbox;
+
+  beforeEach(() => {
+    sandbox = createSandbox();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  const schema = {type: "string"};
+
+  const formContext = {foo: "bar"};
+
+  const CustomComponent = function(props) {
+    return (<div id={props.formContext.foo} />);
+  };
+
+  it("should be passed to Form", () => {
+    const {comp} = createFormComponent({
+      schema: schema,
+      formContext
+    });
+    expect(comp.props.formContext).eq(formContext);
+  });
+
+  it("should be passed to custom field", () => {
+    const fields = {custom: CustomComponent};
+
+    const {node} = createFormComponent({
+      schema: schema,
+      uiSchema: {"ui:field": "custom"},
+      fields,
+      formContext
+    });
+
+    expect(node.querySelectorAll("#" + formContext.foo))
+      .to.have.length.of(1);
+  });
+
+  it("should be passed to custom widget", () => {
+    const widgets = {custom: CustomComponent};
+
+    const {node} = createFormComponent({
+      schema: {type: "string"},
+      uiSchema: {"ui:widget": "custom"},
+      widgets,
+      formContext
+    });
+
+    expect(node.querySelectorAll("#" + formContext.foo))
+      .to.have.length.of(1);
+  });
+
+  it("should be passed to custom TitleField", () => {
+    const fields = {TitleField: CustomComponent};
+
+    const {node} = createFormComponent({
+      schema: {
+        type: "object",
+        title: "A title",
+        properties: {
+          prop: {
+            type:  "string"
+          }
+        }
+      },
+      fields,
+      formContext
+    });
+
+    expect(node.querySelectorAll("#" + formContext.foo))
+      .to.have.length.of(1);
+  });
+
+  it("should be passed to custom DescriptionField", () => {
+    const fields = {DescriptionField: CustomComponent};
+
+    const {node} = createFormComponent({
+      schema: {type: "string"},
+      uiSchema: {
+        "ui:description": "A description"
+      },
+      fields,
+      formContext
+    });
+
+    expect(node.querySelectorAll("#" + formContext.foo))
+      .to.have.length.of(1);
+  });
+});