react-helmet-async
Version:
Thread-safe Helmet for React 16+ and friends
241 lines (210 loc) • 7.42 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import fastCompare from 'react-fast-compare';
import invariant from 'invariant';
import { Context } from './Provider';
import Dispatcher from './Dispatcher';
import { TAG_NAMES, VALID_TAG_NAMES, HTML_TAG_MAP } from './constants';
export { default as HelmetProvider } from './Provider';
/* eslint-disable class-methods-use-this */
export class Helmet extends Component {
/**
* @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
* @param {Object} bodyAttributes: {"className": "root"}
* @param {String} defaultTitle: "Default Title"
* @param {Boolean} defer: true
* @param {Boolean} encodeSpecialCharacters: true
* @param {Object} htmlAttributes: {"lang": "en", "amp": undefined}
* @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
* @param {Array} meta: [{"name": "description", "content": "Test description"}]
* @param {Array} noscript: [{"innerHTML": "<img src='http://mysite.com/js/test.js'"}]
* @param {Function} onChangeClientState: "(newState) => console.log(newState)"
* @param {Array} script: [{"type": "text/javascript", "src": "http://mysite.com/js/test.js"}]
* @param {Array} style: [{"type": "text/css", "cssText": "div { display: block; color: blue; }"}]
* @param {String} title: "Title"
* @param {Object} titleAttributes: {"itemprop": "name"}
* @param {String} titleTemplate: "MySite.com - %s"
*/
/* eslint-disable react/prop-types, react/forbid-prop-types, react/require-default-props */
static propTypes = {
base: PropTypes.object,
bodyAttributes: PropTypes.object,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
defaultTitle: PropTypes.string,
defer: PropTypes.bool,
encodeSpecialCharacters: PropTypes.bool,
htmlAttributes: PropTypes.object,
link: PropTypes.arrayOf(PropTypes.object),
meta: PropTypes.arrayOf(PropTypes.object),
noscript: PropTypes.arrayOf(PropTypes.object),
onChangeClientState: PropTypes.func,
script: PropTypes.arrayOf(PropTypes.object),
style: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string,
titleAttributes: PropTypes.object,
titleTemplate: PropTypes.string,
};
/* eslint-enable react/prop-types, react/forbid-prop-types, react/require-default-props */
static defaultProps = {
defer: true,
encodeSpecialCharacters: true,
};
static displayName = 'Helmet';
shouldComponentUpdate(nextProps) {
return !fastCompare(this.props, nextProps);
}
mapNestedChildrenToProps(child, nestedChildren) {
if (!nestedChildren) {
return null;
}
switch (child.type) {
case TAG_NAMES.SCRIPT:
case TAG_NAMES.NOSCRIPT:
return {
innerHTML: nestedChildren,
};
case TAG_NAMES.STYLE:
return {
cssText: nestedChildren,
};
default:
throw new Error(
`<${
child.type
} /> elements are self-closing and can not contain children. Refer to our API for more information.`
);
}
}
flattenArrayTypeChildren({ child, arrayTypeChildren, newChildProps, nestedChildren }) {
return {
...arrayTypeChildren,
[child.type]: [
...(arrayTypeChildren[child.type] || []),
{
...newChildProps,
...this.mapNestedChildrenToProps(child, nestedChildren),
},
],
};
}
mapObjectTypeChildren({ child, newProps, newChildProps, nestedChildren }) {
switch (child.type) {
case TAG_NAMES.TITLE:
return {
...newProps,
[child.type]: nestedChildren,
titleAttributes: { ...newChildProps },
};
case TAG_NAMES.BODY:
return {
...newProps,
bodyAttributes: { ...newChildProps },
};
case TAG_NAMES.HTML:
return {
...newProps,
htmlAttributes: { ...newChildProps },
};
default:
return {
...newProps,
[child.type]: { ...newChildProps },
};
}
}
mapArrayTypeChildrenToProps(arrayTypeChildren, newProps) {
let newFlattenedProps = { ...newProps };
Object.keys(arrayTypeChildren).forEach(arrayChildName => {
newFlattenedProps = {
...newFlattenedProps,
[arrayChildName]: arrayTypeChildren[arrayChildName],
};
});
return newFlattenedProps;
}
warnOnInvalidChildren(child, nestedChildren) {
invariant(
VALID_TAG_NAMES.some(name => child.type === name),
typeof child.type === 'function'
? `You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.`
: `Only elements types ${VALID_TAG_NAMES.join(
', '
)} are allowed. Helmet does not support rendering <${
child.type
}> elements. Refer to our API for more information.`
);
invariant(
!nestedChildren ||
typeof nestedChildren === 'string' ||
(Array.isArray(nestedChildren) &&
!nestedChildren.some(nestedChild => typeof nestedChild !== 'string')),
`Helmet expects a string as a child of <${
child.type
}>. Did you forget to wrap your children in braces? ( <${child.type}>{\`\`}</${
child.type
}> ) Refer to our API for more information.`
);
return true;
}
mapChildrenToProps(children, newProps) {
let arrayTypeChildren = {};
React.Children.forEach(children, child => {
if (!child || !child.props) {
return;
}
const { children: nestedChildren, ...childProps } = child.props;
// convert React props to HTML attributes
const newChildProps = Object.keys(childProps).reduce((obj, key) => {
// eslint-disable-next-line no-param-reassign
obj[HTML_TAG_MAP[key] || key] = childProps[key];
return obj;
}, {});
let { type } = child;
if (typeof type === 'symbol') {
type = type.toString();
} else {
this.warnOnInvalidChildren(child, nestedChildren);
}
switch (type) {
case TAG_NAMES.FRAGMENT:
// eslint-disable-next-line no-param-reassign
newProps = this.mapChildrenToProps(nestedChildren, newProps);
break;
case TAG_NAMES.LINK:
case TAG_NAMES.META:
case TAG_NAMES.NOSCRIPT:
case TAG_NAMES.SCRIPT:
case TAG_NAMES.STYLE:
arrayTypeChildren = this.flattenArrayTypeChildren({
child,
arrayTypeChildren,
newChildProps,
nestedChildren,
});
break;
default:
// eslint-disable-next-line no-param-reassign
newProps = this.mapObjectTypeChildren({
child,
newProps,
newChildProps,
nestedChildren,
});
break;
}
});
return this.mapArrayTypeChildrenToProps(arrayTypeChildren, newProps);
}
render() {
const { children, ...props } = this.props;
let newProps = { ...props };
if (children) {
newProps = this.mapChildrenToProps(children, newProps);
}
return (
<Context.Consumer>
{context => <Dispatcher {...newProps} context={context} />}
</Context.Consumer>
);
}
}