ManageIQ/manageiq-ui-classic

View on GitHub
app/javascript/miq-component/registry.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { writeProxy, lockInstanceProperties } from './utils';
import { cleanVirtualDom } from './helpers';

const registry = {}; // Map<name, {name, blueprint, instances: Set}>

/**
 * Get definition of a component with the given `name`.
 */
export function getDefinition(name) {
  return registry[name];
}

/**
 * Make sure the instance `id` is sane and cannot be changed.
 */
export function sanitizeAndFreezeInstanceId(instance, definition) {
  const id = instance.id || `${definition.name}-${definition.instances.size}`;

  Object.defineProperty(instance, 'id', {
    get() {
      return id;
    },
    set() {
      throw new Error(`Attempt to modify id of instance ${id}`);
    },
    enumerable: true,
  });
}

/**
 * Check the following:
 * - the given instance isn't already in the registry
 * - the given instance `id` isn't already taken
 */
export function validateInstance(instance, definition) {
  if (Array.from(definition.instances).find(existingInstance => existingInstance === instance)) {
    throw new Error('Instance already present, check your blueprint.create implementation');
  }
  if (getInstance(definition.name, instance.id)) {
    throw new Error(`Instance with id ${instance.id} already present`);
  }
}

/**
 * Implementation of the `ComponentApi.define` method.
 */
export function define(name, blueprint = {}, options = {}) {
  // validate inputs
  if (typeof name !== 'string') {
    throw new Error(`Registry.define: non-string name: ${name}`);
  }
  if (isDefined(name) && !options.override) {
    throw new Error(`Registry.define: component already exists: ${name} (use { override: true } ?)`);
  }

  // add new definition to the registry
  const instances = new Set();
  const newDefinition = { name, blueprint, instances };
  registry[name] = newDefinition;

  // add existing instances to the registry
  if (Array.isArray(options.instances)) {
    options.instances.filter((instance) => !!instance)
      .forEach((instance) => {
        sanitizeAndFreezeInstanceId(instance, newDefinition);
        validateInstance(instance, newDefinition);

        newDefinition.instances.add(instance);
      });
  }
}

/**
 * Implementation of the `ComponentApi.newInstance` method.
 */
export function newInstance(name, initialProps = {}, mountTo = undefined) {
  // clean all left over components
  cleanVirtualDom();
  // validate inputs
  const definition = getDefinition(name);
  if (!definition) {
    return;
  }

  // check if the blueprint supports instance creation
  const blueprint = definition.blueprint;
  if (typeof blueprint.create !== 'function') {
    return;
  }

  // multiple props modifications will trigger single instance update
  let newPropsForUpdate = {};
  function handlePropUpdate(propName, value) {
    if (typeof blueprint.update !== 'function') {
      return;
    }

    if (Object.keys(newPropsForUpdate).length === 0) {
      setTimeout(() => {
        newInstance.update(newPropsForUpdate);
        newPropsForUpdate = {};
      });
    }
    newPropsForUpdate[propName] = value;
  }

  // proxy props to handle instance update upon props modification
  let actualProps = writeProxy(Object.assign({}, initialProps), handlePropUpdate);

  // create new instance
  let newInstance = blueprint.create(actualProps, mountTo);
  if (!newInstance) {
    throw new Error(`blueprint.create returned falsy value when trying to instantiate ${name}`);
  }

  // make sure the instance id is sane and cannot be changed
  sanitizeAndFreezeInstanceId(newInstance, definition);

  // validate the instance
  validateInstance(newInstance, definition);

  // provide access to current props
  Object.defineProperty(newInstance, 'props', {
    get() {
      return actualProps;
    },
    set() {
      throw new Error(`Attempt to rewrite props associated with instance ${newInstance.id}`);
    },
    enumerable: true,
    configurable: true,
  });

  // provide instance update method
  newInstance.update = (newProps) => {
    if (typeof blueprint.update !== 'function') {
      return;
    }

    // update current props and delegate to blueprint
    actualProps = writeProxy(Object.assign({}, actualProps, newProps), handlePropUpdate);
    blueprint.update(actualProps, mountTo);
  };

  // provide instance destroy method
  newInstance.destroy = () => {
    // delegate to blueprint
    if (typeof blueprint.destroy === 'function') {
      blueprint.destroy(newInstance, mountTo);
    }

    // remove instance from the registry
    definition.instances.delete(newInstance);

    // prevent access to existing instance properties except for id
    lockInstanceProperties(newInstance);

    // clear instance reference
    newInstance = null;
  };

  // add instance to the registry
  definition.instances.add(newInstance);

  return newInstance;
}

/**
 * Implementation of the `ComponentApi.getInstance` method.
 */
export function getInstance(name, id) {
  const definition = getDefinition(name);
  return definition && Array.from(definition.instances).find(instance => instance.id === id);
}

/**
 * Implementation of the `ComponentApi.isDefined` method.
 */
export function isDefined(name) {
  return !! getDefinition(name);
}

/**
 * Test helper: get names of all components.
 */
export function getComponentNames() {
  return Object.keys(registry);
}

/**
 * Test helper: get all instances of the given component.
 */
export function getComponentInstances(name) {
  const definition = getDefinition(name);
  return definition ? Array.from(definition.instances) : [];
}

/**
 * Test helper: remove all component data.
 */
export function clearRegistry() {
  Object.keys(registry).forEach((k) => (delete registry[k]));
}