0.1.5 โ€ข Published 7 years ago

react-html-connect v0.1.5

Weekly downloads
-
License
ISC
Repository
github
Last release
7 years ago

๐Ÿ”Œ React HTML Connect ยท npm.io npm.io

A JavaScript function that eases integratation of React (or Preact) into existing server-side templating.

Works with:

Package State

This package is still considered experimental. This means that it works as intended, however features may still be change or be removed in radically in future versions. It is currently used in-house by the team at OpenUp, however feel free to try it out and provide feedback. If it addresses a use-case that is important to you or you come across any bugs, please let me know at schalk@openup.org.za.

Usage

This package is intended to be used as an import into a NodeJS module resolution library like Webpack.

However it is compiled in accordance with the UMD JavaScript specification. This means that it can also be imported directly into the browser via a <script> tag from the following URL:

<script src="http://unpkg.com/react-html-connect" async></script>

Getting Started

1. Make sure that you have the latest version of NodeJS installed:
2. Install the package along with React and React DOM:
npm install --save react react-dom react-html-connect
3. Create your server-side template:
// Users.php

<body>
  <ul data-component="Users">
    <?php foreach ($user_array as $user) { ?>
      <li data-users <?php $user[active] ? echo "data-active" : null ?>
        <span data-name><?php $user[name] ?></span>
      </li>
      >
    <?php } ?>
  </ul>
</body>


<script src="scripts.min.js"></script>
4. Create your React component
// Users.jsx


import React from 'react';


export default class Users extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showAll: false,
    }
  }

  toggleShowAll() {
    this.setState({ showAll: !this.state.showAll })
  }

  render() {
    return (
      <ul>
        {
          users.map((user) => {
            if (!this.state.showAll && !active) {
              return null;
            }

            return (
              <li>
                <span>{name}</span>
              </li>
            )
          }
        }
      </ul>
      <button onClick={this.toggleShowAll}>
        {this.state.showAll ? 'Hide inactive' : 'Show inactive'}
      </button>
    )
  }
}
4. Connect React component to 'data-component' attribute:
// scripts.jsx


import React from 'react';
import render from 'react-dom';
import connect from 'react-html-connect';


connect(
  render, 
  <Users />, 
  { users: [{ name: 'innerHTML', active: 'boolean' }]}
);

API

Primary

