Repository: speee/jsx-slack
Pull request #21 (2019-06-28 06:57:20)

Make interchangeable with Image component and intrinsic img tag
So far, usages `<Image>` component and `<img>` intrinsic HTML tag had not a consistency.

- In `<Blocks>`, `<Image>` and `<img>` have a same meaning (Image block).
- In `<Section>` block, only `<Image>` component can use as an accessory. It would raise an error `Unknown HTML-like element: img` if you try to use `<img>` tag.
- In `<Context>` block, only `<img>` component can use as an image element. It would be generated a strange text `[Object object]` if you try to use `<Image>` component.

By this PR, `<Image>` component and `<img>` element would make interchangeable in any components.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 03f5331..c541296 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## [Unreleased]
 
+### Changed
+
+- Make interchangeable with `<Image>` component and intrinsic `<img>` tag ([#21](https://github.com/speee/jsx-slack/pull/21))
+
 ## v0.4.3 - 2019-05-15
 
 ### Fixed
diff --git a/README.md b/README.md
index cdc0e8b..3053df3 100644
--- a/README.md
+++ b/README.md
@@ -174,7 +174,7 @@ The content of `<Section>` may include one of an accessory component. A defined
 
 ###### Accessory components
 
-- [`<Image>`](#image-image-block)
+- [`<Image>` / `<img>`](#image-image-block)
 - [`<Button>`](#button-button-element)
 - [`<Select>`](#select-select-menu-with-static-options)
 - [`<ExternalSelect>`](#externalselect-select-menu-with-external-data-source)
@@ -234,9 +234,7 @@ Just a divider. `<hr>` intrinsic HTML element works as well.
 
 #### [`<Image>`: Image Block](https://api.slack.com/reference/messaging/blocks#image)
 
-Display an image block. It has well-known props like `<img>` HTML element.
-
-In `<Blocks>`, `<img>` intrinsic HTML element works as well.
+Display an image block. It has well-known props like `<img>` HTML element. In fact, `<img>` intrinsic HTML element works as well.
 
 ```jsx
 <Blocks>
@@ -263,7 +261,7 @@ A block to hold [interactive elements](#interactive-elements). Slack allows a ma
 
 #### [`<Context>`: Context Block](https://api.slack.com/reference/messaging/blocks#context)
 
-Display message context. It allows mixed contents consisted of the text and the `<img>` tag image.
+Display message context. It allows mixed contents consisted of the text and the `<Image>` component / `<img>` tag.
 
 ```jsx
 <Blocks>
@@ -539,6 +537,8 @@ Define confirmation dialog. Some interactive elements can open confirmation dial
 
 [confirmation]: https://api.slack.com/tools/block-kit-builder?blocks=%5B%7B%22type%22%3A%22actions%22%2C%22elements%22%3A%5B%7B%22type%22%3A%22button%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Commit%22%2C%22emoji%22%3Atrue%7D%2C%22action_id%22%3A%22commit%22%2C%22confirm%22%3A%7B%22title%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Commit%20your%20action%22%2C%22emoji%22%3Atrue%7D%2C%22text%22%3A%7B%22type%22%3A%22mrkdwn%22%2C%22text%22%3A%22*Are%20you%20sure%3F*%20Please%20confirm%20your%20action%20again.%22%2C%22verbatim%22%3Atrue%7D%2C%22confirm%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Yes%2C%20please%22%2C%22emoji%22%3Atrue%7D%2C%22deny%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Cancel%22%2C%22emoji%22%3Atrue%7D%7D%2C%22value%22%3A%22value%22%7D%5D%7D%5D
 
+> :information_source: You can use [HTML-like formatting](#html-like-formatting) to the content of confirmation dialog. However, you have to be careful that Slack ignores any line breaks and the content will render just in a line.
+
 ##### Props
 
 - `title` (**required**): The title of confirmation dialog.
diff --git a/demo/schema.js b/demo/schema.js
index a32d97b..72d7eba 100644
--- a/demo/schema.js
+++ b/demo/schema.js
@@ -52,11 +52,23 @@ export default {
   // Block Kit component
   Section: {
     attrs: blockCommonAttrs,
-    children: ['Field', 'Image', ...interactiveComponents, ...markupHTML],
+    children: [
+      'Field',
+      'Image',
+      'img',
+      ...interactiveComponents,
+      ...markupHTML,
+    ],
   },
   section: {
     attrs: { id: null },
-    children: ['Field', 'Image', ...interactiveComponents, ...markupHTML],
+    children: [
+      'Field',
+      'Image',
+      'img',
+      ...interactiveComponents,
+      ...markupHTML,
+    ],
   },
   Field: { attrs: {}, children: markupHTML },
   Divider: { attrs: blockCommonAttrs, children: [] },
@@ -70,7 +82,10 @@ export default {
     children: [],
   },
   Actions: { attrs: blockCommonAttrs, children: interactiveComponents },
-  Context: { attrs: blockCommonAttrs, children: ['img', ...markupHTML] },
+  Context: {
+    attrs: blockCommonAttrs,
+    children: ['Image', 'img', ...markupHTML],
+  },
 
   // Interactive components
   Button: {
diff --git a/src/block-kit/Context.tsx b/src/block-kit/Context.tsx
index 8e6a3ba..965f5f7 100644
--- a/src/block-kit/Context.tsx
+++ b/src/block-kit/Context.tsx
@@ -1,7 +1,7 @@
 /** @jsx JSXSlack.h */
 import { ContextBlock, ImageElement, MrkdwnElement } from '@slack/types'
-import { JSXSlack } from '../jsx'
 import html from '../html'
+import { JSXSlack } from '../jsx'
 import { ObjectOutput } from '../utils'
 import { BlockComponentProps } from './Blocks'
 
@@ -14,10 +14,21 @@ export const Context: JSXSlack.FC<
   let current: (string | JSXSlack.Node)[] = []
 
   for (const child of [...JSXSlack.normalizeChildren(children), endSymbol]) {
-    const img =
-      child && typeof child === 'object' && child.type === 'img'
-        ? child
-        : undefined
+    const img = (() => {
+      if (typeof child !== 'object') return undefined
+
+      const { props } = child
+
+      // <img> intrinsic HTML element
+      if (child.type === 'img')
+        return { image_url: props.src, alt_text: props.alt }
+
+      // A converted <Image> component
+      if (child.type === JSXSlack.NodeType.object && props.type === 'image')
+        return { image_url: props.image_url, alt_text: props.alt_text }
+
+      return undefined
+    })()
 
     if (current.length > 0 && (img || child === endSymbol)) {
       // Text content
@@ -27,11 +38,7 @@ export const Context: JSXSlack.FC<
 
     if (img) {
       // Image content
-      elements.push({
-        type: 'image',
-        image_url: img.props.src,
-        alt_text: img.props.alt,
-      })
+      elements.push({ type: 'image', ...img })
     } else if (typeof child !== 'symbol') {
       current.push(child)
     }
diff --git a/src/block-kit/Section.tsx b/src/block-kit/Section.tsx
index 3bb6c43..ec880c9 100644
--- a/src/block-kit/Section.tsx
+++ b/src/block-kit/Section.tsx
@@ -4,6 +4,7 @@ import { JSXSlack } from '../jsx'
 import { ObjectOutput } from '../utils'
 import html from '../html'
 import { BlockComponentProps } from './Blocks'
+import { Image } from './Image'
 
 export const Section: JSXSlack.FC<
   BlockComponentProps & { children: JSXSlack.Children<{}> }
@@ -14,30 +15,42 @@ export const Section: JSXSlack.FC<
   let fields: SectionBlock['fields']
 
   for (const child of JSXSlack.normalizeChildren(children)) {
-    if (typeof child === 'object' && child.type === JSXSlack.NodeType.object) {
+    let eaten = false
+
+    if (typeof child === 'object') {
       // Accessory and fields
-      switch (child.props.type) {
-        case 'image':
-        case 'button':
-        case 'static_select':
-        case 'external_select':
-        case 'users_select':
-        case 'conversations_select':
-        case 'channels_select':
-        case 'overflow':
-        case 'datepicker':
-          accessory = JSXSlack(child)
-          break
-        case 'mrkdwn':
-          if (!fields) fields = []
-          fields.push(child.props)
-          break
-        default:
-          throw new Error('<Section> has unexpected component as accessory.')
+      if (child.type === JSXSlack.NodeType.object) {
+        switch (child.props.type) {
+          case 'image':
+          case 'button':
+          case 'static_select':
+          case 'external_select':
+          case 'users_select':
+          case 'conversations_select':
+          case 'channels_select':
+          case 'overflow':
+          case 'datepicker':
+            accessory = JSXSlack(child)
+            break
+          case 'mrkdwn':
+            if (!fields) fields = []
+            fields.push(child.props)
+            break
+          default:
+            throw new Error(
+              '<Section> has unexpected component as an accessory.'
+            )
+        }
+        eaten = true
+      } else if (child.type === 'img') {
+        accessory = JSXSlack(
+          <Image alt={child.props.alt} src={child.props.src} />
+        )
+        eaten = true
       }
-    } else {
-      normalized.push(child)
     }
+
+    if (!eaten) normalized.push(child)
   }
 
   const text = html(normalized)


Test patch:
diff --git a/test/index.tsx b/test/index.tsx
index 221b7b1..ca9f387 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -84,20 +84,23 @@ describe('jsx-slack', () => {
         },
       }
 
-      it('outputs section block with <Image> accessory', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Section blockId="with_image">
-                Image example
-                <Image
-                  src="https://example.com/image.jpg"
-                  alt="Example image"
-                />
-              </Section>
-            </Blocks>
-          )
-        ).toStrictEqual([section]))
+      it('outputs section block with image accessories', () => {
+        for (const accessory of [
+          <Image src="https://example.com/image.jpg" alt="Example image" />,
+          <img src="https://example.com/image.jpg" alt="Example image" />,
+        ]) {
+          expect(
+            JSXSlack(
+              <Blocks>
+                <Section blockId="with_image">
+                  Image example
+                  {accessory}
+                </Section>
+              </Blocks>
+            )
+          ).toStrictEqual([section])
+        }
+      })
 
       it('output section block with action accessories', () => {
         for (const accessory of [
@@ -741,6 +744,10 @@ describe('jsx-slack', () => {
                 Hello! <b>World!</b>
                 <img src="https://example.com/test.jpg" alt="image" />
                 Image + Text
+                <Image
+                  src="https://example.com/test2.jpg"
+                  alt="image component"
+                />
               </Context>
             </Blocks>
           )
@@ -764,6 +771,11 @@ describe('jsx-slack', () => {
                 text: 'Image + Text',
                 verbatim: true,
               },
+              {
+                type: 'image',
+                image_url: 'https://example.com/test2.jpg',
+                alt_text: 'image component',
+              },
             ],
           },
         ]))