react-html-connect v0.1.5
๐ React HTML Connect ยท

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 examplePreact.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 examplePreact.h
).component <React Component>
: The React (or React-like) component you want to bind to a specific area in your template (via thedata-component
attributes). Converts the name of the component to a string and searches for adata-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 toquery
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 insideoptions
:
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 whendata-component
is already in use elsewhere.
Examples
Below area a couple of examples to illustrate the connect function can be used:
Basic Examples
- Basic usage
- Passing props to components
- Initialising multiple components
- Overriding the query schema
- Using the nodeQuery method independantly
- Scoping method to specific DOM node
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'
returnstrue
orfalse
, 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="{ "brother": "Billy Johnson", "sister": "Jane Doe" }'></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>
);
}