connect(render, component, props, additional options)

  • createElement <function>: This needs to be the React (or React-like) createElement method (for example: React.createElement). By passing this manually you can control what version of React you want to use, and enables compatibility with React-like libraries such as Preact (for example Preact.h).
  • render <function>: This needs to be a React (or React-like) render method (for example: ReactDOM.render. By passing this manually you can control what version of React you want to use, and enables compatibility with React-like libraries such as Preact (for example Preact.h).
  • component <React Component>: The React (or React-like) component you want to bind to a specific area in your template (via the data-component attributes). Converts the name of the component to a string and searches for a data-component attribute that contains the string (for example: data-component="ExampleComponent"). Note that the name in the component data attribute is case sensitive.
  • query <Object> | <function> (optional, default: null): Optional parameter that instructs the connect function on how to collect and pass values from templates into the component vai props. The value passed to query uses custom schema, created specifically for this package (learn more at Passing props to components). Alternatively, query also accepts a function for more control over props passed.
  • options <Object>: (optional, default: null): Optional parameter that accepts an object of key/value pairs that sets specific rules/conditions. See the next section for all valid values that can be passed inside options:

Additional Options

  • scope <HTMLelement> (optional, default: window.document): A property that restricts the connect function's searchable range to a specifc DOM node and its children. Useful to avoid conflicting attribute names used elsewhere in the template.
  • attribute <string> (optional, default: 'data-component'): A property that changes the name of the attribute used to bind components to your template. Useful when data-component is already in use elsewhere.

Examples

Below area a couple of examples to illustrate the connect function can be used:

Basic Examples

Query examples

Basic Examples

Basic usage:

Let's say that we have following the basic file structure:

.
โ”œโ”€โ”€ index.html
โ”œโ”€โ”€ Example.jsx
โ””โ”€โ”€ scripts.jsx

The examples below work under the assumption that index.html is a server-side templating file. (It might just as easily be something like contact-widget.php). It also assumed that scripts.jsx will be the file that gets compiled into your public facing JavaScript file (for example scripts.min.js) and then imported into your template.

// index.html


<body>
  <div data-component="Example"></div>
</body>
// Example.jsx


import React from 'react';


export default function Example({ name = 'unknown user' }) {
  return <span>Hello {name}</span>;
}
// scripts.jsx


import Example from './Example.jsx';
import { render } from 'react-dom';
import connect from 'react-html-connect'


connect(render, <Test />);

The first parameter takes a render function. This will either be the render function from React DOM or Preact. Passing the render method allows you explicitly control what specific library (or version of that library) you want to use.

The second parameter takes the component itself. The name of the component will be converted into a string that is used to match the component to a specific data-component attribute. Note that the string is case-sensitive.

The examples above will output the following HTML:

<body>
  <div data-component="Test">
    <span>Hello uknown user</span>
  </div>
</body>

Passing props to components:

In addition you can pass instructions as an object to the query parameter inside the function. These instructions indicate what values from the HTML to pass to the component.

This instruction object uses a custom schema loosely inspired by GraphQl. See the 'Query Schema' heading below for full instructions on writing a query.

However, for this specific example you should know only that name of the key in the object indicates the name of the data attribute on the HTML node itself ('data-name' in this case). While the value indicates how the value of this attribute should be parsed. The following example will generate <span>Hello John Smith</span>:

// index.html


<body>
  <div data-component="Example" data-name="John Smith"></div>
</body>
// scripts.jsx


import Test from './Test.jsx';
import { render } from 'react-dom';
import connect from 'react-html-connect'


connect(render, <Example />, { name: 'string' });

Initialising multiple components:

When looking at the example above it becomes clear how instances of React components can be repeated to generate different outputs based on server-side values:

// index.html

<body>
  <div data-component="Example"></div>
  <div data-component="Example" data-name="John Smith"></div>
  <div data-component="Example" data-name="Jane Doe"></div>
  <div data-component="Example" data-name="Billy Johnson"></div>
  <div data-component="Example" data-name="Sarah Jackson"></div>
</body>
// scripts.jsx

import Example from './Example.jsx';
import { render } from 'react-dom';
import connect from 'react-html-connect'


connect(render, <Example />, { name: 'string' });

Overriding the query schema.

You can override the query parameter by manually passing a function. This allows you the flexibility to edit or customise props passed to the component. The following example will generate <span>Hello Mr. John Smith</span>:

// scripts.jsx

import Example from './Example.jsx';
import { render } from 'react-dom';
import connect from 'react-html-connect'


const title = 'Mr. '

connect(
  render, 
  <Example />, 
  (node) => {
    const rawName = node.getAttribute('data-name');
    return { name: title + rawName };
  },
);

Using the nodeQuery method independantly.

Underlying the connect function's query parameter is a function called nodeQuery that creates the props object from the query instructions.

Note that this function can be destructed from the package and used independantly. This is very useful when combining the ease of query parameter object with the flexibility of passing a function. The above example can also be written as follows:

// scripts.jsx

import Example from './Example.jsx';
import { render } from 'react-dom';
import { nodeQuery }, connect from 'react-html-connect'


const title = 'Mr. '

connect(
  render, 
  <Example />, 
  (node) => {
    const rawName = nodeQuery(node, { name: 'string' });
    return { name: title + rawName };
  },
);

The API is as folows: nodeQuery(query, node)

  • query <Object>
  • scope <HTMLelement>

Scoping method to specific DOM node.

By default the connect function searches the entire DOM via the window.document tree.

However you can narrow the scope to a specific DOM node by passing that node as inside the options parameter. This is usually encouraged to improve performance or to mitigate conflicting attribute names:

// index.html

<body>
  <div id="initialise">
    <div data-component="Example"></div>
  </div>
  <div id="ignore">
    <div data-component="Example"></div>
  </div>
</body>
// index.jsx


import Example from './Example.jsx';
import { render } from 'react-dom';
import connect from 'react-html-helpers'


connect(
  render, 
  <Example />, 
  {}, 
  {
    scope: document.getElementById('initialise')
  }
);

Examples involving the query schema

Basic query

// HTML


<body>
  <div data-age="30">
</body>
// index.js


import { nodeQuery } from 'react-html-connect'


console.log(nodeQuery({ age: 'number' }))
// console output


{ age: 30 }

You will see that key in the query object is used to find the data-value HTML node. Since all custom attributes need to have data- prefixed to them, you do not need to repeat data- in the key itself. Once the attribute is located the value inside the query object determines how to parse its contents. In this example we passed 'number', which means that it is converted to a number via parseFloat().

Valid parse commands are as follows:

  • 'string' returns the attribute value as a string.
  • 'number' returns the attribute value as a number (can include decimals).
  • 'boolean' returns true or false, depending whether the attribute exists.
  • 'json' returns the attribute value as a JavaScript object (automatically decodes HTML entities).
  • 'innerHTML' returns the innerHTML of the node that the attribute is attached to.
  • 'outerHTML' returns the outerHTML of the node that the attribute is attached to.
  • null return the node itself.

Lastly, you can also pass a function as a value. This function will then use the HTML node itself as its first parameter. For example the following will return 600:

node => parseInt(node.getAttribute('data-value')) * 2

This means that in the example below the query will return the following:

// HTML


<body>
  <div data-age="30" data-name="John Smith" data-male data-family='{ "brother": "Billy Johnson", "sister": "Jane Doe" }'>Hello</div>
</body>
// index.js


import { nodeQuery } from 'react-html-connect'


const query = {
  age: 'number',
  name: 'string',
  male: 'boolean',
  greeting: 'innerHTML'
  family: 'json'
};


console.log(nodeQuery(query))
// console output


{
  age: 30,
  name: 'John Smith',
  male: true,
  greeting: 'Hello',
  family: { brother: 'Billy Johnson', sister: 'Jane Doe'}
}

Note that true to the JSON.parse() method, 'json' is also able to parse an array:

// HTML


<body>
  <div data-family='["Billy Johnson", "Jane Doe"]'></div>
</body>
// index.js


import { nodeQuery } from 'react-html-connect'


console.log(nodeQuery({ family: 'json' }))
// console output


{ family: ['Billy Johnson', 'Jane Doe'] }

In addition, you will notice that the values of family, in both examples, are encapsulated in single quotes. This is generally not regarded as good HTML practice since HTML attributes should be encased in double quotes. However, since JSON.parse() only accepts double quotes, the outer quotes are swapped as single quotes to not conflict with the inner JSON double quotes.

This is a quick way to get a JSON string into JavaScript. However, as mentioned this goes against the standard HTML convention and it also means that they might conflict with any single quotes inside the JSON values themselves. Fortunately, we are able to escape double quotes with HTML entities. This means that we are able to write the above as follows:

// HTML


<body>
  <div data-male data-family="{ &quot;brother&quot;: &quot;Billy Johnson&quot;, &quotsister&quot: &quotJane Doe&quot }'></div>
</body>
// index.js


import { nodeQuery } from 'react-html-connect'


console.log(nodeQuery({ family: 'json' }))
// console output


{ family: ['Billy Johnson', 'Jane Doe'] }

It's neither pretty nor readable. However we can use an online tool like Freeformatter to encode our string into HTML entities, and also decode them for debugging or editing. In addition most server-side templating have HTML entity escape functions that you can pass the string through. For example:

  • Jekyll: {{ '{ "brother": "Billy Johnson", "sister": "Jane Doe" }' | escape }}
  • Wordpress: esc_html('{ "brother": "Billy Johnson", "sister": "Jane Doe" }')

Props from child nodes

It's easy to see how the above query parameter can be used to create React component from scratch. However, going further there are cases where you might want some of the content to be rendered by the server-side templating before the JavaScript fires either for performance reasons or SEO concerns.

It is possible to render HTML server-side and then infer values from it to be used in a React component (often used to replace or enhance the original markup). Luckily the query parameter not only scans the DOM node that a component is bound to but also all of it's children:

// HTML


<body>
  <div >
	<h1 data-name>John Smith</h1>
    <p data-bio data-male><strong>John</strong> is a male. His sister is <em>Jane</em>. His brother is <em>Billy</em>. He is <span data-age>30</span> years old.</p>
  </div>
</body>
// index.js


import { nodeQuery } from 'react-html-connect'

const query = {
  name: 'innerHTML',
  age: node => parseInt(node.innerHTML),
  male: 'boolean',
  bio: 'outerHTML',
}


console.log(nodeQuery(query));
// console output


{ 
  name: 'John Smith',
  male: true,
  age: 30,
  bio: '<p data-bio data-male><strong>John</strong> is a male. His sister is <em>Jane</em>. His brother is <em>Billy</em>. He is <span data-age>30</span> years old.</p>'
}

These data attribute can be anywhere in the DOM tree since scope is set to window.document by default. This means that you need to be careful to use the same name for two different things (for example 'data-item', 'data-outer-item', data-inner-item, etc.)

Find multiple instances of attribute

We can instruct our query to collect all instances of data-people into an array by placing our query object as the first (and only) value in an array. The key of the array needs to correspond to the name of data attribute ('people' in the example below):

// index.HTML


<body>
  <div data-people>
    <h1 data-name>John Smith</h1>
    <p data-bio data-male><strong>John</strong> is a male. His sister is <em data-relatives>Jane</em>. His brother is <em data-relatives>Billy</em>. He is <span data-age>30</span> years old.</p>
  </div>
  <div data-people>
    <h1 data-name>Jane Doe</h1>
    <p data-bio><strong>Jane</strong> is a female. Her brothers are <em data-relatives>Billy</em> and <em data-relatives>John</em>. She is <span data-age>30</span> years old.</p>
  </div>
</body>
// index.js


import { nodeQuery } from 'react-html-connect'


const query = {
  people: [
    {
      name: 'innerHTML',
      age: node => parseInt(node.innerHTML)
      bio: 'outerHTML',
      relatives: ['innerHTML'],
    }
  ]
}


console.log(nodeQuery(query));
// console output


{ 
  people: [
    {
      name: 'John Smith',
      male: true,
      age: 30,
      bio: '<p data-bio data-male><strong>John</strong> is a male. His sister is <em data-relatives>Jane</em>. His brother is <em data-relatives>Billy</em>. He is <span data-age>30</span> years old.</p>'
      relatives: ['Jane', 'Billy'],
    },
    { 
      name: 'Jane Doe',
      male: false,
      age: 24,
      bio: '<p data-bio><strong>Jane</strong> is a female. Her brother is <em data-relatives>John</em>. Her other brother is <em data-relatives>Billy</em>. She is <span data-age>24</span> years old. </p>',
      relatives: ['John', 'Billy'],
    }
  ]

Advanced Example

Now that we've gone through all the above, let's end with an example highlighting all the aspects touched upon above:

// HTML

<body>
  <div data-component="Other">
    <ul>
      <li data-people>John Smith</li>
      <li data-people>Jane Doe</li>
      <li data-people>Billy Johnson</li>
      <li data-people>Sarah Jackson</li>
    </ul>
  </div>
  <div data-component="People">
    <h1 data-title>Club Members</h1>
    <div data-people data-id="001">
      <h2 data-name>John Smith</h2>
      <p data-bio data-male><strong>John</strong> is a male. His sister is <em data-relative>Jane</em>. His brother is <em data-relative>Billy</em>. He is <span data-age>30</span> years old. </p>
    </div>
    <div data-people data-id="002">
      <h2 data-name>Jane Doe</h2>
      <div><p data-bio><strong>Jane</strong> is a female. Her brother is <em data-relative>John</em>. Her other brother is <em data-relative>Billy</em>. She is <span data-age>24</span> years old. </p></div>
    </div>
    <div>Loading button...</div>
   </div>
</body>
// scripts.jsx

import People from './People.jsx';
import Other from './Other.jsx';
import { render } from 'react-dom';
import connect from 'react-html-connect'

const peopleQuery = {
  title: 'innerHTML',
  people: [
    {
      id: number,
      name: 'innerHTML',
      age: node => parseInt(node.innerHTML)
      bio: 'outerHTML',
      relatives: ['innerHTML'],
    }
  ]
}

const otherQuery = {
  people: ['innerHTML'],
}


connect(render, <People />, peopleQuery)
connect(render, <Other />, otherQuery)
// People.jsx

import React from 'react';


function People({ title, people }) {
  const logToConsole = () => console.log(people.map(obj => obj.name));

  const list = people.map(({name, bio, id }) => (
    <div key={id}>
      <h2>{name}</h2>
      <div dangerouslySetInnerHTML={{ __html: bio }}>
    </div>
  ));

  return (
    <div>
      <h1>{title}</h1>
      {list.length > 0 ? list : null}
    </div>
    <div>
      <button onClick={logToConsole}>Log list of people to console</button>
  );
}
0.1.5

7 years ago

0.1.4

7 years ago

0.1.3

7 years ago

0.1.2

7 years ago

0.1.1

7 years ago

0.1.0

7 years ago