Skip to content

Commit

Permalink
Lodash: Refactor away from _.setWith() (#47017)
Browse files Browse the repository at this point in the history
* Lodash: Refactor away from _.setWith()

* Remove dot notation support
  • Loading branch information
tyxla authored Jan 20, 2023
1 parent d84b05a commit acd9cfe
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 2 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const restrictedImports = [
'reject',
'repeat',
'reverse',
'setWith',
'size',
'snakeCase',
'some',
Expand Down
104 changes: 104 additions & 0 deletions packages/block-editor/src/hooks/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,113 @@ import { applyFilters } from '@wordpress/hooks';
* Internal dependencies
*/
import '../anchor';
import { immutableSet } from '../utils';

const noop = () => {};

describe( 'immutableSet', () => {
describe( 'handling falsy values properly', () => {
it( 'should create a new object if `undefined` is passed', () => {
const result = immutableSet( undefined, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'should create a new object if `null` is passed', () => {
const result = immutableSet( null, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'should create a new object if `false` is passed', () => {
const result = immutableSet( false, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'should create a new object if `0` is passed', () => {
const result = immutableSet( 0, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'should create a new object if an empty string is passed', () => {
const result = immutableSet( '', 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'should create a new object if a NaN is passed', () => {
const result = immutableSet( NaN, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );
} );

describe( 'manages data assignment properly', () => {
it( 'assigns value properly when it does not exist', () => {
const result = immutableSet( {}, 'test', 1 );

expect( result ).toEqual( { test: 1 } );
} );

it( 'overrides existing values', () => {
const result = immutableSet( { test: 1 }, 'test', 2 );

expect( result ).toEqual( { test: 2 } );
} );

describe( 'with array notation access', () => {
it( 'assigns values at deeper levels', () => {
const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 );

expect( result ).toEqual( { foo: { bar: { baz: 5 } } } );
} );

it( 'overrides existing values at deeper levels', () => {
const result = immutableSet(
{ foo: { bar: { baz: 1 } } },
[ 'foo', 'bar', 'baz' ],
5
);

expect( result ).toEqual( { foo: { bar: { baz: 5 } } } );
} );

it( 'keeps other properties intact', () => {
const result = immutableSet(
{ foo: { bar: { baz: 1 } } },
[ 'foo', 'bar', 'test' ],
5
);

expect( result ).toEqual( {
foo: { bar: { baz: 1, test: 5 } },
} );
} );
} );
} );

describe( 'does not mutate the original object', () => {
it( 'clones the object at the first level', () => {
const input = {};
const result = immutableSet( input, 'test', 1 );

expect( result ).not.toBe( input );
} );

it( 'clones the object at deeper levels', () => {
const input = { foo: { bar: { baz: 1 } } };
const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 );

expect( result ).not.toBe( input );
expect( result.foo ).not.toBe( input.foo );
expect( result.foo.bar ).not.toBe( input.foo.bar );
expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz );
} );
} );
} );

describe( 'anchor', () => {
const blockSettings = {
save: noop,
Expand Down
70 changes: 68 additions & 2 deletions packages/block-editor/src/hooks/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { isEmpty, mapValues, get, setWith, clone } from 'lodash';
import { isEmpty, mapValues, get } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -30,8 +30,74 @@ export const cleanEmptyObject = ( object ) => {
return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects;
};

/**
* Converts a path to an array of its fragments.
* Supports strings, numbers and arrays:
*
* 'foo' => [ 'foo' ]
* 2 => [ '2' ]
* [ 'foo', 'bar' ] => [ 'foo', 'bar' ]
*
* @param {string|number|Array} path Path
* @return {Array} Normalized path.
*/
function normalizePath( path ) {
if ( Array.isArray( path ) ) {
return path;
} else if ( typeof path === 'number' ) {
return [ path.toString() ];
}

return [ path ];
}

/**
* Clones an object.
* Non-object values are returned unchanged.
*
* @param {*} object Object to clone.
* @return {*} Cloned object, or original literal non-object value.
*/
function cloneObject( object ) {
if ( typeof object === 'object' ) {
return {
...Object.fromEntries(
Object.entries( object ).map( ( [ key, value ] ) => [
key,
cloneObject( value ),
] )
),
};
}

return object;
}

/**
* Perform an immutable set.
* Handles nullish initial values.
* Clones all nested objects in the specified object.
*
* @param {Object} object Object to set a value in.
* @param {number|string|Array} path Path in the object to modify.
* @param {*} value New value to set.
* @return {Object} Cloned object with the new value set.
*/
export function immutableSet( object, path, value ) {
return setWith( object ? clone( object ) : {}, path, value, clone );
const normalizedPath = normalizePath( path );
const newObject = object ? cloneObject( object ) : {};

normalizedPath.reduce( ( acc, key, i ) => {
if ( acc[ key ] === undefined ) {
acc[ key ] = {};
}
if ( i === normalizedPath.length - 1 ) {
acc[ key ] = value;
}
return acc[ key ];
}, newObject );

return newObject;
}

export function transformStyles(
Expand Down

1 comment on commit acd9cfe

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in acd9cfe.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/3966829313
📝 Reported issues:

Please sign in to comment.