codevise/pageflow

View on GitHub
entry_types/scrolled/package/src/collections.js

Summary

Maintainability
C
1 day
Test Coverage
import {useReducer} from 'react';
import {createSelector, createSelectorCreator, defaultMemoize} from 'reselect';

const PREFIX = 'PAGEFLOW_SCROLLED_COLLECTION';
const RESET = `${PREFIX}_RESET`;
const ADD = `${PREFIX}_ADD`;
const CHANGE = `${PREFIX}_CHANGE`;
const PATCH_CONFIGURATION = `${PREFIX}_PATCH_CONFIGURATION`;
const REMOVE = `${PREFIX}_REMOVE`;
const SORT = `${PREFIX}_SORT`;

export function useCollections(seed = {}, {keyAttribute} = {}) {
  return useReducer(reducer, Object.keys(seed).reduce((result, key) => {
    result[key] = init(seed[key], keyAttribute);
    return result;
  }, {}));
}

function reducer(state, action) {
  const collectionName = action.payload.collectionName;
  const keyAttribute = action.payload.keyAttribute;

  switch (action.type) {
  case RESET:
    return {
      ...state,
      [collectionName]: init(action.payload.items, keyAttribute)
    };
  case ADD:
    return {
      ...state,
      [collectionName]: {
        order: action.payload.order,
        items: {
          ...state[collectionName].items,
          [action.payload.attributes[keyAttribute]]: action.payload.attributes
        }
      }
    }
  case CHANGE:
    return {
      ...state,
      [collectionName]: {
        order: state[collectionName].order,
        items: {
          ...state[collectionName].items,
          [action.payload.attributes[keyAttribute]]: action.payload.attributes
        }
      }
    }
  case PATCH_CONFIGURATION:
    const key = action.payload.key;

    return {
      ...state,
      [collectionName]: {
        order: state[collectionName].order,
        items: {
          ...state[collectionName].items,
          [key]: {
            ...state[collectionName].items[key],
            configuration: {
              ...state[collectionName].items[key].configuration,
              ...action.payload.configuration
            }
          }
        }
      }
    }
  case REMOVE:
    const clonedItems = {
      ...state[collectionName].items
    };
    delete clonedItems[action.payload.key];

    return {
      ...state,
      [collectionName]: {
        order: action.payload.order,
        items: clonedItems
      }
    }
  case SORT:
    return {
      ...state,
      [collectionName]: {
        order: action.payload.order,
        items: state[collectionName].items
      }
    }
  default:
    return state;
  }
}

function init(items, keyAttribute = 'id') {
  items = items.filter(item => item[keyAttribute]);

  return {
    order: items.map(item => item[keyAttribute]),
    items: items.reduce((result, item) => {
      result[item[keyAttribute]] = item;
      return result;
    }, {})
  };
}

export function updateConfiguration({dispatch, name, key, configuration}) {
  dispatch({
    type: PATCH_CONFIGURATION,
    payload: {
      collectionName: name,
      key,
      configuration
    }
  })
}

