diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/src/controller/datacontroller.js
index eab30b12e8e..52e9ec2029e 100644
--- a/packages/ckeditor5-engine/src/controller/datacontroller.js
+++ b/packages/ckeditor5-engine/src/controller/datacontroller.js
@@ -133,6 +133,7 @@ export default class DataController {
this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
this.decorate( 'init' );
+ this.decorate( 'set' );
// Fire `ready` event when initialisation has completed. Such low level listener gives possibility
// to plug into initialisation pipeline without interrupting the initialisation flow.
@@ -317,6 +318,7 @@ export default class DataController {
*
* dataController.set( { main: '
Foo
', title: 'Bar
' } ); // Sets data on the `main` and `title` roots.
*
+ * @fires set
* @param {String|Object.} data Input data as a string or an object containing `rootName` - `data`
* pairs to set data on multiple roots at once.
*/
@@ -452,6 +454,15 @@ export default class DataController {
*
* @event init
*/
+
+ /**
+ * Event fired after {@link #set set() method} has been run.
+ *
+ * The `set` event is fired by decorated {@link #set} method.
+ * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
+ *
+ * @event set
+ */
}
mix( DataController, ObservableMixin );
diff --git a/packages/ckeditor5-engine/tests/controller/datacontroller.js b/packages/ckeditor5-engine/tests/controller/datacontroller.js
index cdd2b60eb2d..e21c1bf2967 100644
--- a/packages/ckeditor5-engine/tests/controller/datacontroller.js
+++ b/packages/ckeditor5-engine/tests/controller/datacontroller.js
@@ -258,6 +258,16 @@ describe( 'DataController', () => {
} );
describe( 'set()', () => {
+ it( 'should be decorated', () => {
+ const spy = sinon.spy();
+
+ data.on( 'set', spy );
+
+ data.set( 'foo bar' );
+
+ sinon.assert.calledWithExactly( spy, sinon.match.any, [ 'foo bar' ] );
+ } );
+
it( 'should set data to default main root', () => {
schema.extend( '$text', { allowIn: '$root' } );
data.set( 'foo' );
diff --git a/packages/ckeditor5-undo/src/basecommand.js b/packages/ckeditor5-undo/src/basecommand.js
index 5d272ebf714..ff54d00ee22 100644
--- a/packages/ckeditor5-undo/src/basecommand.js
+++ b/packages/ckeditor5-undo/src/basecommand.js
@@ -41,6 +41,8 @@ export default class BaseCommand extends Command {
// Refresh state, so the command is inactive right after initialization.
this.refresh();
+
+ this.listenTo( editor.data, 'set', () => this.clearStack() );
}
/**
diff --git a/packages/ckeditor5-undo/tests/redocommand.js b/packages/ckeditor5-undo/tests/redocommand.js
index 51d9bf4bc07..a76a65fa83e 100644
--- a/packages/ckeditor5-undo/tests/redocommand.js
+++ b/packages/ckeditor5-undo/tests/redocommand.js
@@ -288,5 +288,13 @@ describe( 'RedoCommand', () => {
expect( undoSpy.firstCall.args[ 1 ] ).to.equal( redoingBatch );
} );
} );
+
+ it( 'should clear stack on DataController set()', () => {
+ const spy = sinon.stub( redo, 'clearStack' );
+
+ editor.setData( 'foo' );
+
+ sinon.assert.called( spy );
+ } );
} );
} );
diff --git a/packages/ckeditor5-undo/tests/undocommand.js b/packages/ckeditor5-undo/tests/undocommand.js
index fd02da418b9..b7056590b2d 100644
--- a/packages/ckeditor5-undo/tests/undocommand.js
+++ b/packages/ckeditor5-undo/tests/undocommand.js
@@ -347,5 +347,13 @@ describe( 'UndoCommand', () => {
expect( getCaseText( root ) ).to.equal( 'adbcef' );
expect( editor.model.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true;
} );
+
+ it( 'should clear stack on DataController set()', () => {
+ const spy = sinon.stub( undo, 'clearStack' );
+
+ editor.setData( 'foo' );
+
+ sinon.assert.called( spy );
+ } );
} );
} );