diff --git a/common/changes/@uifabric/utilities/feature-isEqual-function_2018-04-18-21-10.json b/common/changes/@uifabric/utilities/feature-isEqual-function_2018-04-18-21-10.json new file mode 100644 index 00000000000000..a20d4289cbd7c7 --- /dev/null +++ b/common/changes/@uifabric/utilities/feature-isEqual-function_2018-04-18-21-10.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/utilities", + "comment": "Add isEqual and isNotEqual utilities", + "type": "patch" + } + ], + "packageName": "@uifabric/utilities", + "email": "v-jojanz@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/feature-isEqual-function_2018-04-18-21-10.json b/common/changes/office-ui-fabric-react/feature-isEqual-function_2018-04-18-21-10.json new file mode 100644 index 00000000000000..e09bda92643719 --- /dev/null +++ b/common/changes/office-ui-fabric-react/feature-isEqual-function_2018-04-18-21-10.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Fix #3607", + "type": "patch" + } + ], + "packageName": "office-ui-fabric-react", + "email": "v-jojanz@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/List/List.tsx b/packages/office-ui-fabric-react/src/components/List/List.tsx index 49581283a9479e..849dac8177ce08 100644 --- a/packages/office-ui-fabric-react/src/components/List/List.tsx +++ b/packages/office-ui-fabric-react/src/components/List/List.tsx @@ -9,7 +9,8 @@ import { divProperties, getNativeProps, IRenderFunction, - createRef + createRef, + isEqual, } from '../../Utilities'; import { IList, IListProps, IPage, IPageProps } from './List.types'; @@ -299,7 +300,7 @@ export class List extends BaseComponent implements IList return true; } - if (newProps.items === this.props.items && + if (isEqual(newProps.items, this.props.items) && oldPages!.length === newPages!.length) { for (let i = 0; i < oldPages!.length; i++) { const oldPage = oldPages![i]; diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 962253d3a2a665..fa1fd3eaaf675a 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -31,6 +31,7 @@ export * from './hoist'; export * from './hoistStatics'; export * from './initializeFocusRects'; export * from './initials'; +export * from './isEqual'; export * from './keyboard'; export * from './language'; export * from './math'; diff --git a/packages/utilities/src/isEqual.test.ts b/packages/utilities/src/isEqual.test.ts new file mode 100644 index 00000000000000..49868aaaed5785 --- /dev/null +++ b/packages/utilities/src/isEqual.test.ts @@ -0,0 +1,125 @@ +import { isEqual, isNotEqual } from './isEqual'; + +describe('isEquals and isNotEquals function helper', () => { + describe('Test indexes', () => { + // Set up arrays to compare + const arrayBase = ['1', '2', '3']; + const arrayControl = ['1', '2', '3']; + const arrayDifferentValues = ['1', '2', '4']; + const arrayDifferentTypes = ['1', '2', 3]; + const arrayDifferentLength = ['1', '2', '3', '4']; + + describe('isEqual', () => { + it('returns true with arrays with the same values', () => { + expect(isEqual(arrayBase, arrayControl)).toEqual(true); + }); + + it('returns false with arrays with different values', () => { + expect(isEqual(arrayBase, arrayDifferentValues)).toEqual(false); + }); + + it('returns false with arrays with different types', () => { + expect(isEqual(arrayBase, arrayDifferentTypes)).toEqual(false); + }); + + it('returns false with arrays with different length', () => { + expect(isEqual(arrayBase, arrayDifferentLength)).toEqual(false); + }); + }); + + describe('isNotEqual', () => { + it('returns true with arrays with the same values', () => { + expect(isNotEqual(arrayBase, arrayControl)).toEqual(false); + }); + }); + }); + + describe('Test objects', () => { + // Set up objects to compare + const objectBase = { a: '1', b: '2' }; + const objectControl = { a: '1', b: '2' }; + const objectDifferentValues = { a: '1', b: '3' }; + const objectDifferentValueTypes = { a: '1', b: 2 }; + const objectDifferentKeyTypes = { 1: '1', 2: '2' }; + const objectDifferentLength = { a: '1', b: '2', c: '3' }; + + describe('isEqual', () => { + it('returns true with objects with the same values', () => { + expect(isEqual(objectBase, objectControl)).toEqual(true); + }); + + it('returns false with objects with different values', () => { + expect(isEqual(objectBase, objectDifferentValues)).toEqual(false); + }); + + it('returns false with objects with different value types', () => { + expect(isEqual(objectBase, objectDifferentValueTypes)).toEqual(false); + }); + + it('returns false with objects with different key types', () => { + expect(isEqual(objectBase, objectDifferentKeyTypes)).toEqual(false); + }); + + it('returns false with objects with different length', () => { + expect(isEqual(objectBase, objectDifferentLength)).toEqual(false); + }); + }); + + describe('isNotEqual', () => { + it('returns true with objects with the same values', () => { + expect(isNotEqual(objectBase, objectControl)).toEqual(false); + }); + }); + }); + + describe('Test numbers', () => { + // Set up numbers to compare + const numberBase = 123; + const numberControl = 123; + const numberDifferentValues = 1234; + const numberDifferentTypes = '123'; + + describe('isEqual', () => { + it('returns true with numbers with the same values', () => { + expect(isEqual(numberBase, numberControl)).toEqual(true); + }); + + it('returns false with numbers with different values', () => { + expect(isEqual(numberBase, numberDifferentValues)).toEqual(false); + }); + + it('returns false with numbers with different types', () => { + expect(isEqual(numberBase, numberDifferentTypes)).toEqual(false); + }); + }); + + describe('isNotEqual', () => { + it('returns true with numbers with the same values', () => { + expect(isNotEqual(numberBase, numberControl)).toEqual(false); + }); + }); + }); + + describe('Test strings', () => { + // Set up strings to compare + const stringBase = 'This is a string'; + const stringControl = 'This is a string'; + const stringDifferentValues = 'This is a string that isn\'t the same'; + + describe('isEqual', () => { + it('returns true with strings with the same values', () => { + expect(isEqual(stringBase, stringControl)).toEqual(true); + }); + + it('returns false with strings with different values', () => { + expect(isEqual(stringBase, stringDifferentValues)).toEqual(false); + }); + }); + + describe('isNotEqual', () => { + it('returns true with strings with the same values', () => { + expect(isNotEqual(stringBase, stringControl)).toEqual(false); + }); + }); + }); +}); diff --git a/packages/utilities/src/isEqual.ts b/packages/utilities/src/isEqual.ts new file mode 100644 index 00000000000000..78e0a8edee3870 --- /dev/null +++ b/packages/utilities/src/isEqual.ts @@ -0,0 +1,85 @@ +/** + * Checks if the first and second items are the same, recursively. Use for checking arrays and objects. + * + * @param itemA First item to compare to second item. + * @param itemB Second item to compare to first item. + * @returns {boolean} True if items are the same or false if not. + */ +export const isEqual = (itemA: any, itemB: any): boolean => { // tslint:disable-line no-any + // First, a simple check for strings and numbers + if (typeof itemA === 'string' || typeof itemA === 'number') { + if (itemA === itemB) { + return true; + } + return false; + } + // Get the value type + const type = Object.prototype.toString.call(itemA); + // If the two objects are not the same type, return false + if (type !== Object.prototype.toString.call(itemB)) { + return false; + } + // If items are not an object or array, return false + if (['[object Array]', '[object Object]'].indexOf(type) < 0) { + return false; + } + // Compare the length of the length of the two items + const valueLen = type === '[object Array]' ? itemA.length : Object.keys(itemA).length; + const otherLen = type === '[object Array]' ? itemB.length : Object.keys(itemB).length; + if (valueLen !== otherLen) { + return false; + } + // Compare two items + const compare = (item1: any, item2: any) => { // tslint:disable-line no-any + // Get the object type + const itemType = Object.prototype.toString.call(item1); + // If an object or array, compare recursively + if (['[object Array]', '[object Object]'].indexOf(itemType) >= 0) { + if (!isEqual(item1, item2)) { + return false; + } + } + // If the two items are not the same type, return false + if (itemType !== Object.prototype.toString.call(item2)) { + return false; + } + // Else if it's a function, convert to a string and compare + // Otherwise, just compare + if (itemType === '[object Function]') { + if (item1.toString() !== item2.toString()) { + return false; + } + } + if (item1 !== item2) { + return false; + } + }; + // Compare properties + if (type === '[object Array]') { + for (let i = 0; i < valueLen; i++) { + if (compare(itemA[i], itemB[i]) === false) { + return false; + } + } + } + for (const key in itemA) { + if (itemA.hasOwnProperty(key)) { + if (compare(itemA[key], itemB[key]) === false) { + return false; + } + } + } + // If nothing failed, return true + return true; +}; + +/** + * Checks if the first and second items are NOT the same, recursively. Use for checking arrays and objects. + * + * @param itemA First item to compare to second item. + * @param itemB Second item to compare to first item. + * @returns {boolean} True if items are NOT the same or false if they are. + */ +export const isNotEqual = (itemA: any, itemB: any): boolean => { // tslint:disable-line no-any + return !isEqual(itemA, itemB); +};