Repository: vazco/uniforms (ts)

Issue:
Invalid `id`
Hello,

I've just run the example from: https://github.com/vazco/uniforms/issues/549
In the generated form we have:

```
<div class="ui checkbox"><input id="uniforms-0000-0001-Impact One" name="impact" type="checkbox"><label for="uniforms-0000-0001-Impact One">Impact One</label></div>
```

`uniforms-0000-0001-Impact One` contains a whitespace which is invalid: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id



Pull request description:
Correct attribute id format (fixes #671).
This pull request fixes #671 by escaping select item names with its base64 encoding.

Patch:
diff --git a/packages/uniforms-bootstrap3/src/RadioField.tsx b/packages/uniforms-bootstrap3/src/RadioField.tsx
index cce88872..dfce992e 100644
--- a/packages/uniforms-bootstrap3/src/RadioField.tsx
+++ b/packages/uniforms-bootstrap3/src/RadioField.tsx
@@ -4,6 +4,12 @@ import { connectField } from 'uniforms';
 
 import wrapField from './wrapField';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const Radio = props =>
   wrapField(
     props,
@@ -15,11 +21,11 @@ const Radio = props =>
           `radio${props.inline ? '-inline' : ''}`,
         )}
       >
-        <label htmlFor={`${props.id}-${item}`}>
+        <label htmlFor={`${props.id}-${escape(item)}`}>
           <input
             checked={item === props.value}
             disabled={props.disabled}
-            id={`${props.id}-${item}`}
+            id={`${props.id}-${escape(item)}`}
             name={props.name}
             onChange={() => props.onChange(item)}
             type="radio"
diff --git a/packages/uniforms-bootstrap3/src/SelectField.tsx b/packages/uniforms-bootstrap3/src/SelectField.tsx
index 78a91dac..5eb34c1b 100644
--- a/packages/uniforms-bootstrap3/src/SelectField.tsx
+++ b/packages/uniforms-bootstrap3/src/SelectField.tsx
@@ -4,6 +4,12 @@ import { connectField } from 'uniforms';
 
 import wrapField from './wrapField';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const xor = (item, array) => {
   const index = array.indexOf(item);
   if (index === -1) {
@@ -22,7 +28,7 @@ const renderCheckboxes = props =>
         `checkbox${props.inline ? '-inline' : ''}`,
       )}
     >
-      <label htmlFor={`${props.id}-${item}`}>
+      <label htmlFor={`${props.id}-${escape(item)}`}>
         <input
           checked={
             props.fieldType === Array
@@ -30,7 +36,7 @@ const renderCheckboxes = props =>
               : props.value === item
           }
           disabled={props.disabled}
-          id={`${props.id}-${item}`}
+          id={`${props.id}-${escape(item)}`}
           name={props.name}
           onChange={() =>
             props.onChange(
diff --git a/packages/uniforms-bootstrap4/src/RadioField.tsx b/packages/uniforms-bootstrap4/src/RadioField.tsx
index 39c006cc..5b497e27 100644
--- a/packages/uniforms-bootstrap4/src/RadioField.tsx
+++ b/packages/uniforms-bootstrap4/src/RadioField.tsx
@@ -4,6 +4,12 @@ import { connectField } from 'uniforms';
 
 import wrapField from './wrapField';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const Radio = props =>
   wrapField(
     props,
@@ -15,12 +21,15 @@ const Radio = props =>
           'custom-control-inline': props.inline,
         })}
       >
-        <label htmlFor={`${props.id}-${item}`} className="form-check-label">
+        <label
+          htmlFor={`${props.id}-${escape(item)}`}
+          className="form-check-label"
+        >
           <input
             checked={item === props.value}
             className="form-check-input"
             disabled={props.disabled}
-            id={`${props.id}-${item}`}
+            id={`${props.id}-${escape(item)}`}
             name={props.name}
             onChange={() => props.onChange(item)}
             type="radio"
