Repository: speee/jsx-slack
Pull request #106 (2020-01-20 02:41:45)

Custom transformer for modal's private metadata
`<Modal>` component will allow to specify `Function` as  `privateMetadata` prop. It can customize the transformer for stringification of hidden values set up via `<Input type="hidden">`.

```jsx
<Modal
  title="test"
  privateMetadata={hidden =>
    hidden && new URLSearchParams(hidden).toString()
  }
>
  <Input type="hidden" name="A" value="foobar" />
  <Input type="hidden" name="B" value={123} />
  <Input type="hidden" name="C" value={true} />
</Modal>
```

In this example, The value of `privateMetadata` field in returned payload would be `A=foobar&B=123&C=true` by transformation using `URLSearchParams`.

In addition, user can modify and merge custom values in transformer. jsx-slack can store metadata specified in the modal along with values in `<Input type="hidden">`.

```jsx
<Modal
  title="test"
  privateMetadata={hidden =>
    hidden && JSON.stringify({ ...hidden, foo: 'bar' })
  }
>
  <Input type="hidden" name="abc" value={123} />
</Modal>
```

The custom transformer takes an argument: JSON object of hidden values or `undefined` when there was no hidden values. It must return the transformed string or `undefined` (Won't assign private metadata).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1655b4..c09a05c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Added
+
+- [Custom transformer](https://github.com/speee/jsx-slack/blob/master/docs/block-elements.md#custom-transformer) for modal's private metadata ([#106](https://github.com/speee/jsx-slack/pull/106))
+
 ### Changed
 
 - Mark `<Home>` container as stable ([#105](https://github.com/speee/jsx-slack/pull/105))
diff --git a/docs/block-containers.md b/docs/block-containers.md
index 4391f3b..1baca94 100644
--- a/docs/block-containers.md
+++ b/docs/block-containers.md
@@ -73,7 +73,7 @@ export default function shareModal(opts) {
 - `title` (**required**): An user-facing title of the modal. (24 characters maximum)
 - `close` (optional): A text for close button of the modal. (24 characters maximum)
 - `submit` (optional): A text for submit button of the modal. The value specified in this prop is preferred than [`<Input type="submit">`](block-elements.md#input-submit) (24 characters maximum)
-- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received. The value specified in this prop is preferred than [`<Input type="hidden">`](block-elements.md#input-hidden). (3000 characters maximum)
+- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received (3000 characters maximum). [By setting function, you can use the custom transformer to serialize hidden values set up via `<Input type="hidden">`](block-elements.md#custom-transformer).
 - `clearOnClose` (optional): If enabled by setting `true`, all stacked views will be cleared by close button.
 - `notifyOnClose` (optional): If enabled by setting `true`, `view_closed` event will be sent to request URL of Slack app when closed modal.
 - `callbackId` (optional): An identifier for this modal to recognize it in various events. (255 characters maximum)
diff --git a/docs/block-elements.md b/docs/block-elements.md
index 911d8e4..7827c7e 100644
--- a/docs/block-elements.md
+++ b/docs/block-elements.md
@@ -634,7 +634,7 @@ You can use hidden values by parsing JSON stored in [callbacked `private_metadat
 
 #### Note
 
-`privateMetadata` prop in parent `<Modal>` _must not define_ when using `<Input type="hidden">`. `<Modal>` prefers `privateMetadata` than `<Input type="hidden">` whenever it has any value in `privateMetadata` prop.
+`<Modal>` prefers the string defined in `privateMetadata` prop directly than `<Input type="hidden">`.
 
 And please take care that the maximum length validation by Slack will still apply for stringified JSON. The value like string and array that cannot predict the length might over the limit of JSON string length easily (3000 characters).
 
@@ -646,6 +646,25 @@ The best practice is only storing the value of a pointer to reference data store
 - `name` (**required**): The name of hidden value.
 - `value` (**required**): A value to store into modal. It must be [a serializable value to JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description).
 
+#### Custom transformer
+
+If you want to store hidden values by own way, you can use a custom transformer by passing function to `privateMetadata` prop in the parent `<Modal>`.
+
+```jsx
+<Modal
+  title="test"
+  privateMetadata={hidden => hidden && new URLSearchParams(hidden).toString()}
+>
+  <Input type="hidden" name="A" value="foobar" />
+  <Input type="hidden" name="B" value={123} />
+  <Input type="hidden" name="C" value={true} />
+</Modal>
+```
+
+In this example, the value of `private_metadata` field in returned payload would be **`A=foobar&B=123&C=true`** by transformation using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) instead of `JSON.stringify`.
+
+The transformer takes an argument: JSON object of hidden values or `undefined` when there was no hidden values. It must return the transformed string, or `undefined` if won't assign private metadata.
+
 ### <a name="input-submit" id="input-submit"></a> `<Input type="submit">`: Set submit button text of modal
 
 `<Input type="submit">` can set the label of submit button for the current modal. It is meaning just an alias into `submit` prop of `<Modal>`, but JSX looks like more natural HTML form.
@@ -657,7 +676,7 @@ The best practice is only storing the value of a pointer to reference data store
 </Modal>
 ```
 
-As same as `<Input type="hidden">`, `submit` prop in `<Modal>` must not define when using `<Input type="submit">`. `<Modal>` prefers props defined directly.
+`submit` prop in `<Modal>` must not define when using `<Input type="submit">`. `<Modal>` prefers the prop defined directly.
 
 #### Props
 
diff --git a/src/block-kit/Modal.tsx b/src/block-kit/Modal.tsx
index 723409b..f5086d4 100644
--- a/src/block-kit/Modal.tsx
+++ b/src/block-kit/Modal.tsx
@@ -11,6 +11,10 @@ import {
 import { internalHiddenType, internalSubmitType } from './Input'
 import { plainText } from './composition/utils'
 
+type PrivateMetadataTransformer = (
+  hiddenValues: object | undefined
+) => string | undefined
+
 export interface ModalProps {
   callbackId?: string
   children: JSXSlack.Children<BlockComponentProps>
@@ -18,7 +22,7 @@ export interface ModalProps {
   close?: string
   externalId?: string
   notifyOnClose?: boolean
-  privateMetadata?: string
+  privateMetadata?: string | PrivateMetadataTransformer
   submit?: string
   title: string
 }
@@ -27,7 +31,7 @@ const submitText = plainText('Submit')
 
 export const Modal: JSXSlack.FC<ModalProps> = props => {
   let hasInputBlock = false
-  let hidden: any
+  let hidden: object | undefined
   let inputSubmit: string | undefined
 
   const blocks = JSXSlack(
@@ -61,15 +65,6 @@ export const Modal: JSXSlack.FC<ModalProps> = props => {
     return undefined
   })()
 
-  const privateMetadata = (() => {
-    if (props.privateMetadata !== undefined)
-      return props.privateMetadata.toString()
-
-    if (hidden) return JSON.stringify(hidden)
-
-    return undefined
-  })()
-
   return (
     <ObjectOutput<View & { external_id?: string }>
       type="modal"
@@ -78,7 +73,11 @@ export const Modal: JSXSlack.FC<ModalProps> = props => {
       external_id={props.externalId}
       submit={submit}
       close={props.close ? plainText(props.close) : undefined}
-      private_metadata={privateMetadata}
+      private_metadata={
+        typeof props.privateMetadata === 'string'
+          ? props.privateMetadata.toString()
+          : (props.privateMetadata || (h => h && JSON.stringify(h)))(hidden)
+      }
       clear_on_close={
         props.clearOnClose !== undefined ? !!props.clearOnClose : undefined
       }
diff --git a/src/date.ts b/src/date.ts
index 4da39b6..dba8dc3 100644
--- a/src/date.ts
+++ b/src/date.ts
@@ -52,7 +52,7 @@ export default function formatDate(date: Date, format: string) {
     return `${h}:${m}:${s} ${ampm}`
   }
 
-  const prettifiedDate = (camelize = false) => {
+  const prettifiedDate = (camelize: boolean) => {
     const now = new Date()
     const ny = now.getUTCFullYear()
     const nm = now.getUTCMonth()


Test patch:
diff --git a/test/block-kit/block-elements/input-components-for-modal.tsx b/test/block-kit/block-elements/input-components-for-modal.tsx
index f13a861..71308da 100644
--- a/test/block-kit/block-elements/input-components-for-modal.tsx
+++ b/test/block-kit/block-elements/input-components-for-modal.tsx
@@ -139,6 +139,36 @@ describe('Input components for modal', () => {
 
       expect(modal.private_metadata).toBe('customMeta')
     })
+
+    it('can customize private metadata transformer for assigned hidden values', () => {
+      const transformer = jest.fn(
+        hidden => hidden && new URLSearchParams(hidden).toString()
+      )
+
+      expect(
+        JSXSlack(
+          <Modal title="test" privateMetadata={transformer}>
+            <Input type="text" name="a" label="a" />
+            <Input type="hidden" name="A" value="foobar" />
+            <Input type="hidden" name="B" value={123} />
+            <Input type="hidden" name="C" value={true} />
+          </Modal>
+        ).private_metadata
+      ).toBe('A=foobar&B=123&C=true')
+
+      expect(transformer).toBeCalledWith({ A: 'foobar', B: 123, C: true })
+
+      // Transformer will call with undefined when there are no hidden values
+      expect(
+        JSXSlack(
+          <Modal title="test" privateMetadata={transformer}>
+            <Input type="text" name="a" label="a" />
+          </Modal>
+        ).private_metadata
+      ).toBeUndefined()
+
+      expect(transformer).toBeCalledWith(undefined)
+    })
   })
 
   describe('<Input type="submit">', () => {
diff --git a/test/block-kit/block-elements/interactive-components.tsx b/test/block-kit/block-elements/interactive-components.tsx
index 3132571..42a58c2 100644
--- a/test/block-kit/block-elements/interactive-components.tsx
+++ b/test/block-kit/block-elements/interactive-components.tsx
@@ -781,7 +781,7 @@ describe('Interactive components', () => {
       ).toThrow(/must include/i)
     })
 
-    it('throws error when using <RadioButtonGroup> within <Blocks> container', () =>
+    it('throws error when using <RadioButtonGroup> within <Blocks> container', () => {
       expect(() =>
         JSXSlack(
           <Blocks>
@@ -793,6 +793,19 @@ describe('Interactive components', () => {
             </Section>
           </Blocks>
         )
-      ).toThrow(/incompatible/i))
+      ).toThrow(/incompatible/i)
+
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <RadioButtonGroup>
+                <RadioButton value="a">A</RadioButton>
+              </RadioButtonGroup>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow(/incompatible/i)
+    })
   })
 })