chromium/ui/webui/resources/cr_elements/list_property_update_mixin.ts

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview |ListPropertyUpdateMixin| is used to update an existing
 * polymer list property given the list after all the edits were made while
 * maintaining the reference to the original list. This allows
 * dom-repeat/iron-list elements bound to this list property to not fully
 * re-rendered from scratch.
 *
 * The minimal splices needed to transform the original list to the edited list
 * are calculated using |Polymer.ArraySplice.calculateSplices|. All the edits
 * are then applied to the original list. Once completed, a single notification
 * containing information about all the edits is sent to the polyer object.
 */

import type {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {calculateSplices, dedupingMixin} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';

type Constructor<T> = new (...args: any[]) => T;

export const ListPropertyUpdateMixin = dedupingMixin(
    <T extends Constructor<PolymerElement>>(superClass: T): T&
    Constructor<ListPropertyUpdateMixinInterface> => {
      class ListPropertyUpdateMixin extends superClass implements
          ListPropertyUpdateMixinInterface {
        updateList<T>(
            propertyPath: string, identityGetter: (item: T) => (T | string),
            updatedList: T[], identityBasedUpdate: boolean = false): boolean {
          const list = this.get(propertyPath);
          const splices = calculateSplices(
              updatedList.map(item => identityGetter(item)),
              list.map(identityGetter));

          splices.forEach(splice => {
            const index = splice.index;
            const deleteCount = splice.removed.length;
            // Transform splices to the expected format of notifySplices().
            // Convert !Array<string> to !Array<!Object>.
            splice.removed = list.slice(index, index + deleteCount);
            splice.object = list;
            splice.type = 'splice';

            const added = updatedList.slice(index, index + splice.addedCount);
            const spliceParams = [index, deleteCount].concat(added);
            list.splice.apply(list, spliceParams);
          });

          let updated = splices.length > 0;
          if (!identityBasedUpdate) {
            list.forEach((item: object, index: number) => {
              const updatedItem = updatedList[index];
              if (JSON.stringify(item) !== JSON.stringify(updatedItem)) {
                this.set([propertyPath, index], updatedItem);
                updated = true;
              }
            });
          }

          if (splices.length > 0) {
            this.notifySplices(propertyPath, splices);
          }
          return updated;
        }
      }
      return ListPropertyUpdateMixin;
    });

export interface ListPropertyUpdateMixinInterface {
  /** @return Whether notifySplices was called. */
  updateList<T>(
      propertyPath: string, identityGetter: (item: T) => (T | string),
      updatedList: T[], identityBasedUpdate?: boolean): boolean;
}