diff --git a/packages/uniforms-bootstrap4/src/SelectField.tsx b/packages/uniforms-bootstrap4/src/SelectField.tsx
index f490ce82..bbc3a00f 100644
--- a/packages/uniforms-bootstrap4/src/SelectField.tsx
+++ b/packages/uniforms-bootstrap4/src/SelectField.tsx
@@ -4,6 +4,12 @@ import { connectField } from 'uniforms';
 
 import wrapField from './wrapField';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const xor = (item, array) => {
   const index = array.indexOf(item);
   if (index === -1) {
@@ -22,7 +28,7 @@ const renderCheckboxes = props =>
         `checkbox${props.inline ? '-inline' : ''}`,
       )}
     >
-      <label htmlFor={`${props.id}-${item}`}>
+      <label htmlFor={`${props.id}-${escape(item)}`}>
         <input
           checked={
             props.fieldType === Array
@@ -30,7 +36,7 @@ const renderCheckboxes = props =>
               : props.value === item
           }
           disabled={props.disabled}
-          id={`${props.id}-${item}`}
+          id={`${props.id}-${escape(item)}`}
           name={props.name}
           onChange={() =>
             props.onChange(
diff --git a/packages/uniforms-material/src/SelectField.tsx b/packages/uniforms-material/src/SelectField.tsx
index e1b771b1..8b161312 100644
--- a/packages/uniforms-material/src/SelectField.tsx
+++ b/packages/uniforms-material/src/SelectField.tsx
@@ -12,6 +12,12 @@ import { connectField, filterDOMProps } from 'uniforms';
 
 import wrapField from './wrapField';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const xor = (item, array) => {
   const index = array.indexOf(item);
   if (index === -1) {
@@ -126,7 +132,7 @@ const renderCheckboxes = ({
       >
         {allowedValues.map(item => (
           <FormControlLabel
-            control={<Radio id={`${id}-${item}`} {...filteredProps} />}
+            control={<Radio id={`${id}-${escape(item)}`} {...filteredProps} />}
             key={item}
             label={transform ? transform(item) : item}
             value={item}
@@ -144,7 +150,7 @@ const renderCheckboxes = ({
             control={
               <SelectionControl
                 checked={value.includes(item)}
-                id={`${id}-${item}`}
+                id={`${id}-${escape(item)}`}
                 name={name}
                 onChange={() => disabled || onChange(xor(item, value))}
                 ref={inputRef}
diff --git a/packages/uniforms-semantic/src/RadioField.tsx b/packages/uniforms-semantic/src/RadioField.tsx
index ac9f8df5..7fcbf1dc 100644
--- a/packages/uniforms-semantic/src/RadioField.tsx
+++ b/packages/uniforms-semantic/src/RadioField.tsx
@@ -2,6 +2,12 @@ import React from 'react';
 import classnames from 'classnames';
 import { connectField, filterDOMProps } from 'uniforms';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const Radio = ({
   allowedValues,
   checkboxes, // eslint-disable-line no-unused-vars
@@ -35,13 +41,13 @@ const Radio = ({
           <input
             checked={item === value}
             disabled={disabled}
-            id={`${id}-${item}`}
+            id={`${id}-${escape(item)}`}
             name={name}
             onChange={() => onChange(item)}
             type="radio"
           />
 
-          <label htmlFor={`${id}-${item}`}>
+          <label htmlFor={`${id}-${escape(item)}`}>
             {transform ? transform(item) : item}
           </label>
         </div>
diff --git a/packages/uniforms-semantic/src/SelectField.tsx b/packages/uniforms-semantic/src/SelectField.tsx
index 7d519e47..f6292027 100644
--- a/packages/uniforms-semantic/src/SelectField.tsx
+++ b/packages/uniforms-semantic/src/SelectField.tsx
@@ -2,6 +2,12 @@ import React from 'react';
 import classnames from 'classnames';
 import { connectField, filterDOMProps } from 'uniforms';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const xor = (item, array) => {
   const index = array.indexOf(item);
   if (index === -1) {
@@ -27,7 +33,7 @@ const renderCheckboxes = ({
         <input
           checked={fieldType === Array ? value.includes(item) : value === item}
           disabled={disabled}
-          id={`${id}-${item}`}
+          id={`${id}-${escape(item)}`}
           name={name}
           onChange={() =>
             onChange(fieldType === Array ? xor(item, value) : item)
@@ -35,7 +41,7 @@ const renderCheckboxes = ({
           type="checkbox"
         />
 
-        <label htmlFor={`${id}-${item}`}>
+        <label htmlFor={`${id}-${escape(item)}`}>
           {transform ? transform(item) : item}
         </label>
       </div>
diff --git a/packages/uniforms-unstyled/src/RadioField.tsx b/packages/uniforms-unstyled/src/RadioField.tsx
index 28817d2b..b7ed9385 100644
--- a/packages/uniforms-unstyled/src/RadioField.tsx
+++ b/packages/uniforms-unstyled/src/RadioField.tsx
@@ -1,6 +1,12 @@
 import React from 'react';
 import { connectField, filterDOMProps } from 'uniforms';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const Radio = ({
   allowedValues,
   checkboxes, // eslint-disable-line no-unused-vars
@@ -21,13 +27,13 @@ const Radio = ({
         <input
           checked={item === value}
           disabled={disabled}
-          id={`${id}-${item}`}
+          id={`${id}-${escape(item)}`}
           name={name}
           onChange={() => onChange(item)}
           type="radio"
         />
 
-        <label htmlFor={`${id}-${item}`}>
+        <label htmlFor={`${id}-${escape(item)}`}>
           {transform ? transform(item) : item}
         </label>
       </div>
diff --git a/packages/uniforms-unstyled/src/SelectField.tsx b/packages/uniforms-unstyled/src/SelectField.tsx
index 9ca426a0..b83465d0 100644
--- a/packages/uniforms-unstyled/src/SelectField.tsx
+++ b/packages/uniforms-unstyled/src/SelectField.tsx
@@ -1,6 +1,12 @@
 import React from 'react';
 import { connectField, filterDOMProps } from 'uniforms';
 
+const base64 =
+  typeof btoa !== 'undefined'
+    ? btoa
+    : (x: string) => Buffer.from(x).toString('base64');
+const escape = (x: string) => base64(x).replace(/=+$/, '');
+
 const xor = (item, array) => {
   const index = array.indexOf(item);
   if (index === -1) {
@@ -25,13 +31,13 @@ const renderCheckboxes = ({
       <input
         checked={fieldType === Array ? value.includes(item) : value === item}
         disabled={disabled}
-        id={`${id}-${item}`}
+        id={`${id}-${escape(item)}`}
         name={name}
         onChange={() => onChange(fieldType === Array ? xor(item, value) : item)}
         type="checkbox"
       />
 
-      <label htmlFor={`${id}-${item}`}>
+      <label htmlFor={`${id}-${escape(item)}`}>
         {transform ? transform(item) : item}
       </label>
     </div>


Test patch:
diff --git a/packages/uniforms-bootstrap3/__tests__/RadioField.tsx b/packages/uniforms-bootstrap3/__tests__/RadioField.tsx
index e55b0e47..a7acb1d4 100644
--- a/packages/uniforms-bootstrap3/__tests__/RadioField.tsx
+++ b/packages/uniforms-bootstrap3/__tests__/RadioField.tsx
@@ -82,13 +82,13 @@ test('<RadioField> - renders a set of checkboxes with correct id (specified)', (
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<RadioField> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-bootstrap3/__tests__/SelectField.tsx b/packages/uniforms-bootstrap3/__tests__/SelectField.tsx
index 551bdb7c..c6f86a52 100644
--- a/packages/uniforms-bootstrap3/__tests__/SelectField.tsx
+++ b/packages/uniforms-bootstrap3/__tests__/SelectField.tsx
@@ -336,13 +336,13 @@ test('<SelectField checkboxes> - renders a set of checkboxes with correct id (sp
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<SelectField checkboxes> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-bootstrap4/__tests__/RadioField.tsx b/packages/uniforms-bootstrap4/__tests__/RadioField.tsx
index 61a98d56..5d0c76ec 100644
--- a/packages/uniforms-bootstrap4/__tests__/RadioField.tsx
+++ b/packages/uniforms-bootstrap4/__tests__/RadioField.tsx
@@ -83,13 +83,13 @@ test('<RadioField> - renders a set of checkboxes with correct id (specified)', (
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<RadioField> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-bootstrap4/__tests__/SelectField.tsx b/packages/uniforms-bootstrap4/__tests__/SelectField.tsx
index c4b258ed..5b6470c1 100644
--- a/packages/uniforms-bootstrap4/__tests__/SelectField.tsx
+++ b/packages/uniforms-bootstrap4/__tests__/SelectField.tsx
@@ -336,13 +336,13 @@ test('<SelectField checkboxes> - renders a set of checkboxes with correct id (sp
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<SelectField checkboxes> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-material/__tests__/SelectField.tsx b/packages/uniforms-material/__tests__/SelectField.tsx
index f2ec6db1..641ed49f 100644
--- a/packages/uniforms-material/__tests__/SelectField.tsx
+++ b/packages/uniforms-material/__tests__/SelectField.tsx
@@ -334,13 +334,13 @@ test('<SelectField checkboxes> - renders a set of Radio buttons with correct id
       .find(Radio)
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find(Radio)
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<SelectField checkboxes> - renders a set of Radio buttons with correct name', () => {
diff --git a/packages/uniforms-semantic/__tests__/RadioField.tsx b/packages/uniforms-semantic/__tests__/RadioField.tsx
index c7e08d78..9d621ebb 100644
--- a/packages/uniforms-semantic/__tests__/RadioField.tsx
+++ b/packages/uniforms-semantic/__tests__/RadioField.tsx
@@ -71,13 +71,13 @@ test('<RadioField> - renders a set of checkboxes with correct id (specified)', (
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<RadioField> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-semantic/__tests__/SelectField.tsx b/packages/uniforms-semantic/__tests__/SelectField.tsx
index 612daed5..0772dc46 100644
--- a/packages/uniforms-semantic/__tests__/SelectField.tsx
+++ b/packages/uniforms-semantic/__tests__/SelectField.tsx
@@ -325,13 +325,13 @@ test('<SelectField checkboxes> - renders a set of checkboxes with correct id (sp
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<SelectField checkboxes> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-unstyled/__tests__/RadioField.tsx b/packages/uniforms-unstyled/__tests__/RadioField.tsx
index d3bc6447..ba864635 100644
--- a/packages/uniforms-unstyled/__tests__/RadioField.tsx
+++ b/packages/uniforms-unstyled/__tests__/RadioField.tsx
@@ -71,13 +71,13 @@ test('<RadioField> - renders a set of checkboxes with correct id (specified)', (
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<RadioField> - renders a set of checkboxes with correct name', () => {
diff --git a/packages/uniforms-unstyled/__tests__/SelectField.tsx b/packages/uniforms-unstyled/__tests__/SelectField.tsx
index e96f6096..524d3577 100644
--- a/packages/uniforms-unstyled/__tests__/SelectField.tsx
+++ b/packages/uniforms-unstyled/__tests__/SelectField.tsx
@@ -325,13 +325,13 @@ test('<SelectField checkboxes> - renders a set of checkboxes with correct id (sp
       .find('input')
       .at(0)
       .prop('id'),
-  ).toBe('y-a');
+  ).toBe('y-YQ');
   expect(
     wrapper
       .find('input')
       .at(1)
       .prop('id'),
-  ).toBe('y-b');
+  ).toBe('y-Yg');
 });
 
 test('<SelectField checkboxes> - renders a set of checkboxes with correct name', () => {