デザインパターン:ユーザーのUIを生成する


私は私の反応のアプリに問題があった:私は複数のタイプの複数の入力を使用してフォームをレンダリングする必要がありました.
しかし、ここではキッカーです:SuveLeyMonkeyやTypeFormのようなフォームビルダーと同様に、ユーザーはこれらのフォーム自体をデザインし、必要なフィールドを含めるように設定する必要があります.
どうやって行くの?ユーザーは自分で反応しないので、フォームの設定を記述するデータモデルが必要です.データ構造とアルゴリズムは典型的に私の強いスーツではありませんが、私が着地したものは、私が実現するようになったものです.

ビジターパターンは何ですか?


The Wikipedia page for the visitor pattern ビジターパターンを「動作するオブジェクト構造からアルゴリズムを分離する方法」として記述します.もう一つの方法は、オブジェクト自体を変更することなくオブジェクトまたはコードがどのように動作するかを変更することです.
これらの種類のコンピュータサイエンスのトピックは、概念の実際のユースケースを見ずに私の頭の上に行く.それでは、簡単に現実世界のユースケースを使用してビジターパターンを探索しましょう.
Babel アクションのビジターパターンの実用的な例です.Babelは抽象構文木(AST)で動作し、ソースコード内のさまざまなノード(EG、テキストブロック)を訪問してコードを変換します.
Babelがあなたのコードを変えるためにビジターパターンを使用する方法の最小限のHello World例は、ここにあります:
// source.js
const hello = "world"
const goodbye = "mars"

// babel-transform.js
export default function () {
  return {
    visitor: {
      Identifier(path) {
        path.node.name = path.node.name.split('').reverse().join('')
      }
    }
  }
}

// output.js
const olleh = "world"
const eybdoog = "mars"
この例で自分で遊ぶことができますhere .
訪問者パターンを実装することによって、バベルは各々を訪問しますIdentifier トークンsource.js . 上の例ではIdentifier トークンは変数名ですhello and goodbye .
バベルが認めるときIdentifier , それは我々の変換コードに何かを手にして、我々がどのようにトークンを変えたいかを決めさせます.ここで、変数文字列を反転し、結果を変数の新しい名前として割り当てます.しかし、我々は我々が欲しいコードを修正することができました.
バベルがソースコードを解析するためにすべての重い持ち上げをするので、これは強力です.そして、どんなタイプのトークンがどこにあるかについてわかります.Identifier ) そして、我々がそれについてやりたいことを尋ねてください.我々はどのようにバベルが動作し、バベルは我々が訪問者の機能で何を気にしないかを知る必要はありません.

反応の訪問者パターン


