Dynamic Data Binding Based on JSX

Keywords: Javascript React Attribute Webpack

Dynamic Data Binding Based on JSX Belonging to the author React and Front End Engineering Practice References to the design in this article React Learning and Practice Data Index If you have any questions about the basic JavaScript syntax, you can refer to Modern JavaScript Development: Grammar Basis and Practical Skills.

Dynamic Data Binding Based on JSX

The author is in the 2016-My Front-End Road: Tooling and Engineering As mentioned in the article, the front-end community has spent 15 years splitting HTML, JavaScript and CSS, but with the advent of JSX, things seem to have returned overnight to pre-liberation. In MVVM front-end frameworks such as Angular and Vue.js, instructions are used to describe business logic, while JSX is essentially JavaScript, that is, JavaScript is used to describe business logic. Although JSX has been criticized as an ugly grammar by some developers, the author still adheres to the principle of JavaScript First and uses JavaScript to write business code as much as possible. In the previous article React: JSX Explanation In this section, we start to write a simple DOM-oriented JSX parsing and dynamic data binding library; the code in this section is summarized in Ueact Library.

JSX parsing and DOM element construction

Element Construction

The author is in the JavaScript Syntax Tree and Code Conversion Practice Babel is still used as a JSX parsing tool. In order to convert JSX declarations into createElement calls, the following configuration is needed in the project's. babelrc file:

  "plugins": [
    "transform-decorators-legacy",
    "async-to-promises",
    [
      "transform-react-jsx", {
        "pragma": "createElement"
      }
    ]
  ],

There createElement function The statement is as follows:

/**
 * Description Building Virtual DOM from JSX
 * @param tagName Label name
 * @param props attribute
 * @param childrenArgs List of child elements
 */
export function createElement(
  tagName: string,
  props: propsType,
  ...childrenArgs: [any]
) {}

This function contains three parameters, which specify the tag name, attribute object and list of sub-elements. In fact, after Babel's transformation, JSX text will become the following function calls (including other ES2015 syntax transformations):

...
  (0, _createElement.createElement)(
    'section',
    null,
    (0, _createElement.createElement)(
      'section',
      null,
      (0, _createElement.createElement)(
        'button',
        { className: 'link', onClick: handleClick },
        'Custom DOM JSX'
      ),
      (0, _createElement.createElement)('input', {
        type: 'text',
        onChange: function onChange(e) {
          console.log(e);
        }
      })
    )
  ),
...

After getting the element label, the first thing we need to do is create the element; create the element createElementByTag In the process, we need to pay attention to distinguishing ordinary elements from SVG elements.

export const createElementByTag = (tagName: string) => {
  if (isSVG(tagName)) {
    return document.createElementNS('http://www.w3.org/2000/svg', tagName);
  }
  return document.createElement(tagName);
};

Attribute processing

After creating a new element object, we need to deal with the subsequent parameters passed in by the createElement function, that is, to set the corresponding attributes for the element; the basic attributes include style class, in-line style, label attributes, events, sub-elements and plain HTML code. First we need to deal with the sub-elements:

// Processing all child elements, if the child element is a simple string, the text node is created directly
const children = flatten(childrenArgs).map(child => {
  // If the child element is also Element, a copy of the child element is created
  if (child instanceof HTMLElement) {
    return child;
  }

  if (typeof child === 'boolean' || child === null) {
    child = '';
  }

  return document.createTextNode(child);
});

As you can see here, the execution of the createElement function is bottom-up, so the incoming child element parameters are actually rendered HTML elements. Next we need to deal with other attributes:

...
// Support class and className settings at the same time
const className = props.class || props.className;

// If there are style classes, set
if (className) {
  setAttribute(tagName, el, 'class', classNames(className));
}

// Parsing in-line styles
getStyleProps(props).forEach(prop => {
  el.style.setProperty(prop.name, prop.value);
});

// Parsing other HTML attributes
getHTMLProps(props).forEach(prop => {
  setAttribute(tagName, el, prop.name, prop.value);
});

// Set up event listener. To solve the problem of asynchrony in some browsers, synchronous writing is adopted.
let events = getEventListeners(props);

for (let event of events) {
  el[event.name] = event.listener;
}
...

React also allows you to directly set the internal HTML code of the element, where we also need to determine whether there is a dangerouslySetInnerHTML attribute:

// If HTML is set manually, add HTML, otherwise set display sub-elements
if (setHTML && setHTML.__html) {
  el.innerHTML = setHTML.__html;
} else {
  children.forEach(child => {
    el.appendChild(child);
  });
}

At this point, we have completed the simple DOM tag transformation for JSX format of the createElement function, complete source code reference. Here.

Simple use

Here we still use it. create-webpack-app Scaffolding is used to build a sample project. Here we describe the use of a simple counter as an example. It should be noted that this section has not yet introduced bidirectional data binding, or automatic state change update, or the simple DOM selector query update method used:

// App.js
import { createElement } from '../../../src/dom/jsx/createElement';

// Intra-page status
const state = {
  count: 0
};

/**
 * Description Click Event Processing
 * @param e
 */
