Skip to content

Commit 07a493f

Browse files
committed
save(false, true) + enable() error fix
#fix 1793 * `save(false, true)` followed by enable() throws error. * we now have new `Utils.cloneDeep()` (needed for grid options) and added test cases.
1 parent e0d7e50 commit 07a493f

File tree

4 files changed

+148
-6
lines changed

4 files changed

+148
-6
lines changed

doc/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Change log
6565
* fix [#1791](https://github.com/gridstack/gridstack.js/pull/1791) removed drag flicker and scroll issue. Thanks [@nelsieborja](https://github.com/nelsieborja)
6666
* fix [#1795](https://github.com/gridstack/gridstack.js/issues/1795) `save(false)` will no longer have `.content` field (removed existing one if present)
6767
* fix [#1782](https://github.com/gridstack/gridstack.js/issues/1782) `save(false, false)` now correctly saves nested grids
68+
* fix [#1793](https://github.com/gridstack/gridstack.js/issues/1793) `save(false, true)` followed by enable() throws error. we now have new `Utils.cloneDeep()`
6869

6970
## 4.2.5 (2021-5-31)
7071

spec/utils-spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,116 @@ describe('gridstack utils', function() {
129129
});
130130
});
131131

132+
describe('clone', () => {
133+
const a: any = {first: 1, second: 'text'};
134+
const b: any = {first: 1, second: {third: 3}};
135+
const c: any = {first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}};
136+
it('Should have the same values', () => {
137+
const z = Utils.clone(a);
138+
expect(z).toEqual({first: 1, second: 'text'});
139+
});
140+
it('Should have 2 in first key, and original unchanged', () => {
141+
const z = Utils.clone(a);
142+
z.first = 2;
143+
expect(a).toEqual({first: 1, second: 'text'});
144+
expect(z).toEqual({first: 2, second: 'text'});
145+
});
146+
it('Should have new string in second key, and original unchanged', () => {
147+
const z = Utils.clone(a);
148+
z.second = 2;
149+
expect(a).toEqual({first: 1, second: 'text'});
150+
expect(z).toEqual({first: 1, second: 2});
151+
});
152+
it('new string in both cases - use cloneDeep instead', () => {
153+
const z = Utils.clone(b);
154+
z.second.third = 'share';
155+
expect(b).toEqual({first: 1, second: {third: 'share'}});
156+
expect(z).toEqual({first: 1, second: {third: 'share'}});
157+
});
158+
it('Array Should match', () => {
159+
const z = Utils.clone(c);
160+
expect(c).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
161+
expect(z).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
162+
});
163+
it('Array[0] changed in both cases - use cloneDeep instead', () => {
164+
const z = Utils.clone(c);
165+
z.second[0] = 0;
166+
expect(c).toEqual({first: 1, second: [0, 2, 3], third: { fourth: {fifth: 5}}});
167+
expect(z).toEqual({first: 1, second: [0, 2, 3], third: { fourth: {fifth: 5}}});
168+
});
169+
it('fifth changed in both cases - use cloneDeep instead', () => {
170+
const z = Utils.clone(c);
171+
z.third.fourth.fifth = 'share';
172+
expect(c).toEqual({first: 1, second: [0, 2, 3], third: { fourth: {fifth: 'share'}}});
173+
expect(z).toEqual({first: 1, second: [0, 2, 3], third: { fourth: {fifth: 'share'}}});
174+
});
175+
});
176+
describe('cloneDeep', () => {
177+
// reset our test cases
178+
const a: any = {first: 1, second: 'text'};
179+
const b: any = {first: 1, second: {third: 3}};
180+
const c: any = {first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}};
181+
const d: any = {first: [1, [2, 3], ['four', 'five', 'six']]};
182+
const e: any = {first: 1, __skip: {second: 2}};
183+
const f: any = {first: 1, _dontskip: {second: 2}};
184+
185+
it('Should have the same values', () => {
186+
const z = Utils.cloneDeep(a);
187+
expect(z).toEqual({first: 1, second: 'text'});
188+
});
189+
it('Should have 2 in first key, and original unchanged', () => {
190+
const z = Utils.cloneDeep(a);
191+
z.first = 2;
192+
expect(a).toEqual({first: 1, second: 'text'});
193+
expect(z).toEqual({first: 2, second: 'text'});
194+
});
195+
it('Should have new string in second key, and original unchanged', () => {
196+
const z = Utils.cloneDeep(a);
197+
z.second = 2;
198+
expect(a).toEqual({first: 1, second: 'text'});
199+
expect(z).toEqual({first: 1, second: 2});
200+
});
201+
it('Should have new string nested object, and original unchanged', () => {
202+
const z = Utils.cloneDeep(b);
203+
z.second.third = 'diff';
204+
expect(b).toEqual({first: 1, second: {third: 3}});
205+
expect(z).toEqual({first: 1, second: {third: 'diff'}});
206+
});
207+
it('Array Should match', () => {
208+
const z = Utils.cloneDeep(c);
209+
expect(c).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
210+
expect(z).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
211+
});
212+
it('Array[0] changed in z only', () => {
213+
const z = Utils.cloneDeep(c);
214+
z.second[0] = 0;
215+
expect(c).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
216+
expect(z).toEqual({first: 1, second: [0, 2, 3], third: { fourth: {fifth: 5}}});
217+
});
218+
it('nested firth element changed only in z', () => {
219+
const z = Utils.cloneDeep(c);
220+
z.third.fourth.fifth = 'diff';
221+
expect(c).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 5}}});
222+
expect(z).toEqual({first: 1, second: [1, 2, 3], third: { fourth: {fifth: 'diff'}}});
223+
});
224+
it('nested array only has one item changed', () => {
225+
const z = Utils.cloneDeep(d);
226+
z.first[1] = 'two';
227+
z.first[2][2] = 6;
228+
expect(d).toEqual({first: [1, [2, 3], ['four', 'five', 'six']]});
229+
expect(z).toEqual({first: [1, 'two', ['four', 'five', 6]]});
230+
});
231+
it('skip __ items so it mods both instance', () => {
232+
const z = Utils.cloneDeep(e);
233+
z.__skip.second = 'two';
234+
expect(e).toEqual({first: 1, __skip: {second: 'two'}}); // TODO support clone deep of function workaround
235+
expect(z).toEqual({first: 1, __skip: {second: 'two'}});
236+
});
237+
it('correctly copy _ item', () => {
238+
const z = Utils.cloneDeep(f);
239+
z._dontskip.second = 'two';
240+
expect(f).toEqual({first: 1, _dontskip: {second: 2}});
241+
expect(z).toEqual({first: 1, _dontskip: {second: 'two'}});
242+
});
243+
});
132244
});