現在、我々はビジターパターンが汎用アルゴリズムとしてどのように見えるかを知っています.
さて、この反応アプリは私が構築している、私は私はユーザーの設定されたカスタムフォームを記述するデータモデルが必要になると述べた.これをフォームのschema .
このスキーマの各フィールドには以下のような属性があります.

  • フィールドタイプ.ドロップダウン、日付、番号など

  • レーベル.フィールドが表すデータ.EG、ファーストネーム、誕生日など.

  • 必要です.フィールドがフォームに必須であるかどうか.
  • スキーマには他のカスタマイズオプションも含めることができますが、これらから始めましょう.
    また、各フィールドが示す順序を実施する必要があります.これを行うには、各フィールドを配列に配置できます.
    ここでは、3つのフィールドを持つフォームに対して使用できるスキーマの例を示します.
    const schema = [
      {
        label: "Name",
        required: true,
        fieldType: "Text",
      },
      {
        label: "Birthdate",
        required: true,
        fieldType: "Date",
      },
      {
        label: "Number of Pets",
        required: false,
        fieldType: "Number",
      },
    ]
    

    単純だが限定的なアプローチ


    どうやってこれを反応させるのだろう?ストレートフォワードの解決策は次のようになります.
    function Form({ schema }) {
      return schema.map((field) => {
        switch (field.fieldType) {
          case "Text":
            return <input type="text" /> 
          case "Date":
            return <input type="date" />
          case "Number":
            return <input type="number" />
          default:
            return null
        }
      })
    }
    
    これは既にBabelで見たようなビジターパターンのようです.そして、これはおそらく多くの基本的な形のために静かにスケールすることができました!
    しかし、このアプローチはビジターパターンの重要な側面を欠いています.実装を変更せずにカスタマイズを許可しません.
    たとえば、プロファイルビューのような他のユースケースのためにこのスキーマを再利用することができますForm 両方のユースケースをキャプチャするコンポーネントです.

    カスタマイズ可能なビジターパターンアプローチ


    私たちの完全なカスタマイズを可能にするビジターパターンの使用法を正式化しましょうschema レンダリングを必要とせずにレンダリングForm 実装
    const defaultComponents = {
      Text: () => <input type="text" />,
      Date: () => <input type="date" />,
      Number: () => <input type="number" />
    }
    
    function ViewGenerator({ schema, components }) {
      const mergedComponents = {
        ...defaultComponents,
        ...components,
      }
    
      return schema.map((field) => {
        return mergedComponents[field.fieldType](field);
      });
    }
    
    この新しいViewGenerator コンポーネントは同じものを達成するForm 前にやっていたschema と貸し手input に基づいた要素fieldType . しかし、スイッチ文の中から各コンポーネントの型を抽出し、components マップ.
    この変更により、デフォルトの動作はViewGenerator (物を)使うdefaultComponents ). でも、どう変えたらいいかschema レンダリングされます我々は変更する必要はありませんViewGenerator まったく!
    代わりに、我々は新しいcomponents 新しい行動を定義するマップ.以下のようになります.
    const data = {
      name: "John",
      birthdate: "1992-02-01",
      numPets: 2
    }
    
    const profileViewComponents = {
      Text: ({ label, name }) => (
        <div>
          <p>{label}</p>
          <p>{data[name]}</p>
        </div>
      ),
      Date: ({ label, name }) => (
        <div>
          <p>{label}</p>
          <p>{data[name]}</p>
        </div>
      ),
      Number: ({ label, name }) => (
        <div>
          <p>{label}</p>
          <p>{data[name]}</p>
        </div>
      )
    }
    
    function ProfileView({ schema }) {
      return (
        <ViewGenerator
          schema={schema}
          components={profileViewComponents}
        />
      )
    }
    
    ViewGenerator スキーマ上のマップを盲目的にそれぞれの関数を呼び出しますprofileViewComponents それが彼らの向こうに来るようにschema . ViewGenerator 我々がその機能で何をするか気にしないでください、そして、我々の機能はどのように気にする必要はありませんViewGenerator スキーマの構文解析.The components Propは、私たちがスキーマがどのように構文解析されるかについて考える必要がなくて、スキーマが解釈される方法をカスタマイズさせるためにビジターパターンを活用する強力な概念です.

    フレームワークの拡張


    我々のアプリは、これらのユーザーが設定されたフォームの新しい要件を持っています:ユーザーはセクションにグループの入力フィールドをすることができますし、それらを非表示にコンテンツを崩壊する.
    基本的なユーザー設定フォームを実装するためのフレームワークを持っているので、このフレームワークをどのように拡張して、これらの新しい機能を有効にするにはどうすればよいのですか?
    始めるには、Aを加えることができますSection コンポーネントcomponents 地図
    const components = {
      Section: ({ label }) => (
        <details>
          <summary>{label}</summary>
          {/* grouped fields go here? */}
        </details>
      )
    }
    
    しかし、私たちは、どの分野が我々に関連しているかを識別する良い方法を持っていませんSection . つの解決策はsectionId それぞれのフィールドにマップし、それらの上にマップを収集するSection . しかし、それはViewGenerator 'sの仕事!
    もう一つのオプションはViewGenerator フレームワークは、子要素の概念を含めるこのchildren 反応の支柱以下にそのスキーマがどのように見えるかを示します.
    const schema = [
      {
        label: "Personal Details",
        fieldType: "Section",
        children: [
          {
            label: "Name",
            fieldType: "Text",
          },
          {
            label: "Birthdate",
            fieldType: "Date",
          },
        ],
      },
      {
        label: "Favorites",  
        fieldType: "Section",
        children: [
          {
            label: "Favorite Movie",
            fieldType: "Text",
          },
        ],
      },
    ]
    
    我々のスキーマは、反応木のように見え始めています!JSXをこのスキーマのフォーム版に書き込もうとするなら、次のようになります.
    function Form() {
      return (
        <>
          <details>
            <summary>Personal Details</summary>
            <label>
              Name
              <input type="text" />
            </label>
            <label>
              Birthdate
              <input type="date" />
            </label>
          </details>
          <details>
            <summary>Favorites</summary>
            <label>
              Favorite Movies
              <input type="text" />
            </label>
          </details>
        </>
      )
    }
    
    さあ更新しましょうViewGenerator 新しいフレームワークをサポートするフレームワークchildren 上記のJSXを生成するための概念
    function ViewGenerator({ schema, components }) {
      const mergedComponents = {
        ...defaultComponents,
        ...components,
      }
    
      return schema.map((field) => {
        const children = field.children ? (
          <ViewGenerator
            schema={field.children}
            components={mergedComponents}
          />
        ) : null
    
        return mergedComponents[field.fieldType]({ ...field, children });
      })
    }
    
    通知方法children ちょうど別のインスタンスViewGenerator スキーマプロップを親スキーマとして設定するchildren プロパティ.我々がネストしたいならばchildren 我々は通常のJSXと同じように深いと小道具.再帰!カメですViewGenerator ずっと.children は現在、我々のノードに渡される反応ノードですcomponents 関数マップとその使い方
    const components = {
      Section: ({ label, children }) => (
        <details>
          <summary>{label}</summary>
          {children}
        </details>
      )
    }
    
    Section を返します.children そして、それは気にする必要はありませんchildren がレンダリングされるViewGenerator コンポーネントはその処理です.
    CodesAndBoxの最終的なソリューションで再生できます.

    結論


    何も新しいソフトウェアです.新しいアイデアは帽子をかぶった古い考えだ.上の例で見るように、訪問者のパターンを反応させるのに多くのコードをとらない.しかし、概念として、それは設定駆動UISをレンダリングするための強力なパターンを解除します.
    この記事は設定可能な“フォームジェネレータ”コンポーネントを構築していますが、このパターンは設定(別名、スキーマ)駆動UIを必要とする多くの状況に適用できます.
    私はあなた自身のために何を使用するケースを参照してくださいするのが大好きだViewGenerator フレームワーク!私はあなたのビルドを参照してください.

    追加リソース


  • react-jsonschema-form に基づいてフォームを生成する反応ライブラリですjson-schema そして、ここで紹介されたものと非常によく似た概念を使用する
  • Babelプラグインについてもっと知りたい場合はBabel plugin handbook ジェイミーカイルでは、訪問者のパターンの実用的なアプリケーションを歩いてのための素晴らしいリソースです.
  • バニラJavaScriptを使用したビジターパターンの簡単な例を示します.