Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit a7e7c94

Browse files
authored
Merge pull request #204 from ckeditor/t/203
Fix: ViewCollection#destroy should wait for all ViewCollection#add promises to resolve to avoid errors. Closes #203.
2 parents cfbe329 + f06fbcd commit a7e7c94

File tree

4 files changed

+151
-31
lines changed

4 files changed

+151
-31
lines changed

src/view.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,14 @@ export default class View {
269269
* }
270270
*
271271
* @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Children views to be registered.
272+
* @returns {Promise}
272273
*/
273274
addChildren( children ) {
274275
if ( !isIterable( children ) ) {
275276
children = [ children ];
276277
}
277278

278-
for ( let child of children ) {
279-
this._unboundChildren.add( child );
280-
}
279+
return Promise.all( children.map( c => this._unboundChildren.add( c ) ) );
281280
}
282281

283282
/**
@@ -314,15 +313,14 @@ export default class View {
314313
destroy() {
315314
this.stopListening();
316315

317-
const promises = this._viewCollections.map( c => c.destroy() );
318-
319-
this._unboundChildren.clear();
320-
this._viewCollections.clear();
321-
322-
this.element = this.template = this.locale = this.t =
323-
this._viewCollections = this._unboundChildren = null;
316+
return Promise.all( this._viewCollections.map( c => c.destroy() ) )
317+
.then( () => {
318+
this._unboundChildren.clear();
319+
this._viewCollections.clear();
324320

325-
return Promise.all( promises );
321+
this.element = this.template = this.locale = this.t =
322+
this._viewCollections = this._unboundChildren = null;
323+
} );
326324
}
327325

328326
/**

src/viewcollection.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ export default class ViewCollection extends Collection {
6969
* @member {HTMLElement}
7070
*/
7171
this._parentElement = null;
72+
73+
/**
74+
* A set containing promises created by {@link #add}. If the {@link #destroy} is called
75+
* before the child views' {@link module:ui/view~View#init} are completed,
76+
* {@link #destroy} will wait until all the promises are resolved.
77+
*
78+
* @private
79+
* @member {Set}
80+
*/
81+
this._addPromises = new Set();
7282
}
7383

7484
/**
@@ -99,13 +109,13 @@ export default class ViewCollection extends Collection {
99109
* @returns {Promise} A Promise resolved when the destruction process is finished.
100110
*/
101111
destroy() {
102-
let promises = [];
103-
104-
for ( let view of this ) {
105-
promises.push( view.destroy() );
106-
}
107-
108-
return Promise.all( promises );
112+
// Wait for all #add() promises to resolve before destroying the children.
113+
// https://github.com/ckeditor/ckeditor5-ui/issues/203
114+
return Promise.all( this._addPromises )
115+
// Then begin the process of destroying the children.
116+
.then( () => {
117+
return Promise.all( this.map( v => v.destroy() ) );
118+
} );
109119
}
110120