const handleClick = e => {
  state.count++;
  document.querySelector('#count').innerText = state.count;
};

export default (
  <div className="header">
    <section>
      <section>
        <button className="link" onClick={handleClick}>
          Custom DOM JSX
        </button>
        <input type="text"
          onChange={(e)=>{
            console.log(e);
          }}
        />
      </section>
    </section>
    <svg>
      <circle cx="64" cy="64" r="64" style="fill: #00ccff;" />
    </svg>
    <br />
    <span id="count" style={{ color: 'red' }}>
      {state.count}
    </span>
  </div>
);

// client.js
// @flow

import App from './component/Count';

document.querySelector('#root').appendChild(App);

Data binding

When we use Webpack to compile JSX in the back end, it will be directly converted into function calls in JavaScript, so we can naturally declare variables in scope and refer to them directly in JSX. However, when designing Ueact, the author considers that in order to facilitate quick start or simple H5 page development or upgrade of existing code base, we still need to support runtime action. In this section, we discuss how to write HTML templates in JSX format and dynamically bind data. In this section, our HTML template is the JSX code used above, but we also need to introduce babel-standalone and Ueact's umd schema library:

Then in the script tag on this page, we can render the template and bind the data:

<script>
  var ele = document.querySelector("#inline-jsx");

  Ueact.observeDOM(
    ele,
    {
      state: {
        count: 0,
        delta: 1,
        items: [1, 2, 3]
      },
      methods: {
        handleClick: function () {
          this.state.count+=this.state.delta;
          this.state.items.push(this.state.count);
        },
        handleChange:function (e) {
          let value = parseInt(e.target.value);
          if(!Number.isNaN(value)){
            this.state.delta = value;
          }
        }
      },
      hooks: {
        mounted: function () {
          console.log('mounted');
        },
        updated:function () {
          console.log('updated');
        }
      }
    },
    Babel
  );
</script>

Here we call the Ueact.observeDOM function to render the template. This function takes the outerHTML attribute of the specified element and compiles it through the Babel dynamic plug-in:

    let input = html2JSX(ele.outerHTML);

    let output = Babel.transform(input, {
      presets: ['es2015'],
      plugins: [
        [
          'transform-react-jsx',
          {
            pragma: 'Ueact.createElement'
          }
        ]
      ]
    }).code;

It is worth mentioning that since there are some differences between HTML and JSX grammars, we need to modify some element grammars after we get the DOM objects rendered. It mainly includes the following three scenarios:

  • Self-closing label processing, i.e. < input >=> < input />.

  • Remove the quotation marks for event listening in the input HTML, that is, onclick="{methods. handleClick}"=> onclick={methods. handleClick}

  • Remove the extra quotation marks for the value value, that is, value="{state.a}"=> value={state.a}

Here we have the Function call code transformed by Babel. Next we need to execute this part of code and complete data filling. The simplest way is to use the eval Function, but because it is directly exposed to the global scope, it is not recommended to use; we use the way of dynamically constructing the Function to invoke:

/**
 * Description Complete the construction from the input JSX function string
 * @param innerContext
 */
function renderFromStr(innerContext) {
  let func = new Function(
    'innerContext',
    `
     let { state, methods, hooks } = innerContext;
     let ele = ${innerContext.rawJSX}
     return ele;
    `
  ).bind(innerContext);

  // Building new nodes
  let newEle: Element = func(innerContext);

  // Replace itself with the parent node of the specified element
  innerContext.root.parentNode.replaceChild(newEle, innerContext.root);

  // Delete the reference of the old node after replacing, triggering GC
  innerContext.root = newEle;
}

innerContext includes objects such as State and Methods, which we define. Here, we use the Lexical Scope of JavaScript to transfer variables. This part is a complete code reference. Here.

Change Monitoring and Rendering

The author is in the 2015 - My Front End Road: Data Flow Driven Interface In this section, we discuss how to automatically monitor state changes and complete re-rendering. Here we use the way of monitoring JavaScript object attributes to monitor state changes, using another library of the author. Observer-X Its basic use is as follows:

import { observe } from '../../dist/observer-x';

const obj = observe(
  {},
  {
    recursive: true
  }
);

obj.property = {};

obj.property.listen(changes => {
  console.log(changes);
  console.log('changes in obj');
});

obj.property.name = 1;

obj.property.arr = [];

obj.property.arr.listen(changes => {
  // console.log('changes in obj.arr');
});

// changes in the single event loop will be print out

setTimeout(() => {
  obj.property.arr.push(1);

  obj.property.arr.push(2);

  obj.property.arr.splice(0, 0, 3);
}, 500);

The core is to trigger a registered callback event when an object's attributes change (add, delete and assign values); that is:

  ...
  // Transforming the internal state into observable variables
  let state = observe(innerContext.state);
  ...
  state.listen(changes => {
    renderFromStr(innerContext);
    innerContext.hooks.updated && innerContext.hooks.updated();
  });
  ...

Complete online Demo can be viewed A Simple Counter Based on JSX and Observer-X

Posted by crochk on Fri, 07 Jun 2019 18:47:46 -0700