preface

In the last article, our Topology supported rendering UI components, so in this article, I’m going to walk you through the events that interact with the diagram component.

An overview of

Instead of assigning values directly, we do this indirectly through the Store. This way, our logic is clear, and the centralized management of the data makes it easier to debug the code.

Define the configuration of production components

What is a production component. As the name implies, it is simply a component that provides data support for other components. Such as input, timePicker, etc., which tend to provide the source data for the query button. So what do we need to configure in order to do this in Topology?

    {
      text: 'Input field text'.icon: 'icon-diamond'.name: 'input'.data: {
        rect: {
          x: 100.y: 200.width: 200.height: 50
        },
        name: 'input'.data: {
          props: {
            allowClear: true // Props for the component}},events: [ // Event binding
          {
            type: 'doFn'.action: 'Function'.value: `let fun = (a) =>  console.log(params + a); fun(123);`.params: 123.name: 'onChange'}}}]Copy the code

The figure above shows the configuration items corresponding to the INPUT production component. We did this by adding the event configuration in Events. For example, if we want to enter some strings in the input field and then click a button (the consumption component), an asynchronous request will be made to the background with the parameter values from the input field. Then we need to listen for the onChange event and constantly update the value in the Store so that the consumer component gets the latest value from the Store.

For the definition of events in event, please refer to the definition in the source section, which will not be described in this article.

  events: { type: EventType; action: EventAction; value: string; params: string; name? : string }[] = []; private eventFns: string[] = ['link'.'doStartAnimate'.'doFn'.'doWindowFn'.' '.'doPauseAnimate'.'doStopAnimate'];
  enum EventType {
  Click,
  DblClick,
  WebSocket,
  Mqtt,
}

enum EventAction {
  Link,
  StartAnimate,
  Function,
  WindowFn,
  SetProps,
  PauseAnimate,
  StopAnimate,
}
Copy the code

Once we have configured the consumption component, we need to handle the distribution of events and values in the plug-in.

Event handling

Event processing is divided into two parts: 1. Processing custom functions 2. Distribution of production component values

First we’ll define eventProps to store synthesized events in React. Some wrapping of events (parameter passing and value distribution) is done by traversing the Events array.

Let _fn = new Function(‘params’, element.value); The original function is wrapped in a layer to support the passing of parameters. If a subsequent call involving the interface returns parameters, we can also modify it based on the current code.

Because different components have different shapes without method callbacks, we need to treat some special components separately. For example, datePicker’s onChange value is moment, so we need to parse it and so on. We then call the store.set method to distribute the component’s value to the Store. Finally, deconstruct the eventProps to the React component.

  let eventProps = {};
  if (node.events.length > 0) {
    node.events.forEach((element) = > {
      let _fn = new Function('params', element.value);
      eventProps[element.name] = async (componentValue) => {

        /** * Takes special care of the callback functions of different components, passing the component's callback value into the Store for use by other components. */

        if (['datePicker'].includes(node.name)) {
          Store.set(`componentValue-${node.id}`, moment(componentValue).format('YYYY-MM-DD'));
        } else if (['input'].includes(node.name)) {
          Store.set(`componentValue-${node.id}`, componentValue.target.value);
          node.data.props.value = componentValue.target.value;
        } else {
          Store.set(`componentValue-${node.id}`, componentValue);
        }

        await _fn(element.params);
      };
    });
  }

  reactNodesData[node.id].component = ReactDOM.render(
    <ReactComponent {. node.data.props} {. eventProps} / >,
    reactNodesData[node.id].div
  );
Copy the code

Le5le-store supports the following operations:

import { Store } from 'le5le-store';

Store.set('name'.'topology');
Store.get('name');

// Monitor changes in real time
const subcribe = Store.subcribe('name'.value= > {
  console.log('name:', value);
});
// Unsubscribe (listening)
subcribe.unsubcribe();


Store.set('obj', { str: 'abc'.num: 1.arr: ['aaa'.111].children: { key: 123}}); Store.get('obj.num'); / / = = 1

Store.get('obj').num = 100;
// Notify obj.num of the change and trigger the subscription callback
Store.updated('obj.num');
Copy the code

Just enough, so we don’t need to introduce other third-party data flow frameworks. Once our production component has finished distributing the value, its job is done, and then it’s up to the consumer component.

Define the configuration of the transit hub components

The hub component is meant to take the value I want from the Store, but it is also responsible for asynchronously requesting the data and processing the returned value for the presentation component to use.

The following is the configuration of the transit hub component

    {
      text: 'button'.icon: 'icon-rectangle'.name: 'button'.data: {
        autoRect: true.strokeStyle: '#fff'.rect: {
          x: 100.y: 200.width: 100.height: 50
        },
        name: 'button'.data: {
          // Component property configuration
          props: {
            type: 'primary'.children: 'query'
          },
          // Asynchronous request configuration
          http: {
            api: '/api/topologies? '.type: 'get'.paramsGetStyle: 'subscribe'.paramsArr: []},// Bind as chart legend ID
          bind: []},events: [{type: 'doFn'.action: 'Function'.value: `let fun = (a) => params + a; fun(123); return 1231; `.params: 123.name: 'onClick'}}}]Copy the code

In addition to the production component, we add HTTP as a configuration item

   http: {
        api: '/api/topologies? '.type: 'get'.paramsArr: []}Copy the code
  • The API represents the address of the back end
  • Type The type requested by the code
  • ParamsArr refers to the query condition

Let’s focus on paramsArr.

Each parameter in paramsArr contains: 1. ParamsKey 2. ParamsValue

ParamsKey is the query key value in the URL, and paramsValue is the value. We can get the value from the Store.

const renderForm = () = > {
  getFieldDecorator('keys', { initialValue: []});const keys = getFieldValue('keys');
  return keys.map((item, idx) = > (
    <div key={idx}>
      <Col span={24}>
        <Form.Item label={` ${idx + 1Parameter name}key`} >
          {getFieldDecorator(`paramsKey[${idx}]`, {
            initialValue: void 0
          })(<Input style={{ width: 180 }} placeholder="Please fill in your key" />)}
        </Form.Item>
      </Col>

      <Col span={24}>
        <Form.Item label={` ${idx + 1} parametervalue`} >
          {getFieldDecorator(`paramsValue[${idx}]`, {
            initialValue: void 0
          })(
            <Select style={{ width: 180 }} placeholder="Please select the source data to bind">
              {canvas.data.pens.map((item) => (
                <Select.Option key={item.id} value={`componentValue-The ${item.id} `} >
                  {item.id}
                </Select.Option>
              ))}
            </Select>
          )}
        </Form.Item>
      </Col>
    </div>
  ));
};
Copy the code

Since the rule that defines a key in a Store is componentValue- + the ID of the component. So when we select paramsValue, we can pull the data directly from pens.

After the configuration is complete, we still need to handle the logic related to the hub component in the plug-in.

  const { api, type, paramsArr } = node.data.http;
  const queryData = {};
  paramsArr.forEach((item) = > {
    queryData[item.key] = Store.get(item.value);
  });
  /** * the asynchronous request is called here and the obtained data is stuffed into store */
  const data = await Axios[type](`${api}${querystring.stringify(queryData)}`);
  Store.set(`http-${node.id}`, data);
Copy the code

We convert paramsArr into a template that can be serialized by QueryString.

The QueryString module in Node provides utilities for parsing and formatting URL query strings.

Now that we have the data, should we render it?

So the next task for the pivot component is to render the diagram (display the component).

So the question is, is the presentation component going to subscribe to the data source? Or is the central component directly bound to the display component.

I choose the latter. My idea is to try to centralize the operations and not spread them out too much.

So there’s another configuration in the backbone component definition: bind,

By binding the id of the chart, we control it one by one based on the value of BIND when we initiate a query.

if (node.data.bind && node.data.bind.length > 0) {
      node.data.bind.forEach((b) = > {
        let _pen = canvas.data.pens.find((pen) = > pen.id === b);
        let idx = canvas.data.pens.findIndex((pen) = > pen.id === b);
        if (_pen.data.echarts) {
          canvas.data.pens[idx].elementRendered = false;
          const { seriesFunction } = canvas.data.pens[idx].data.echarts.option;
          let _seriesFn = new Function('params', seriesFunction);
          canvas.data.pens[idx].data.echarts.option = _seriesFn(data)
        } else {
          // Ignore line nodes for now
          if (canvas.data.pens[idx] instanceof Line) {
            return;
          }
          // Normal nodes can be processed later}}); }Copy the code

In order to make developers more flexible, we have made echarts options into a callback function. Developers can handle certain options based on the value returned in the background.

Define the configuration of the consuming component

In the previous section, we mentioned the term consumption component, which is simply a presentation component. For example, the most common ECharts chart component.

   {
     elementRendered: false.name: 'Line chart'.icon: 'icon-line-chart'.data: {
       text: 'Line chart'.rect: {
         width: 300.height: 200
       },
       name: 'echarts'.data: {
         echarts: {
           option: {
             xAxis: {
               type: 'category'.data: ['Mon'.'Tue'.'Wed'.'Thu'.'Fri'.'Sat'.'Sun']},yAxis: {
               type: 'value'
             },
             series: [{data: [820.932.901.934.1290.1330.1320].type: 'line'}].seriesFunction: ` return { // .... Series: [{name: 'direct access ', type: 'bar', barWidth: '60%', data: params.map(item => XXX)}]}'}}}}},Copy the code

SeriesFunction can be configured to be overwritten based on the value of params.

Write in the last

Our final result looks like this:

What we are going to do next is to optimize each item carefully. If you are interested, please check github