no versions found for ApplicationFrameworkReferences / Utility/observablearray_bindData / en

ObservableArray: bindData()

引数で渡されたオブジェクトを、対象の ObservableArray のバインディングターゲットとして登録します。バインディングターゲットとなるオブジェクトは、対象の ObservableArray との間で配列操作および配列要素の変更が同期されます。

構文

observableArray.bindData(target) => boolean
observableArray.bindData(target, twoWay) => boolean

引数

  • target: object

    対象の ObservableArray とデータ同期されるオブジェクト(バインディングターゲット)です。バインディングターゲットは以下のプロパティを持つ必要があります

    • target.syncComponents(operation): (operation: object) => void

      同期処理のために呼び出される関数です。

      • operation: object

        同期処理のための差分情報を持ったオブジェクトです。

        • operation.kind : ObservableArray.OperationKind.SORT | ObservableArray.OperationKind.SPLICE

          バインディングターゲットに対して行う配列操作の種別です。SORT は並べ替え、SPLICE は要素の削除および挿入を表します。

        • operation.from : object

          バインディングターゲットまたはバインディングソースへの参照です。このプロパティはターゲットまたはソースのどちらが操作されたかを示します。

        • operation.indexMap: ({ from: number, to: number }[]?)

          配列要素の置換を表すオブジェクトの配列です。このプロパティは operation.kindSORT の場合にのみ設定されます

        • operation.startIndex: number?

          配列要素の削除または挿入の開始位置を示す整数です。このプロパティは operation.kindSPLICE の場合にのみ設定されます

        • operation.deleteCount: number?

          削除される配列要素の個数を示す整数です。このプロパティは operation.kindSPLICE の場合にのみ設定されます

        • operation.insertedItems: object[]?

          挿入される要素の配列です。このプロパティは operation.kindSPLICE の場合にのみ設定されます

    • target.onDataBinding(source): (source: ObservableArray) => void

      bindData() によって target が同期対象に設定された際に呼び出されるコールバック関数です。

      • source: ObservableArray

        バインディングソースへの参照です

  • twoWay: boolean

    対象の ObservableArray (バインディングソース)と与えられたオブジェクト(バインディングターゲット)を双方向に同期するかどうかを表す boolean です。true ならバインディングソースとバインディングターゲットは双方向に同期され、false ならバインディングソースからバインディングターゲットへの単方向の同期のみが行われます。対象の ObservableArray のプロパティ twoWayfalse なら単方向バインディングを要求した場合のみ target をバインディングターゲットとして登録し、プロパティ twoWaytrue なら単方向・双方向いずれの場合でも target をバインディングターゲットとして登録します。引数 twoWay が指定されなかった場合、プロパティ twoWay の値が代わりに使用されます。

返値: boolean

登録に成功したかどうかです。true ならバインディングターゲットの登録に成功したか、既に自身に登録済みであることを示し、false なら何らかの理由で登録に失敗したことを示します。

//  ObservableObject のバインディングターゲットの簡易な実装
const BindingTarget = class {
    curateValues() {
        const valueMap = Object.create(null);

        for (const k in this) {
            if (!Object.prototype.hasOwnProperty.call(this, k)) { continue; }
            valueMap[k] = this[k];
        }

        return valueMap;
    }

    reflectValues(valueMap) {
        const updatedValues = Object.create(null);
        const currentValues = this.curateValues();

        for (const k in currentValues) {
            if (!Object.prototype.hasOwnProperty.call(currentValues, k)) { continue; }
            if (!(k in valueMap)) { continue; }
            const candidate    = valueMap[k];
            const currentValue = currentValues[k];
            if (!Number.isNaN(candidate) && candidate !== currentValue) {
                this[k]          = candidate;
                updatedValues[k] = candidate;
            }
        }

        const source = this.source;
        if (typeof source?.reflectValues === "function") {
            let updated = false;
            for (const _ in updatedValues) {
                updated = true;
                break;
            }
            return updated ? source.reflectValues(updatedValues) : updatedValues;
        } else {
            return updatedValues;
        }
    }

    onDataBinding(source) {
        if (typeof source?.reflectValues === "function" && typeof source?.curateValues === "function") {
            this.source = source;
            this.reflectValues(source.curateValues());
        }
    }
};

