Data from an external source
The editor below contains a widget that fetches data from an external source and updates all own instances in a set interval of time. In this particular example, the widget shows the current Bitcoin rate.
# Detailed guide
If you would like to create such a widget on your own, take a look at the dedicated tutorial which shows how to achieve this step by step with the source code provided.
# Editor example configuration
View editor configuration script
import { Plugin } from '@ckeditor/ckeditor5-core';
import { ButtonView } from '@ckeditor/ckeditor5-ui/src';
import Command from '@ckeditor/ckeditor5-core/src/command';
import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import List from '@ckeditor/ckeditor5-list/src/list';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
const BitcoinLogoIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" preserveAspectRatio="xMidYMid" viewBox="0 0 1 1"><path d="M63.036 39.741c-4.274 17.143-21.637 27.576-38.782 23.301C7.116 58.768-3.317 41.404.959 24.262 5.23 7.117 22.594-3.317 39.734.957c17.144 4.274 27.576 21.64 23.302 38.784z" style="fill:#f7931a" transform="scale(.01563)"/><path d="M46.1 27.441c.638-4.258-2.604-6.547-7.037-8.074l1.438-5.768-3.511-.875-1.4 5.616c-.923-.23-1.871-.447-2.813-.662l1.41-5.653-3.51-.875-1.438 5.766c-.764-.174-1.514-.346-2.242-.527l.004-.018-4.842-1.209-.934 3.75s2.605.597 2.55.634c1.422.355 1.679 1.296 1.636 2.042l-3.94 15.801c-.174.432-.615 1.08-1.61.834.036.051-2.551-.637-2.551-.637l-1.743 4.019 4.569 1.139c.85.213 1.683.436 2.503.646l-1.453 5.834 3.507.875 1.439-5.772c.958.26 1.888.5 2.798.726l-1.434 5.745 3.51.875 1.454-5.823c5.987 1.133 10.489.676 12.384-4.739 1.527-4.36-.076-6.875-3.226-8.515 2.294-.529 4.022-2.038 4.483-5.155zM38.08 38.69c-1.085 4.36-8.426 2.003-10.806 1.412l1.928-7.729c2.38.594 10.012 1.77 8.878 6.317zm1.086-11.312c-.99 3.966-7.1 1.951-9.082 1.457l1.748-7.01c1.982.494 8.365 1.416 7.334 5.553z" style="fill:#fff" transform="scale(.01563)"/></svg>';
const RESOURCE_URL = 'https://api2.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT';
class ExternalDataWidgetCommand extends Command {
execute() {
const editor = this.editor;
const selection = editor.model.document.selection;
editor.model.change( writer => {
const externalWidget = writer.createElement(
'externalElement', {
...Object.fromEntries( selection.getAttributes() ),
'data-resource-url': RESOURCE_URL
}
);
editor.model.insertObject( externalWidget, null, null, {
setSelection: 'on'
} );
} );
}
refresh() {
const model = this.editor.model;
const selection = model.document.selection;
const isAllowed = model.schema.checkChild( selection.focus.parent, 'externalElement' );
this.isEnabled = isAllowed;
}
}
class ExternalDataWidget extends Plugin {
static get requires() {
return [ ExternalDataWidgetEditing, ExternalDataWidgetUI ];
}
}
class ExternalDataWidgetUI extends Plugin {
init() {
const editor = this.editor;
const externalWidgetCommand = editor.commands.get( 'external' );
editor.ui.componentFactory.add( 'external', locale => {
const button = new ButtonView( locale );
button.set( {
label: 'Bitcoin rate',
tooltip: true,
withText: false,
icon: BitcoinLogoIcon
} );
button.bind( 'isEnabled' ).to( externalWidgetCommand );
button.on( 'execute', () => {
editor.execute( 'external' );
editor.editing.view.focus();
} );
return button;
} );
}
}
class ExternalDataWidgetEditing extends Plugin {
constructor( editor ) {
super( editor );
this.intervalId = this._intervalFetch();
this.externalDataValue = '';
}
static get requires() {
return [ Widget ];
}
destroy() {
clearInterval( this.intervalId );
}
init() {
this._defineSchema();
this._defineConverters();
this._updateWidgetData();
this.editor.commands.add( 'external', new ExternalDataWidgetCommand( this.editor ) );
}
_intervalFetch() {
return setInterval( () => this._updateWidgetData(), 10000 ); // set time interval to 10s
}
async _updateWidgetData( externalUrl = RESOURCE_URL ) {
try {
const response = await fetch( externalUrl );
const data = await response.json();
const updateTime = new Date( data.closeTime );
const parsedData = '$' + Number( data.lastPrice ).toFixed( 2 ) + ' - ' + updateTime.toLocaleString();
this.externalDataValue = parsedData;
const rootElement = this.editor.model.document.getRoot();
for ( const { item } of this.editor.model.createRangeIn( rootElement ) ) {
if ( item.is( 'element', 'externalElement' ) ) {
this.editor.editing.reconvertItem( item );
}
}
} catch ( error ) {
console.error( error );
}
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register( 'externalElement', {
inheritAllFrom: '$inlineObject',
allowAttributes: [ 'data-resource-url' ]
} );
}
_defineConverters() {
const editor = this.editor;
editor.conversion.for( 'upcast' ).elementToElement( {
view: {
name: 'span',
attributes: [ 'data-resource-url' ]
},
model: ( viewElement, { writer } ) => {
const externalUrl = viewElement.getAttribute( 'data-resource-url' );
return writer.createElement( 'externalElement', {
'data-resource-url': externalUrl
} );
}
} );
editor.conversion.for( 'dataDowncast' ).elementToElement( {
model: 'externalElement',
view: ( modelElement, { writer } ) => {
return writer.createEmptyElement( 'span', {
'data-resource-url': modelElement.getAttribute( 'data-resource-url' )
} );
}
} );
editor.conversion.for( 'editingDowncast' ).elementToElement( {
model: 'externalElement',
view: ( modelElement, { writer } ) => {
const externalValueToShow = this.externalDataValue;
const externalDataPreviewElement = writer.createRawElement( 'span', null, function( domElement ) {
domElement.classList.add( 'external-data-widget' );
domElement.textContent = externalValueToShow || 'Fetching data...';
if ( externalValueToShow ) {
domElement.classList.add( 'external-data-widget-bounce' );
setTimeout( () => domElement.classList.remove( 'external-data-widget-bounce' ), 1100 );
}
} );
const externalWidgetContainer = writer.createContainerElement( 'span', null, externalDataPreviewElement );
return toWidget( externalWidgetContainer, writer, {
label: 'External widget'
} );
}
} );
}
}
ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, ExternalDataWidget ],
toolbar: [ 'undo', 'redo', '|', 'external', '|', 'heading', '|', 'bold', 'italic', '|', 'numberedList', 'bulletedList' ]
} )
.then( editor => {
console.log( 'Editor was initialized', editor );
// Expose for playing in the console.
window.editor = editor;
} )
.catch( error => {
console.error( error.stack );
} );
View editor content listing
<style>
.external-data-widget {
border: 2px solid rgb(242, 169, 0);
padding: 0 2px;
}
.external-data-widget-bounce {
animation: external-data-widget-bounce-animation 1.5s 1;
}
@keyframes external-data-widget-bounce-animation {
0% {
box-shadow: 0px 0px 0px 0px rgba(242, 169, 0, 1);
}
100% {
box-shadow: 0px 0px 0px 10px rgba(242, 169, 0, 0);
}
}
</style>
<div id="snippet-external-data-widget">
<h1>This is a simple widget for fetching data from an external source:</h1>
<p>Bitcoin rate: <span data-resource-url="https://api2.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT"></span></p>
</div>
Every day, we work hard to keep our documentation complete. Have you spotted outdated information? Is something missing? Please report it via our issue tracker.