JavaScript
JSX
VirtualDom
React

文字列をフォーマットする関数のパラメータに装飾された仮想DOMを使いたい

何がやりたいか

{
  "format": "こんにちは、{user.name}さん",
  "params": {
    "user": {
      "name": "mpyw"
    }
  }
}

みたいなAPIレスポンスが返ってきたとして,これをフロント側でユーザ名の部分を自由に装飾して表示させたい。

  • 頻繁に新しいメッセージテンプレートが追加されるのに,いちいちそのたびにフロントエンドアプリの更新をさせたくない。(react-nativeとか使ってるとつらそう)
  • HTMLタグを文字列としてそのまま埋め込むのってなんか違う気がするし,dangerouslySetInnerHTMLも気軽には使いたくない。

文字列展開のみなら既に Matt-Esch/string-template などがあるんですが,なかなかJSXによる装飾に対応したのが無かったんですよね。例えばこうしたい。

こんにちは、mpywさん

実装してみる

String.prototype.replaceを使いたくなりますが,仮想DOMに対応できないので,String.prototype.splitで処理するのがミソです。

template.js
import React from 'react'

const renderValue = (Node) => {
  if (typeof Node === 'function') {
    return <Node />
  }
  if (Node === undefined) {
    return null
  }
  return Node
}

const resolveParam = (params, key) => {
  if (typeof key !== 'string') {
    return params[key]
  }
  let param = params
  for (const segment of key.split('.')) {
    if (!param) return
    param = param[segment]
  }
  return param
}

const template = (format, params) => {
  const chunks = format.split(/\{(\{[\S\s]*?})}|\{([\S\s]*?)}/g)
  const nodes = []

  for (const [offset, value] of Object.entries(chunks)) {
    switch (offset % 3) {
      case 0:
        nodes.push(value)
        break
      case 1:
        if (value === undefined) continue
        nodes.push(value)
        break
      case 2:
        if (value === undefined) continue
        nodes.push(renderValue(resolveParam(params, value)))
        break
    }
  }

  return nodes
}

export default template
使用例1
const { format, params } = {
  format: 'こんにちは、{user.name}さん',
  params: {
    user: {
      name: 'mpyw'
    }
  }
}

const decoratedParams = {
  ...params,
  user: {
    ...params.user,
    name: <span style={{ color: 'blue' }}>{params.user.name}</span>,
  },
}

return <div>{template(format, decoratedParams)}</div>

こんにちは、mpywさん

使用例2
(<div>
  {template(
    'Hello, {p1}! Yeah, {p2}! Yo, {p3.foo}! {{This is escaped.}}',
    {
      p1: () => <span style={{ color: 'red' }}>World</span>,
      p2: <span style={{ color: 'blue' }}>World</span>,
      p3: {
        foo: () => <span style={{ color: 'green' }}>World</span>,
      },
    }
  )}
</div>)

Hello, World! Yeah, World! Yo, World! {This is escaped.}

仮想DOM生成関数と仮想DOM両対応,ドットチェインのネストにも対応,って感じで一通り必要そうなやつは実装しました。だいたいこれで事足りるはず。

ライブラリにするほどでもないよね?