//  ObservableArray のバインディングターゲットの簡易な実装
const BindingTargetArray = class extends Array {
    #ctor;

    constructor(ctor) {
        super();
        this.#ctor = ctor;
    }

    get(index) {
        return this.at(index);
    }

    splice(startIndex, deleteCount, ...insertedItems) {
        const mappedItems  = insertedItems.map(x => new this.#ctor(x));
        const removedItems = super.splice(startIndex, deleteCount, ...mappedItems);
        this.syncComponents({
            from         : this,
            kind         : ObservableArray.OperationKind.SPLICE,
            startIndex   : startIndex,
            deleteCount  : deleteCount,
            insertedItems: mappedItems
        });
        return removedItems;
    }

    sort(compare) {
        const compare_ = typeof compare === "function" ? compare : (x, y) => ((x > y) - (x < y));
        const indexMap = [];
        const valuesWithIndices = [...this.entries()];
        valuesWithIndices.sort(([, x], [, y]) => compare_(x, y));
        for (const [to, [from, fromValue]] of valuesWithIndices.entries()) {
            if (from < to) {
                indexMap.push({ from, to });
                this[from] = this[to];
                this[to]   = fromValue;
            }
        }
        this.syncComponents({
            from    : this,
            kind    : ObservableArray.OperationKind.SORT,
            indexMap: indexMap
        });
        return this;
    }

    syncComponents(operation) {
        if (operation == null || this.source == null) { return; }
        if (operation.from === this) {
            this.source.syncComponents(operation);
        } else {
            switch (operation.kind) {
                case ObservableArray.OperationKind.SORT: {
                    const indexMap = operation.indexMap;
                    for (const { from , to } of indexMap) {
                        const fromValue = this[from];
                        this[from] = this[to];
                        this[to]   = fromValue;
                    }
                    break;
                }
                case ObservableArray.OperationKind.SPLICE: {
                    const { startIndex, deleteCount, insertedItems } = operation;
                    const mappedItems = insertedItems.map(x => new this.#ctor(x.curateValues()));
                    this.splice(startIndex, deleteCount, ...mappedItems);
                    break;
                }
                default:
                    break;
            }
        }
    }

    onDataBinding(source) {
        if (typeof source?.syncComponents !== "function") { return; }
        Object.defineProperties(this, {
            source: {
                configurable: true,
                writable    : false,
                enumerable  : false,
                value       : source
            }
        });
    }
};

//  BindingTarget を継承するユーザ定義のデータクラス
const MyData = class extends BindingTarget {
    constructor({x, y}) {
        super();
        this.x = x;
        this.y = y;
    }
};
const MyArray = class extends BindingTargetArray {
    constructor() {
        super(MyData);
    }
};

const twoWayArray = new ObservableArray({ y: "foo" }, { twoWay: true, initCount: 4 });
const targetArray = new MyArray();

//  bindData で twoWayArray に対して双方向バインディングを要求する
twoWayArray.bindData(targetArray, true);

//  twoWayArray の要素に値を設定する
twoWayArray[0].reflectValues({ y: "A" });
twoWayArray[1].reflectValues({ y: "B" });
twoWayArray[2].reflectValues({ y: "C" });
twoWayArray[3].reflectValues({ y: "D" });

//  targetArray に値の変更が反映されていることを確認する
console.log([...targetArray].map(x => x.curateValues()));
//  ==> [ { y: "A" }, { y: "B" }, { y: "C" }, { y: "D" } ]

//  targetArray の要素に値を設定する
targetArray.get(0).reflectValues({ y: "E" });
targetArray.get(1).reflectValues({ y: "F" });
targetArray.get(2).reflectValues({ y: "G" });
targetArray.get(3).reflectValues({ y: "H" });

//  twoWayArray に値の変更が反映されていることを確認する
console.log([...twoWayArray].map(x => x.curateValues()));
//  ==> [ { y: "E" }, { y: "F" }, { y: "G" }, { y: "H" } ]

//  targetArray の末尾に 4 要素追加
targetArray.splice(4, 0, { y: "I" }, { y: "J" }, { y: "K" }, { y: "L" });

//  twoWayArray に要素の追加が反映されていることを確認
console.log([...twoWayArray].map(x => x.curateValues()));
//  ==> [ { y: "E" }, ..., { y: "I" }, { y: "J" }, { y: "K" }, { y: "L" } ]

//  twoWayArray の先頭に 4 要素追加
twoWayArray.splice(0, 0, 4);

//  targetArray に要素の追加が反映されていることを確認
console.log([...twoWayArray].map(x => x.curateValues()));
//  ==> [ { y: "foo" }, { y: "foo" }, { y: "foo" }, { y: "foo" }, { y: "E" }, ... ]