111121
/**
@@ -123,9 +133,15 @@ export default class ViewCollection extends Collection {
123133
let promise = Promise.resolve();
124134

125135
if ( this.ready && !view.ready ) {
126-
promise = promise.then( () => {
127-
return view.init();
128-
} );
136+
promise = promise
137+
.then( () => view.init() )
138+
// The view is ready. There's no point in storing the promise any longer.
139+
.then( () => this._addPromises.delete( promise ) );
140+
141+
// Store the promise so it can be respected (and resolved) before #destroy()
142+
// starts destroying the child view.
143+
// https://github.com/ckeditor/ckeditor5-ui/issues/203
144+
this._addPromises.add( promise );
129145
}
130146

131147
return promise;

tests/view.js

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,47 @@ describe( 'View', () => {
8787
setTestViewInstance();
8888
} );
8989

90+
it( 'should return a promise', () => {
91+
const spy = sinon.spy();
92+
const child = {
93+
init: () => {
94+
return new Promise( resolve => {
95+
setTimeout( () => resolve(), 100 );
96+
} )
97+
.then( () => spy() );
98+
}
99+
};
100+
101+
return view.init()
102+
.then( () => {
103+
const returned = view.addChildren( child );
104+
expect( returned ).to.be.instanceof( Promise );
105+
106+
return returned.then( () => {
107+
sinon.assert.calledOnce( spy );
108+
} );
109+
} );
110+
} );
111+
90112
it( 'should add a single view to #_unboundChildren', () => {
91113
expect( view._unboundChildren ).to.have.length( 0 );
92114

93115
const child = {};
94116

95-
view.addChildren( child );
96-
expect( view._unboundChildren ).to.have.length( 1 );
97-
expect( view._unboundChildren.get( 0 ) ).to.equal( child );
117+
return view.addChildren( child )
118+
.then( () => {
119+
expect( view._unboundChildren ).to.have.length( 1 );
120+
expect( view._unboundChildren.get( 0 ) ).to.equal( child );
121+
} );
98122
} );
99123

100124
it( 'should support iterables', () => {
101125
expect( view._unboundChildren ).to.have.length( 0 );
102126

103-
view.addChildren( [ {}, {}, {} ] );
104-
expect( view._unboundChildren ).to.have.length( 3 );
127+
return view.addChildren( [ {}, {}, {} ] )
128+
.then( () => {
129+
expect( view._unboundChildren ).to.have.length( 3 );
130+
} );
105131
} );
106132
} );
107133

@@ -254,12 +280,14 @@ describe( 'View', () => {
254280
it( 'clears #_unboundChildren', () => {
255281
const cached = view._unboundChildren;
256282

257-
view.addChildren( [ new View(), new View() ] );
258-
expect( cached ).to.have.length.above( 2 );
283+
return view.addChildren( [ new View(), new View() ] )
284+
.then( () => {
285+
expect( cached ).to.have.length.above( 2 );
259286

260-
return view.destroy().then( () => {
261-
expect( cached ).to.have.length( 0 );
262-
} );
287+
return view.destroy().then( () => {
288+
expect( cached ).to.have.length( 0 );
289+
} );
290+
} );
263291
} );
264292

265293
it( 'clears #_viewCollections', () => {
@@ -304,6 +332,44 @@ describe( 'View', () => {
304332
view.destroy();
305333
} ).to.not.throw();
306334
} );
335+
336+
// https://github.com/ckeditor/ckeditor5-ui/issues/203
337+
it( 'waits for all #addChildren promises to resolve', () => {
338+
const spyA = sinon.spy();
339+
const spyB = sinon.spy();
340+
341+
class DelayedInitView extends View {
342+
constructor( delay, spy ) {
343+
super();
344+
345+
this.delay = delay;
346+
this.spy = spy;
347+
}
348+
349+
init() {
350+
return new Promise( resolve => {
351+
setTimeout( () => resolve(), this.delay );
352+
} )
353+
.then( () => super.init() )
354+
.then( () => {
355+
this.spy();
356+
} );
357+
}
358+
}
359+
360+
const viewA = new DelayedInitView( 200, spyA );
361+
const viewB = new DelayedInitView( 100, spyB );
362+
363+
return view.init().then( () => {
364+
view.addChildren( [ viewA, viewB ] );
365+
366+
return view.destroy().then( () => {
367+
expect( viewA.ready ).to.be.true;
368+
expect( viewB.ready ).to.be.true;
369+
sinon.assert.callOrder( spyB, spyA );
370+
} );
371+
} );
372+
} );
307373
} );
308374
} );
309375

tests/viewcollection.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,46 @@ describe( 'ViewCollection', () => {
234234
sinon.assert.callOrder( spyA, spyB );
235235
} );
236236
} );
237+
238+
// https://github.com/ckeditor/ckeditor5-ui/issues/203
239+
it( 'waits for all #add promises to resolve', () => {
240+
const spyA = sinon.spy();
241+
const spyB = sinon.spy();
242+
243+
class DelayedInitView extends View {
244+
constructor( delay, spy ) {
245+
super();
246+
247+
this.delay = delay;
248+
this.spy = spy;
249+
}
250+
251+
init() {
252+
return new Promise( resolve => {
253+
setTimeout( () => resolve(), this.delay );
254+
} )
255+
.then( () => super.init() )
256+
.then( () => {
257+
this.spy();
258+
} );
259+
}
260+
}
261+
262+
const viewA = new DelayedInitView( 200, spyA );
263+
const viewB = new DelayedInitView( 100, spyB );
264+
265+
return collection.init().then( () => {
266+
collection.add( viewA );
267+
collection.add( viewB );
268+
} )
269+
.then( () => {
270+
return collection.destroy().then( () => {
271+
expect( viewA.ready ).to.be.true;
272+
expect( viewB.ready ).to.be.true;
273+
sinon.assert.callOrder( spyB, spyA );
274+
} );
275+
} );
276+
} );
237277
} );
238278

239279
describe( 'add()', () => {

0 commit comments

Comments
 (0)