Close
Angular React Web Components Blazor
Open Source

React Query Builder の概要

Ignite UI for React Query Builder は、指定したデータ セットに対する複雑なデータ フィルタリング クエリを構築できる豊富な UI を提供します。このコンポーネントでは、式ツリーを作成し、各フィールドのデータ型に応じたエディターと条件リストを使用して、式の間に AND / OR 条件を指定できます。作成した式ツリーは、その後バックエンドでサポートされる形式のクエリへ簡単に変換できます。

React Query Builder の使用を開始するには

IgrQueryBuilder の使用を開始するには、まず次のコマンドを実行して Ignite UI for React パッケージをインストールします:

npm install igniteui-react igniteui-react-grids

プロジェクト構成に応じて、対応するスタイルも参照する必要があります。

import 'igniteui-webcomponents-grids/grids/themes/light/bootstrap.css';

React Query Builder の使用

初期の式ツリーが設定されていない場合は、まずエンティティと、クエリが返す対象フィールドを選択します。その後、条件またはサブグループを追加できます。

条件を追加するには、フィールド、フィールドのデータ型に基づく演算子、そして演算子が単項でない場合は値を選択します。In および Not In 演算子を使用すると、単に値を指定する代わりに、別のエンティティに対する条件を持つ内部クエリを作成できます。条件を確定すると、その条件情報を含むチップが表示されます。チップをクリックまたはホバーすると、その条件を編集したり、直後に別の条件やグループを追加したりできます。

各グループの上にある AND または OR ボタンをクリックすると、グループの種類を変更したり、内部の条件をグループ解除したりするためのメニューが開きます。

各条件は特定のエンティティの特定フィールドに関連付けられているため、エンティティを変更すると、事前に設定された条件およびグループはすべてリセットされます。

このコンポーネントは、Entities プロパティに、エンティティ名とそのフィールド配列を記述した配列を設定することで使用できます。各フィールドは、名前とデータ型によって定義されます。フィールドを選択すると、データ型に応じた対応する演算子が自動的に割り当てられます。 また Query Builder には ExpressionTree プロパティがあります。これを使用すると、コントロールの初期状態を設定したり、ユーザーが指定したフィルタリング ロジックにアクセスしたりできます。

private queryBuilderRef: React.RefObject<IgcQueryBuilderComponent>;

constructor(props: any) {
  super(props);
  this.queryBuilderRef = React.createRef();
  this.state = {
    expressionTree: null
  };
}

componentDidMount() {
  const tree = new IgrFilteringExpressionsTree();
  tree.operator = FilteringLogic.And;
  tree.entity = 'Orders';

  this.setState({ expressionTree: tree });

  if (this.queryBuilderRef.current && tree) {
    const queryBuilder = this.queryBuilderRef.current;
    queryBuilder.entities = this.entities as any;
    queryBuilder.expressionTree = tree;
    queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange);
  }
}

componentWillUnmount() {
  if (this.queryBuilderRef.current) {
    this.queryBuilderRef.current.removeEventListener('expressionTreeChange', this.handleExpressionTreeChange);
  }
}

private handleExpressionTreeChange = (event: CustomEvent<IgcExpressionTree>) => {
  this.setState({ expressionTree: event.detail });
};

private get ordersFields(): Field[] {
  return [
    { field: 'orderId', dataType: 'number' },
    { field: 'customerId', dataType: 'string' },
    { field: 'orderDate', dataType: 'date' }
  ];
}

private get entities(): Entity[] {
  return [
    { name: 'Orders', fields: this.ordersFields }
  ];
}

private onExpressionTreeChange() {
  // Handle expression tree changes
  console.log('Expression tree changed:', this.state.expressionTree);
}

public render(): JSX.Element {
  return (
    <div className="container sample">
      <IgrQueryBuilder ref={this.queryBuilderRef} id="queryBuilder"></IgrQueryBuilder>
    </div>
  );
}

IgrExpressionTree はコンポーネントの state に保持されます。つまり、ExpressionTreeChange イベントを購読して、エンドユーザーが条件を作成、編集、削除して UI を変更したときに通知を受け取ることができます。イベント リスナーは componentDidMount で登録され、componentWillUnmount でクリーンアップされます。

private handleExpressionTreeChange = (event: CustomEvent<IgcExpressionTree>) => {
  this.setState({ expressionTree: event.detail });
  this.onExpressionTreeChange();
};

式のドラッグ

条件チップは、マウスのドラッグ アンド ドロップまたはキーボードによる並べ替えで簡単に位置を変更できます。これにより、ユーザーはクエリ ロジックを動的に調整できます。

  • チップをドラッグしても、変更されるのは位置のみであり、条件や内容自体は変更されません。
  • チップはグループおよびサブグループ間でもドラッグできます。たとえば、式のグループ化やグループ解除はこのドラッグ機能によって実現できます。 既存の条件をグループ化するには、まず「グループ追加」ボタンで新しいグループを追加します。その後、必要な式をドラッグしてそのグループへ移動できます。グループ解除するには、すべての条件を現在のグループ外へドラッグします。最後の条件が移動されると、そのグループは削除されます。

