Developing a client side part of a plug in - czcorpus/kontext GitHub Wiki

Developing a client side part of a plug in (React, Flux etc.)

Note: the documentation is outdated

Starting from KonText version 0.11, React (along with the Flux pattern) is the only way (except for D3.js generated charts) how to build dynamic components.

The following text contains an example how a fictional plug-in integrates into a fictional page. Let's say there is an action stats along with a respective template tmpl/stats.tmpl and client-side page logic public/files/js/pages/stats.ts. We as plug-in developers cannot change what the page logic stats.ts does. The page logic is designed in a way it expects different plug-ins (some of them optional) to be initialized on the page. In other words, there is a chain of initialization steps where some of the steps are reserved for plug-ins. I.e. once we implement a plug-in properly, our code becomes a component of the page.

Now let's assume KonText's stats page supports some plugin foo. The HTML template stats.tmpl defines a mounting point for the plugin:

<section class="status">
<div id="component-hook"></div>
</section>

To implement a plug-in, we will need the following:

  • plug-in class along with some Flux stores we need to implement the functionality we want,
  • a React component KonText will insert into the stats page,
  • a .d.ts file for the React component to be able to import it to module where our Plug-in class is defined,
  • optionally some translations for messages/labels/etc. used in our component.

Plug-in class

Client-side plug-in classes must also (just like the server ones in Python) implement prescribed interfaces (see public/files/js/types/plugins.d.ts) but in many cases, these interfaces are quite simple because the plug-ins communicate mostly by:

  • receiving Flux action messages (which is not part of any interface)
  • being called by React views (mostly obtaining data via getters) to collect state; in this case the interface is between a store and a React component - both implemented within a plug-in - i.e. outside of KonText core code.
import {init as viewInit} from './view'; 

export class MyStore extends SimplePageStore {
   // ....
}

export class MyFooPlugin implements PluginInterfaces.IFoo {

    getView():React.Component {
        return this.view;
    }
}

export function init(pluginApi:Kontext.PluginApi):RSVP.Promise<ISomeKontextWidget> {
    return new RSVP.Promise((resolve:(d)=>void, reject:(err)=>void) => {
      const store = new MyStore();
      const views = viewInit(pluginApi.dispatcher(), pluginApi.getComponentHelper(), store);
      resolve(new MyFooPlugin(pluginApi, store, views.MyComponent));
    });
}

We define a React component (myViews.jsx):

import * as React from 'vendor/react';

export function init(dispatcher, helpers, someStore) {       

  class MyComponent extends React.Component {
    constructor(props) {
      super(props);
      this._changeHandler = this._changeHandler.bind(this);
      this._handleClick = this._handleClick.bind(this);
      this.state = {
        someItem = someStore.getSomeItem()
      };
    }

    _changeHandler() {
      this.setState(someStore.getData());
    }
    
    _handleClick() {
      dispatcher.dispatch({
        actionType: 'RANDOM_CORPUS_REQUEST',
        props: {}
      }
    }
     
    componentDidMount() {
      someStore.addChangeListener(this._changeHandler);
    }
    
    componentWillUnmount() {
      someStore.removeChangeListener(this._changeHandler);
    }
    
    render() {
      return (
        <div>
          <h3>{this.props.title}</h3>
          <p><a onClick={this._handleClick}>Get random corpus</a></p>
          <p>{this.state.corpus}</p>
        </div>
      );
    }
  }

  return {
    MyComponent: MyComponent
  };
}   

Then we must define myViews.d.ts to be able to import our JSX file to TypeScript plug-in file.

export function init(dispatcher, helpers, someStore):{MyComponent:React.Component}

Now the core page logic (which is out of our control as plug-in developers) knows that it can do something like this:

import initSomePlugin from 'plugins/foo/init';

class ViewPageLogic {
    init():void {
        doSomeStuffBefore().then(
            () => {
                return initSomePlugin(this.layoutModel.pluginApi());
            }
        ).then(
            (somePlugin) => {
                this.layoutModel.renderReactComponent(
                    somePlugin.getView(),
                    document.getElementById('component-hook'),
                    { /* some properties */ }
                );    
            }
        ).then(
            () => { /* do stuff after */ }
        ).catch(
            (err) => {
                this.layoutModel.showMessage('error', err);
            }
        );
    }
}
⚠️ **GitHub.com Fallback** ⚠️