src/gridstack.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class GridStack {
121121
return null;
122122
}
123123
if (!el.gridstack) {
124-
el.gridstack = new GridStack(el, {...options});
124+
el.gridstack = new GridStack(el, Utils.cloneDeep(options));
125125
}
126126
return el.gridstack
127127
}
@@ -139,7 +139,7 @@ export class GridStack {
139139
let grids: GridStack[] = [];
140140
GridStack.getGridElements(selector).forEach(el => {
141141
if (!el.gridstack) {
142-
el.gridstack = new GridStack(el, {...options});
142+
el.gridstack = new GridStack(el, Utils.cloneDeep(options));
143143
delete options.dragIn; delete options.dragInOptions; // only need to be done once (really a static global thing, not per grid)
144144
}
145145
grids.push(el.gridstack);
@@ -247,7 +247,7 @@ export class GridStack {
247247
let rowAttr = Utils.toNumber(el.getAttribute('gs-row'));
248248

249249
// elements attributes override any passed options (like CSS style) - merge the two together
250-
let defaults: GridStackOptions = {...GridDefaults,
250+
let defaults: GridStackOptions = {...Utils.cloneDeep(GridDefaults),
251251
column: Utils.toNumber(el.getAttribute('gs-column')) || 12,
252252
minRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-min-row')) || 0,
253253
maxRow: rowAttr ? rowAttr : Utils.toNumber(el.getAttribute('gs-max-row')) || 0,
@@ -412,7 +412,7 @@ export class GridStack {
412412
// as the actual value are filled in when _prepareElement() calls el.getAttribute('gs-xyz) before adding the node.
413413
// So make sure we load any DOM attributes that are not specified in passed in options (which override)
414414
let domAttr = this._readAttr(el);
415-
options = {...(options || {})}; // make a copy before we modify in case caller re-uses it
415+
options = Utils.cloneDeep(options) || {}; // make a copy before we modify in case caller re-uses it
416416
Utils.defaults(options, domAttr);
417417
let node = this.engine.prepareNode(options);
418418
this._writeAttr(el, options);
@@ -470,7 +470,7 @@ export class GridStack {
470470

471471
// check if save entire grid options (needed for recursive) + children...
472472
if (saveGridOpt) {
473-
let o: GridStackOptions = {...this.opts};
473+
let o: GridStackOptions = Utils.cloneDeep(this.opts);
474474
// delete default values that will be recreated on launch
475475
if (o.marginBottom === o.marginTop && o.marginRight === o.marginLeft && o.marginTop === o.marginRight) {
476476
o.margin = o.marginTop;
@@ -980,7 +980,7 @@ export class GridStack {
980980
GridStack.getElements(els).forEach(el => {
981981
if (!el || !el.gridstackNode) return;
982982
let n = el.gridstackNode;
983-
let w = {...opt}; // make a copy we can modify in case they re-use it or multiple items
983+
let w = Utils.cloneDeep(opt); // make a copy we can modify in case they re-use it or multiple items
984984
delete w.autoPosition;
985985

986986
// move/resize widget if anything changed

src/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,5 +370,34 @@ export class Utils {
370370
scrollEl.scrollBy({ behavior: 'smooth', top: distance - (height - pointerPosY)});
371371
}
372372
}
373+
374+
/** single level clone, returning a new object with same top fields. This will share sub objects and arrays */
375+
static clone<T>(obj: T): T {
376+
if (obj === null || obj === undefined || typeof(obj) !== 'object') {
377+
return obj;
378+
}
379+
// return Object.assign({}, obj);
380+
if (obj instanceof Array) {
381+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
382+
return [...obj] as any;
383+
}
384+
return {...obj};
385+
}
386+
387+
/**
388+
* Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY.
389+
* Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies.
390+
*/
391+
static cloneDeep<T>(obj: T): T {
392+
// return JSON.parse(JSON.stringify(obj)); // doesn't work with date format ?
393+
const ret = Utils.clone(obj);
394+
for (const key in ret) {
395+
// NOTE: we don't support function/circular dependencies so skip those properties for now...
396+
if (ret.hasOwnProperty(key) && typeof(ret[key]) === 'object' && key.substring(0, 2) !== '__') {
397+
ret[key] = Utils.cloneDeep(obj[key]);
398+
}
399+
}
400+
return ret;
401+
}
373402
}
374403

0 commit comments

Comments
 (0)