あるクエリ ツリーのチップを別のクエリ ツリーへドラッグすることはできません。たとえば、親クエリから内部クエリへ、またはその逆への移動はできません。

Animated Example of Query Builder Drag and Drop using the Mouse

キーボード操作

キー操作

  • Tab / Shift + Tab - 次 / 前のチップ、ドラッグ インジケーター、削除ボタン、「式を追加」ボタンへ移動します。
  • Arrow Down/Arrow Up - チップのドラッグ インジケーターにフォーカスがある場合、チップを上下に移動できます。
  • Space / Enter - フォーカスされた式が編集モードに入ります。チップの移動中であれば、新しい位置を確定します。
  • Esc - チップの並べ替えをキャンセルし、元の位置に戻します。

キーボードによる並べ替えは、マウスのドラッグ アンド ドロップと同じ機能を提供します。チップを移動した後、ユーザーは新しい位置を確定するか、並べ替えをキャンセルする必要があります。

Animated Example of Keyboard Drag and Drop Using the Ignite UI for Angular Query Builder

テンプレート

Ignite UI for React Query Builder では、コンポーネントのヘッダーおよび検索値に対してテンプレートを定義できます:

ヘッダー テンプレート

既定では、IgrQueryBuilder のヘッダーは表示されません。ヘッダーを定義するには、IgrQueryBuilderHeader コンポーネントを Query Builder 内に追加する必要があります。

検索値テンプレート

条件の検索値は、SearchValueTemplate プロパティに lit-html テンプレートを返す関数を設定することでテンプレート化できます。

SearchValueTemplate を使用する場合は、エンティティ内のすべてのフィールド型に対するテンプレートを提供する必要があります。そうしないと Query Builder は正しく動作しません。特定のカスタム テンプレートでカバーされないフィールドや条件を処理する既定 / フォールバック テンプレートを必ず実装してください。これがないと、ユーザーはそれらのフィールドの条件を編集できません。

<IgrQueryBuilder 
  ref={this.queryBuilderRef} 
  id="queryBuilder"
  searchValueTemplate={this.buildSearchValueTemplate}>
  <IgrQueryBuilderHeader title="Query Builder Template Sample"></IgrQueryBuilderHeader>
</IgrQueryBuilder>
componentDidMount() {
  if (this.queryBuilderRef.current && tree) {
    const queryBuilder = this.queryBuilderRef.current;
    queryBuilder.entities = this.entities as any;
    queryBuilder.expressionTree = tree;
  }
}

private buildSearchValueTemplate = (ctx: QueryBuilderSearchValueContext) => {
  const field = ctx.selectedField?.field;
  const condition = ctx.selectedCondition;
  const matchesEqualityCondition = condition === 'equals' || condition === 'doesNotEqual';

  if (!ctx.implicit) {
    ctx.implicit = { value: null };
  }

  if (field === 'Region' && matchesEqualityCondition) {
    return this.buildRegionSelect(ctx);
  }

  if (field === 'OrderStatus' && matchesEqualityCondition) {
    return this.buildStatusRadios(ctx);
  }

  if (ctx.selectedField?.dataType === 'date') {
    return this.buildDatePicker(ctx);
  }

  if (ctx.selectedField?.dataType === 'time') {
    return this.buildTimeInput(ctx);
  }

  return this.buildDefaultInput(ctx, matchesEqualityCondition);
};

以下は、各エディター タイプごとに 1 つのテンプレート例を示したものです:

Region Select の例:

// Field definition
{ field: 'Region', dataType: 'string' }

// Template
private buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => {
  const currentValue = ctx?.implicit?.value?.value ?? '';
  const key = `region-select-${currentValue}`;

  return (
    <IgrSelect
      className="qb-select"
      key={key}
      value={currentValue}
      change={(sender: any) => {
        const value = sender.value;
        const currentKey = ctx?.implicit?.value?.value ?? '';

        if (!value || value === currentKey) return;

        setTimeout(() => {
          ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null;
        });
      }}>
      {this.regionOptions.map(option => (
        <IgrSelectItem key={option.value} value={option.value}>
          <span>{option.text}</span>
        </IgrSelectItem>
      ))}
    </IgrSelect>
  );
};

Status Radio Group の例:

// Field definition
{ field: 'OrderStatus', dataType: 'number' }

// Template
private buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => {
  const implicitValue = ctx.implicit?.value;
  const currentValue = implicitValue === null ? '' : implicitValue.toString();
  const key = `status-radio-${currentValue}`;

  return (
    <IgrRadioGroup
      key={key}
      style={{ gap: '5px' }}
      alignment="horizontal"
      value={currentValue}
      change={(sender: any) => {
        const value = sender.value;
        if (value === undefined) return;

        const numericValue = Number(value);
        if (ctx.implicit.value === numericValue) return;

        setTimeout(() => {
          ctx.implicit.value = numericValue;
        });
      }}>
      {this.statusOptions.map(option => (
        <IgrRadio
          key={option.value}
          name="status"
          value={option.value.toString()}
          checked={option.value.toString() === currentValue}
          labelText={option.text}>
        </IgrRadio>
      ))}
    </IgrRadioGroup>
  );
};

