Contribute to this guide

guideData 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.

Current exchange rate fetched from Binance (updates every 10 seconds):

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 { ClassicEditor } from '@ckeditor/ckeditor5-editor-classic';
import { Bold, Italic } from '@ckeditor/ckeditor5-basic-styles';
import { Command, Plugin } from '@ckeditor/ckeditor5-core';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { List } from '@ckeditor/ckeditor5-list';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { ButtonView } from '@ckeditor/ckeditor5-ui/src';
import { Widget, toWidget } from '@ckeditor/ckeditor5-widget';

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:&nbsp;<span data-resource-url="https://api2.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT"></span></p>
</div>