export function watchCollection(collection,
                                {name, dispatch, attributes, includeConfiguration, keyAttribute = 'id'}) {
  const handle = {};
  const options = {
    attributeNames: attributes,
    includeConfiguration
  };
  let tearingDown = false;

  const watchedAttributeNames = getWatchedAttributeNames(attributes);
  const sourceKeyAttribute = findSourceAttributeName(attributes, keyAttribute);

  dispatch({
    type: RESET,
    payload: {
      collectionName: name,
      keyAttribute: keyAttribute,
      items: collection.map(model => getAttributes(model, options))
    }
  });

  collection.on('add change:id', model => {
    if (!model.isNew()) {
      dispatch({
        type: ADD,
        payload: {
          collectionName: name,
          keyAttribute: keyAttribute,
          order: collection.pluck(sourceKeyAttribute).filter(Boolean),
          attributes: getAttributes(model, options)
        }
      });
    }
  }, handle);

  collection.on('change', model => {
    if (hasChangedAttributes(model, watchedAttributeNames) && !model.isNew()) {
      dispatch({
        type: CHANGE,
        payload: {
          collectionName: name,
          keyAttribute: keyAttribute,
          attributes: getAttributes(model, options)
        }
      });
    }
  }, handle);

  if (includeConfiguration) {
    collection.on('change:configuration', (model, value, {ignoreInWatchCollection} = {}) => {
      if (!model.isNew() && !ignoreInWatchCollection) {
        dispatch({
          type: CHANGE,
          payload: {
            collectionName: name,
            keyAttribute: keyAttribute,
            attributes: getAttributes(model, options)
          }
        })
      }
    }, handle);
  }

  collection.on('remove', model => {
    if (!tearingDown && !model.isNew()) {
      dispatch({
        type: REMOVE,
        payload: {
          collectionName: name,
          order: collection.pluck(sourceKeyAttribute).filter(Boolean),
          key: model.attributes[sourceKeyAttribute]
        }
      })
    }
  }, handle);

  collection.on('sort', model => dispatch({
    type: SORT,
    payload: {
      collectionName: name,
      order: collection
        .pluck(sourceKeyAttribute)
        .filter(Boolean)
    }
  }), handle);

  return function() {
    tearingDown = true;
    collection.off(null, null, handle);
  };
}

function findSourceAttributeName(attributeNames, targetAttributeName) {
  const mapping = attributeNames.find(attributeName =>
    typeof attributeName === 'object' && mappedAttributeTarget(attributeName) === targetAttributeName
  );

  return mapping ? mappedAttributeSource(mapping) : targetAttributeName;
}

function hasChangedAttributes(model, attributeNames) {
  return attributeNames.some(attributeName => model.hasChanged(attributeName));
}

function getWatchedAttributeNames(attributeNames) {
  return attributeNames.map(attributeName =>
    typeof attributeName == 'object' ? mappedAttributeSource(attributeName) : attributeName
  );
}

function mappedAttributeSource(attributeName) {
  return attributeName[Object.keys(attributeName)[0]];
}

function mappedAttributeTarget(attributeName) {
  return Object.keys(attributeName)[0];
}

function getAttributes(model, {attributeNames, includeConfiguration}) {
  const result = attributeNames.reduce((result, attributeName) => {
    if (typeof attributeName == 'object') {
      const key = Object.keys(attributeName)[0];
      const value = attributeName[key];

      if (typeof value == 'function') {
        result[key] = value(model.get(key));
      }
      else {
        result[key] = model.get(value);
      }
    }
    else {
      result[attributeName] = model.get(attributeName);
    }

    return result;
  }, {});

  if (includeConfiguration) {
    result.configuration = {...model.configuration.attributes};
  };

  return result;
}

export function getItems(state, collectionName) {
  return toOrderedItems(state[collectionName]);
}

export function getItem(state, collectionName, key) {
  if (state[collectionName]) {
    return state[collectionName].items[key];
  }
}

export function createMultipleItemsSelector(collectionNames, filter) {
  return createSelector(
    ...collectionNames.map(collectionName =>
      collections => collections[collectionName]
    ),
    (...collections) => {
      return collectionNames.reduce((result, collectionName, index) => {
        result[collectionName] = toOrderedItems(collections[index]);
        return result;
      }, {});
    }
  );
}

export function createItemsSelector(collectionName, filter) {
  if (filter) {
    const itemsSelector = createItemsSelector(collectionName)

    return createShallowEqualArraysSelector(
      collections => itemsSelector(collections).filter(filter),
      items => items
    );
  }

  return createSelector(
    collections => collections[collectionName],
    toOrderedItems
  );
}

function toOrderedItems(collection) {
  if (collection) {
    const items = collection.items;
    return collection.order.map(key => items[key]);
  }
  else {
    return [];
  }
}

const createShallowEqualArraysSelector = createSelectorCreator(
  defaultMemoize,
  shallowEqualArrays
)

function shallowEqualArrays(a, b) {
  return a.length === b.length && a.every((item, index) => item === b[index]);
}