Date Picker の例:

// Field definition
{ field: 'OrderDate', dataType: 'date' }

// Template
private buildDatePicker = (ctx: QueryBuilderSearchValueContext) => {
  const implicitValue = ctx.implicit?.value;
  const currentValue = implicitValue instanceof Date
    ? implicitValue
    : implicitValue
      ? new Date(implicitValue)
      : null;

  const allowedConditions = ['equals', 'doesNotEqual', 'before', 'after'];
  const isEnabled = allowedConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
  const key = `date-picker-${currentValue}`;

  return (
    <IgrDatePicker
      key={key}
      value={currentValue}
      disabled={!isEnabled}
      click={(sender: any) => sender.show()}
      change={(sender: any) => {
        setTimeout(() => {
          ctx.implicit.value = sender.value;
        });
      }}>
    </IgrDatePicker>
  );
};

Time Input の例:

// Field definition
{ field: 'RequiredTime', dataType: 'time' }

// Template
private buildTimeInput = (ctx: QueryBuilderSearchValueContext) => {
  const currentValue = normalizeTimeValue(ctx.implicit?.value);
  const allowedConditions = ['at', 'not_at', 'at_before', 'at_after', 'before', 'after'];
  const isDisabled = ctx.selectedField == null || allowedConditions.indexOf(ctx.selectedCondition ?? '') === -1;
  const key = `time-input-${currentValue}`;

  return (
    <IgrDateTimeInput
      key={key}
      inputFormat="hh:mm tt"
      value={currentValue}
      disabled={isDisabled}
      change={(sender: any) => {
        setTimeout(() => {
          ctx.implicit.value = sender.value;
        });
      }}>
      <div slot="prefix">
        <IgrIcon name="clock" collection="material" />
      </div>
    </IgrDateTimeInput>
  );
};

Default Input テンプレートの例:

// Field definitions for string, number, and boolean types
{ field: 'ShipCountry', dataType: 'string' }
{ field: 'OrderID', dataType: 'number' }
{ field: 'IsRushOrder', dataType: 'boolean' }

// Template that handles all these types
private buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => {
  const selectedField = ctx.selectedField;
  const dataType = selectedField?.dataType;
  const isNumber = dataType === 'number';
  const isBoolean = dataType === 'boolean';

  const placeholder = ctx.selectedCondition === 'inQuery' || ctx.selectedCondition === 'notInQuery'
    ? 'Sub-query results'
    : 'Value';

  const currentImplicitValue = ctx && ctx.implicit ? ctx.implicit.value : null;
  const currentValue = typeof currentImplicitValue === 'object' && currentImplicitValue && 'text' in currentImplicitValue
      ? equalityCondition ? currentImplicitValue.text : ''
      : currentImplicitValue;

  const inputValue = currentValue == null ? '' : currentValue;
  const disabledConditions = ['empty', 'notEmpty', 'null', 'notNull', 'inQuery', 'notInQuery'];
  const isDisabled = isBoolean || selectedField == null || disabledConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
  const key = `default-input-${inputValue}`;

  return (
    <IgrInput 
      key={key}
      value={inputValue?.toString() || ''}
      disabled={isDisabled}
      placeholder={placeholder}
      type={isNumber ? 'number' : 'text'}
      input={(sender: any) => {
        const value = sender.value;
        setTimeout(() => {
          ctx.implicit.value = isNumber
            ? value === '' ? null : Number(value)
            : value;
        });
      }}>
    </IgrInput>
  );
};

フォーマッター

条件が編集モードではないときにチップ内へ表示される検索値の見た目を変更するには、fields 配列に formatter 関数を設定します。検索値には次のように value 引数からアクセスできます:

this.ordersFields = [
  { field: 'OrderID', dataType: 'number' },
  { field: 'ShipCountry', dataType: 'string' },
  {
    field: 'OrderDate',
    dataType: 'date',
    formatter: (value: any) => value.toLocaleDateString(this.queryBuilder?.locale, { 
      month: 'short', 
      day: 'numeric', 
      year: 'numeric' 
    })
  },
  {
    field: 'Region',
    dataType: 'string',
    formatter: (value: any) => value?.text ?? value?.value ?? value
  }
];

デモ

この例では、React Query Builder コンポーネントのヘッダーおよび検索値に対するテンプレート機能と formatter 機能を確認できます。

API リファレンス

IgrQueryBuilder IgrQueryBuilderHeader

その他のリソース

コミュニティは活発で、新しいアイデアをいつでも歓迎しています。