scripts/django_cradmin/widgets/AbstractDataListWidget.js | django_cradmin API Document

# scripts/django_cradmin/widgets/AbstractDataListWidget.js

    import AbstractWidget from "ievv_jsbase/widget/AbstractWidget";
    import makeCustomError from "ievv_jsbase/makeCustomError";

    let CancelledDataRequest = makeCustomError('CancelledDataRequest');

    export default class AbstractDataListWidget extends AbstractWidget {

      getDefaultConfig() {
        return {
          signalNameSpace: null,
          keyAttribute: 'id',
          multiselect: false,
          selectedKeys: [],
          minimumSearchStringLength: 0,
          initialSearchString: '',
          filters: {}
        };
      }

      constructor(element, widgetInstanceId) {
        super(element, widgetInstanceId);
        this._name = `${this.classPath}.${widgetInstanceId}`;
        this.logger = new window.ievv_jsbase_core.LoggerSingleton().getLogger(
          this._name);
        this._onSearchValueChangeSignal = this._onSearchValueChangeSignal.bind(this);
        this._onSetFiltersSignal = this._onSetFiltersSignal.bind(this);
        this._onPatchFiltersSignal = this._onPatchFiltersSignal.bind(this);
        this._onSelectItemSignal = this._onSelectItemSignal.bind(this);
        this._onDeSelectItemSignal = this._onDeSelectItemSignal.bind(this);
        this._onFocusSignal = this._onFocusSignal.bind(this);
        this._onBlurSignal = this._onBlurSignal.bind(this);
        this._onLoadMoreSignal = this._onLoadMoreSignal.bind(this);
        this._onLoadNextPageSignal = this._onLoadNextPageSignal.bind(this);
        this._onLoadPreviousPageSignal = this._onLoadPreviousPageSignal.bind(this);
        this._dataRequestId = 0;
        this._isLoadingDataList = false;
        if(this.config.signalNameSpace == null) {
          throw new Error('The signalNameSpace config is required.');
        }
        this.state = null;
        this.signalHandlersInitialized = false;
      }

      _objectToMap(object) {
        const map = new Map();
        for(let key of Object.keys(object)) {
          map.set(key, object[key]);
        }
        return map;
      }

      _initialize(state) {
        this._initializeSignalHandlers();
        this.signalHandlersInitialized = true;
        this.state = {
          data: {
            count: 0,
            next: null,
            previous: null,
            results: []
          },
          selectedItemsMap: new Map(),
          searchString: '',
          filtersMap: this._objectToMap(this.config.filters),
          focus: false,
          loading: false
        };
        this._sendDataListInitializedSignal();
        this.setState(state, true);
      }

      _makeItemMapFromArray(itemDataArray) {
        const itemMap = new Map();
        for(let itemData of itemDataArray) {
          itemMap.set(this._getKeyFromItemData(itemData), itemData);
        }
        return itemMap;
      }

      _requestItemDataForKeys(keys) {
        const promises = [];
        for(let key of keys) {
          promises.push(this.requestItemData(key));
        }
        return Promise.all(promises);
      }

      _loadInitialState() {
        const state = {
          searchString: this.config.initialSearchString
        };
        this._requestItemDataForKeys(this.config.selectedKeys)
          .then((selectedItemsArray) => {
            state.setSelectedItems = selectedItemsArray;
            this._initialize(state);
          })
          .catch((error) => {
            this.logger.error('Failed to load config.selectedKeys:', this.config.selectedKeys, '. Error:', error.toString());
            this._initialize(state);
          });
      }

      useAfterInitializeAllWidgets() {
        return true;
      }

      afterInitializeAllWidgets() {
        this._loadInitialState();
      }

      _initializeSignalHandlers() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.SearchValueChange`,
          this._name,
          this._onSearchValueChangeSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.SetFilters`,
          this._name,
          this._onSetFiltersSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.PatchFilters`,
          this._name,
          this._onPatchFiltersSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.SelectItem`,
          this._name,
          this._onSelectItemSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.DeSelectItem`,
          this._name,
          this._onDeSelectItemSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.Focus`,
          this._name,
          this._onFocusSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.Blur`,
          this._name,
          this._onBlurSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.LoadMore`,
          this._name,
          this._onLoadMoreSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.LoadNextPage`,
          this._name,
          this._onLoadNextPageSignal
        );
        new window.ievv_jsbase_core.SignalHandlerSingleton().addReceiver(
          `${this.config.signalNameSpace}.LoadPreviousPage`,
          this._name,
          this._onLoadPreviousPageSignal
        );
      }

      destroy() {
        if(this.signalHandlersInitialized) {
          new window.ievv_jsbase_core.SignalHandlerSingleton()
            .removeAllSignalsFromReceiver(this._name);
        }
      }

      _updateSearchStringStateChange(stateChange, stateChangesSet) {
        if(stateChange.searchString != undefined) {
          this.state.searchString = stateChange.searchString;
          if(stateChange.searchString.length >= this.config.minimumSearchStringLength) {
            stateChangesSet.add('searchString');
          } else {
            stateChange.data = {
              count: 0,
              next: null,
              previous: null,
              results: []
            }
          }
        }
      }

      _updateFiltersStateChange(stateChange, stateChangesSet) {
        if(stateChange.filtersMap != undefined) {
          this.state.filtersMap = stateChange.filtersMap;
          stateChangesSet.add('filters');
        }
        if(stateChange.filtersMapPatch != undefined) {
          for(let [filterKey, filterValue] of stateChange.filtersMapPatch) {
            this.state.filtersMap.set(filterKey, filterValue);
          }
          stateChangesSet.add('filters');
        }
      }

      _updateDataStateChange(stateChange, stateChangesSet) {
        if(stateChange.data != undefined) {
          this.state.data = stateChange.data;
          stateChangesSet.add('data');
        } else if(stateChange.appendData != undefined) {
          for(let itemData of stateChange.appendData.results) {
            this.state.data.results.push(itemData);
          }
          this.state.data.count = stateChange.appendData.count;
          this.state.data.next = stateChange.appendData.next;
          this.state.data.previous = stateChange.appendData.previous;
          stateChangesSet.add('data');
        }
      }

      _updateSelectionStateChange(stateChange, stateChangesSet) {
        if(stateChange.addSelectedItem != undefined) {
          if(!this.config.multiselect) {
            this.state.selectedItemsMap.clear();
          }
          this.state.selectedItemsMap.set(
            this._getKeyFromItemData(stateChange.addSelectedItem), stateChange.addSelectedItem);
          stateChangesSet.add('selection');
        }
        if(stateChange.removeSelectedItem != undefined) {
          if(this.config.multiselect) {
            this.state.selectedItemsMap.delete(
              this._getKeyFromItemData(stateChange.removeSelectedItem));
          } else {
            this.state.selectedItemsMap.clear();
          }
          stateChangesSet.add('selection');
        }
        if(stateChange.clearSelectedKeys != undefined) {
          this.state.selectedItemsMap.clear();
          stateChangesSet.add('selection');
        }
        if(stateChange.setSelectedItems != undefined) {
          this.state.selectedItemsMap = this._makeItemMapFromArray(stateChange.setSelectedItems);
          stateChangesSet.add('selection');
        }
      }

      _updateFocusStateChange(stateChange, stateChangesSet) {
        if(stateChange.focus != undefined && this.state.focus != stateChange.focus) {
          this.state.focus = stateChange.focus;
          stateChangesSet.add('focus');
        }
      }

      _updateLoadingStateChange(stateChange, stateChangesSet) {
        if(stateChange.loading != undefined && this.state.loading != stateChange.loading) {
          this.state.loading = stateChange.loading;
          stateChangesSet.add('loading');
        }
      }

      _hasAnyFiltersOrSearchString() {
        return this.state.searchString != '' && this.state.filtersMap.size == 0;
      }

      setState(stateChange, initial=false) {
        const stateChangesSet = new Set();
        this._updateSearchStringStateChange(stateChange, stateChangesSet);
        this._updateFiltersStateChange(stateChange, stateChangesSet);
        this._updateDataStateChange(stateChange, stateChangesSet);
        this._updateSelectionStateChange(stateChange, stateChangesSet);
        this._updateFocusStateChange(stateChange, stateChangesSet);
        this._updateLoadingStateChange(stateChange, stateChangesSet);

        if(stateChangesSet.has('data')) {
          if(!this._hasAnyFiltersOrSearchString()) {
            if(this.state.data.count == 0) {
              this.state.isEmpty = true;
              this._sendIsEmpyDataListSignal();
            } else if(this.state.isEmpty || this.state.isEmpty == undefined) {
              this.state.isEmpty = false;
              this._sendNotEmpyDataListSignal();
            }
          }
          this._sendDataChangeSignal();
        }
        if(stateChangesSet.has('selection')) {
          this._sendSelectionChangeSignal();
        }
        if(stateChangesSet.has('searchString') || stateChangesSet.has('filters')) {
          this._requestDataListAndRefresh(this.makeRequestDataListOptions());
        }
        if(stateChangesSet.has('loading')) {
          this._sendLoadingStateChangeSignal();
        }
        if(stateChangesSet.has('filters')) {
          this._sendFiltersChangeSignal();
        }
        this._sendStateChangeSignal(stateChangesSet);
      }

      _sendIsEmpyDataListSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.IsEmpyDataList`
        );
      }

      _sendNotEmpyDataListSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.NotEmpyDataList`
        );
      }

      _sendDataChangeSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.DataChange`,
          this.state.data
        );
      }

      _sendFiltersChangeSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.FiltersChange`,
          this.state.filtersMap
        );
      }

      _sendSelectionChangeSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.SelectionChange`,
          {selectedItemsMap: this.state.selectedItemsMap}
        );
      }

      _sendLoadingStateChangeSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.LoadingStateChange`,
          this.state.loading
        );
      }

      _sendLostFocusSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.LostFocus`,
          this.state.loading
        );
      }

      _sendStateChangeSignal(stateChanges) {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.StateChange`,
          {state: this.state, stateChanges: stateChanges}
        );
      }

      _sendDataListInitializedSignal() {
        new window.ievv_jsbase_core.SignalHandlerSingleton().send(
          `${this.config.signalNameSpace}.DataListInitialized`,
          this.state
        );
      }

      _onSearchValueChangeSignal(receivedSignalInfo) {
        this.logger.debug('Received:', receivedSignalInfo.toString());
        this.setState({
          searchString: receivedSignalInfo.data
        });
      }

      _onSetFiltersSignal(receivedSignalInfo) {
        this.logger.debug('Received:', receivedSignalInfo.toString());
        this.setState({
          filtersMap: receivedSignalInfo.data
        });
      }

      _onPatchFiltersSignal(receivedSignalInfo) {
        this.logger.debug('Received:', receivedSignalInfo.toString());
        this.setState({
          filtersMapPatch: receivedSignalInfo.data
        });
      }

      _getKeyFromItemData(itemData) {
        return itemData[this.config.keyAttribute];
      }

      _onSelectItemSignal(receivedSignalInfo) {
        this.logger.debug('Received:', receivedSignalInfo.toString());
        const itemData = receivedSignalInfo.data;
        this.setState({
          addSelectedItem: itemData
        });
      }

      _onDeSelectItemSignal(receivedSignalInfo) {
        this.logger.debug('Received:', receivedSignalInfo.toString());
        const itemData = receivedSignalInfo.data;
        this.setState({
          removeSelectedItem: itemData
        });
      }

      _cancelBlurTimer() {
        if(this._blurTimeoutId != undefined) {
          window.clearTimeout(this._blurTimeoutId);
        }
      }

      _startBlurTimer(callback) {
        this._blurTimeoutId = window.setTimeout(
          callback,
          200);
      }

      _onFocusSignal(receivedSignalInfo) {
        this._cancelBlurTimer();
        this.setState({
          focus: true
        });
      }

      _onBlurSignal(receivedSignalInfo) {
        this._startBlurTimer(() => {
          this.setState({
            focus: false
          });
          this._sendLostFocusSignal();
        });
      }

      _onLoadMoreSignal(receivedSignalInfo) {
        if(this._isLoadingDataList) {
          this.logger.warning('Requested LoadMore while loading data.');
          return;
        }
        if(!this.state.data.next) {
          this.logger.warning('Requested LoadMore with no next page.');
          return;
        }
        this._requestDataListAndRefresh(this.makeRequestDataListOptions({
          next: true
        }), 'appendData');
      }

      _onLoadNextPageSignal(receivedSignalInfo) {
        if(!this.state.data.next) {
          this.logger.warning('Requested LoadNextPage with no next page.');
          return;
        }
        this._requestDataListAndRefresh(this.makeRequestDataListOptions({
          next: true
        }), 'data');
      }

      _onLoadPreviousPageSignal(receivedSignalInfo) {
        if(!this.state.data.previous) {
          this.logger.warning('Requested LoadPreviousPage with no previous page.');
          return;
        }
        this._requestDataListAndRefresh(this.makeRequestDataListOptions({
          previous: true
        }), 'data');
      }

      requestItemData(key) {
        throw new Error('requestItemData must be implemented in subclasses of AbstractDataListWidget.');
      }

      requestDataList() {
        throw new Error('requestDataList must be implemented in subclasses of AbstractDataListWidget.');
      }

      _cancelLoadingStateTimer() {
        if(this._loadingStateTimeoutId != undefined) {
          window.clearTimeout(this._loadingStateTimeoutId);
        }
      }

      _startLoadingStateTimer() {
        this._loadingStateTimeoutId = window.setTimeout(() => {
          this.setState({
            loading: true
          });
        }, 200);
      }

      _setLoadingState() {
        this._startLoadingStateTimer();
      }
      _unsetLoadingState() {
        this._cancelLoadingStateTimer();
        this.setState({
          loading: false
        });
      }

      _requestDataList(options) {
        return new Promise((resolve, reject) => {
          const dataRequestId = ++this._dataRequestId;
          // this.logger.debug(`Requesting data ${dataRequestId}`, options);
          this._isLoadingDataList = true;
          this._setLoadingState();
          this.requestDataList(options)
            .then((data) => {
              this._isLoadingDataList = false;
              this._unsetLoadingState();
              if(this._dataRequestId == dataRequestId) {
                resolve(data);
              } else {
                reject(new CancelledDataRequest(
                  `Data list request ${dataRequestId} was cancelled.`, {
                    options: options
                  }));
              }
            })
            .catch((error) => {
              this._isLoadingDataList = false;
              this._unsetLoadingState();
              reject(error);
            });
        });
      }

      makeRequestDataListOptions(overrideOptions={}) {
        return Object.assign({}, {
          filtersMap: this.state.filtersMap,
          searchString: this.state.searchString
        }, overrideOptions);
      }

      _requestDataListAndRefresh(options, stateChangeAttribute='data') {
        this._requestDataList(options)
          .then((data) => {
            const stateChange = {};
            stateChange[stateChangeAttribute] = data;
            this.setState(stateChange);
          })
          .catch((error) => {
            if(error instanceof CancelledDataRequest) {
              this.logger.warning(error.toString(), error.options);
            } else {
              throw error;
            }
          });
      }
    }