react-shadow
Advanced tools
Comparing version 1.1.2 to 2.0.0
@@ -1,8 +0,15 @@ | ||
import http from 'http'; | ||
import express from 'express'; | ||
import { readFileSync } from 'fs'; | ||
import http from 'http'; | ||
import express from 'express'; | ||
const app = express(); | ||
const app = express(); | ||
const server = http.createServer(app); | ||
const port = process.env.PORT || 5000; | ||
app.use(express.static(__dirname + '/example')); | ||
server.listen(process.env.PORT || 5000); | ||
app.get(/\.html$/i, (_, res) => { | ||
res.send(readFileSync(`${__dirname}/example/index.html`, 'utf-8')); | ||
}); | ||
app.use(express.static(`${__dirname}/example`)); | ||
server.listen(port); |
{ | ||
"name": "react-shadow", | ||
"version": "1.1.2", | ||
"version": "2.0.0", | ||
"description": "Utilise Shadow DOM in React with all the benefits of style encapsulation.", | ||
"main": "dist/react-shadow.js", | ||
"scripts": { | ||
"prestart": "npm run build", | ||
"start": "babel example/server/default.js | node", | ||
"build": "webpack && npm run example", | ||
"watch": "webpack --config webpack.dev-config.js --watch", | ||
"example": "webpack --config webpack.dev-config.js", | ||
@@ -32,2 +34,6 @@ "test": "npm run spec && npm run lint", | ||
"homepage": "https://github.com/Wildhoney/ReactShadow", | ||
"dependencies": { | ||
"react": ">=0.14.7 <=15.x", | ||
"react-dom": ">=0.14.7 <=15.x" | ||
}, | ||
"devDependencies": { | ||
@@ -40,2 +46,3 @@ "ava": "^0.16.0", | ||
"babel-loader": "^6.2.5", | ||
"babel-polyfill": "~6.23.0", | ||
"babel-preset-es2015": "^6.14.0", | ||
@@ -52,2 +59,3 @@ "babel-preset-react": "^6.11.1", | ||
"express": "^4.11.2", | ||
"humps": "~2.0.0", | ||
"jasmine-core": "^2.2.0", | ||
@@ -58,2 +66,9 @@ "jsdom": "^9.5.0", | ||
"react-addons-test-utils": "^15.3.1", | ||
"react-document-title": "~2.0.3", | ||
"react-redux": "~5.0.3", | ||
"react-router": "~4.0.0", | ||
"react-router-dom": "~4.0.0", | ||
"react-thunk": "~1.0.0", | ||
"redux": "~3.6.0", | ||
"redux-thunk": "~2.2.0", | ||
"sinon": "^1.17.5", | ||
@@ -98,3 +113,5 @@ "webpack": "^2.1.0-beta.22", | ||
], | ||
"key-spacing": "off", | ||
"default-case": "off", | ||
"no-multi-spaces": "off", | ||
"no-unused-expressions": "off", | ||
@@ -117,7 +134,3 @@ "no-case-declarations": "off", | ||
} | ||
}, | ||
"dependencies": { | ||
"react": ">=0.14.7 <=15.x", | ||
"react-dom": ">=0.14.7 <=15.x" | ||
} | ||
} |
@@ -1,10 +0,25 @@ | ||
import { get as fetch } from 'axios'; | ||
import { get as fetch } from 'axios'; | ||
import React, { Component, PropTypes, DOM, Children } from 'react'; | ||
import { render, findDOMNode } from 'react-dom'; | ||
import dissoc from 'ramda/src/dissoc'; | ||
import memoize from 'ramda/src/memoize'; | ||
import groupBy from 'ramda/src/groupBy'; | ||
import { render, findDOMNode } from 'react-dom'; | ||
import { dissoc, memoize, groupBy } from 'ramda'; | ||
/** | ||
* @method raise | ||
* @constant includeMap | ||
* @type {Object} | ||
*/ | ||
const includeMap = [ | ||
{ extensions: ['js'], tag: 'script', attrs: { type: 'text/javascript' } }, | ||
{ extensions: ['css'], tag: 'style', attrs: { type: 'text/css' } } | ||
]; | ||
/** | ||
* @constant defaultContextTypes | ||
* @type {Object} | ||
*/ | ||
const defaultContextTypes = { | ||
router: PropTypes.object | ||
}; | ||
/** | ||
* @method throwError | ||
* @param {String} message | ||
@@ -14,3 +29,3 @@ * @throws {Error} | ||
*/ | ||
const raise = message => { | ||
const throwError = message => { | ||
throw new Error(`ReactShadow: ${message}.`); | ||
@@ -33,190 +48,264 @@ }; | ||
/** | ||
* @constant includeMap | ||
* @type {Object} | ||
* @method withContext | ||
* @param {Object} contextTypes | ||
* @return {ShadowDOM} | ||
*/ | ||
const includeMap = [ | ||
{ | ||
extensions: ['js'], tag: 'script', attrs: { | ||
type: 'text/javascript' | ||
} | ||
}, | ||
{ | ||
extensions: ['css'], tag: 'style', attrs: { | ||
type: 'text/css' | ||
} | ||
} | ||
]; | ||
export const withContext = contextTypes => { | ||
/** | ||
* @class ShadowDOM | ||
* @extends Component | ||
*/ | ||
export default class ShadowDOM extends Component { | ||
/** | ||
* @constant propTypes | ||
* @type {Object} | ||
* @method createContextProvider | ||
* @param {Object} context | ||
* @return {ContextProvider} | ||
*/ | ||
static propTypes = { | ||
children: PropTypes.node.isRequired, | ||
include: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), | ||
nodeName: PropTypes.string, | ||
boundaryMode: PropTypes.oneOf(['open', 'closed']), | ||
delegatesFocus: PropTypes.bool | ||
}; | ||
const createContextProvider = context => { | ||
/** | ||
* @constant defaultProps | ||
* @type {Object} | ||
*/ | ||
static defaultProps = { | ||
include: [], | ||
nodeName: 'span', | ||
boundaryMode: 'open', | ||
delegatesFocus: false | ||
/** | ||
* @class ContextProvider | ||
* @extends {Component} | ||
*/ | ||
class ContextProvider extends Component { | ||
/** | ||
* @constant propTypes | ||
* @type {Object} | ||
*/ | ||
static propTypes = { | ||
children: PropTypes.node.isRequired | ||
}; | ||
/** | ||
* @constant childContextTypes | ||
* @type {Object} | ||
*/ | ||
static childContextTypes = contextTypes; | ||
/** | ||
* @method shouldComponentUpdate | ||
* @return {Boolean} | ||
*/ | ||
shouldComponentUpdate() { | ||
return true; | ||
} | ||
/** | ||
* @method getChildContext | ||
* @return {Object} | ||
*/ | ||
getChildContext() { | ||
return context; | ||
} | ||
/** | ||
* @method render | ||
* @return {XML} | ||
*/ | ||
render() { | ||
return this.props.children; | ||
} | ||
} | ||
return ContextProvider; | ||
}; | ||
/** | ||
* @constructor | ||
* @class ShadowDOM | ||
* @extends Component | ||
*/ | ||
constructor() { | ||
super(); | ||
this.state = { resolving: false }; | ||
} | ||
return class ShadowDOM extends Component { | ||
/** | ||
* @method getContainer | ||
* @return {Object} | ||
*/ | ||
getContainer() { | ||
/** | ||
* @constant contextTypes | ||
* @type {Object} | ||
*/ | ||
static contextTypes = contextTypes; | ||
// Wrap children in a container if it's an array of children, otherwise | ||
// simply render the single child which is a valid `ReactElement` instance. | ||
const children = this.props.children.props.children; | ||
return children.length ? <this.props.nodeName>{children}</this.props.nodeName> : children; | ||
/** | ||
* @constant propTypes | ||
* @type {Object} | ||
*/ | ||
static propTypes = { | ||
children: PropTypes.node.isRequired, | ||
include: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), | ||
nodeName: PropTypes.string, | ||
boundaryMode: PropTypes.oneOf(['open', 'closed']), | ||
delegatesFocus: PropTypes.bool | ||
}; | ||
} | ||
/** | ||
* @constant defaultProps | ||
* @type {Object} | ||
*/ | ||
static defaultProps = { | ||
include: [], | ||
nodeName: 'span', | ||
boundaryMode: 'open', | ||
delegatesFocus: false | ||
}; | ||
/** | ||
* @method componentDidMount | ||
* @return {void} | ||
*/ | ||
componentDidMount() { | ||
/** | ||
* @constant state | ||
* @type {Object} | ||
*/ | ||
state = { resolving: false }; | ||
const { boundaryMode: mode, delegatesFocus} = this.props; | ||
/** | ||
* @constant ContextProvider | ||
* @type {ContextProvider} | ||
*/ | ||
ContextProvider = createContextProvider(this.context); | ||
// Create the shadow root and take the CSS documents from props. | ||
const node = findDOMNode(this); | ||
const root = node.attachShadow ? node.attachShadow({ mode, delegatesFocus }) : node.createShadowRoot(); | ||
const include = Array.isArray(this.props.include) ? this.props.include : [this.props.include]; | ||
const container = this.getContainer(); | ||
/** | ||
* @constant WrappedComponent | ||
* @type {Object} | ||
*/ | ||
WrappedComponent = this.props.children; | ||
// Render the passed in component to the shadow root, and then `setState` if there | ||
// are no CSS documents to be resolved. | ||
render(container, root); | ||
!include.length && this.setState({ root }); | ||
/** | ||
* @method componentDidMount | ||
* @return {void} | ||
*/ | ||
componentDidMount() { | ||
if (include.length) { | ||
const { boundaryMode: mode, delegatesFocus } = this.props; | ||
// Otherwise we'll fetch and attach the passed in stylesheets which need to be | ||
// resolved before `state.resolved` becomes `true` again. | ||
this.setState({ resolving: true, root }); | ||
this.attachIncludes(include); | ||
// Create the shadow root and take the CSS documents from props. | ||
const node = findDOMNode(this); | ||
const root = node.attachShadow ? node.attachShadow({ mode, delegatesFocus }) : node.createShadowRoot(); | ||
const include = [].concat(this.props.include); | ||
const container = this.wrapContainer(); | ||
// Render the passed in component to the shadow root, and then `setState` if there | ||
// are no CSS documents to be resolved. | ||
render(container, root); | ||
include.length === 0 ? this.setState({ root }) : do { | ||
// Otherwise we'll fetch and attach the passed in stylesheets which need to be | ||
// resolved before `state.resolved` becomes `true` again. | ||
this.setState({ root, resolving: true }); | ||
this.attachIncludes(include); | ||
}; | ||
} | ||
} | ||
/** | ||
* @method wrapContainer | ||
* @return {Object} | ||
*/ | ||
wrapContainer() { | ||
/** | ||
* @method componentDidUpdate | ||
* @return {void} | ||
*/ | ||
componentDidUpdate() { | ||
// Wrap children in a container if it's an array of children, otherwise simply render the single child | ||
// which is a valid `ReactElement` instance. | ||
const { children } = this.props.children.props; | ||
const child = children.length ? <this.props.nodeName>{children}</this.props.nodeName> : children; | ||
const ContextProvider = this.ContextProvider; | ||
// Updates consist of simply rendering the container element into the shadow root | ||
// again, as the `this.getContainer()` element contains the passed in component's | ||
// children. | ||
render(this.getContainer(), this.state.root); | ||
/** | ||
* @method getChildContext | ||
* @return {Object} | ||
*/ | ||
ContextProvider.prototype.getChildContext = () => this.context; | ||
} | ||
return <ContextProvider>{child}</ContextProvider>; | ||
/** | ||
* @method attachIncludes | ||
* @param include {Array|String} | ||
* @return {void} | ||
*/ | ||
attachIncludes(include) { | ||
} | ||
// Group all of the includes by their extension. | ||
const groupedFiles = groupBy(file => file.extension)(include.map(path => ({ path, extension: path.split('.').pop() }))); | ||
/** | ||
* @method componentDidUpdate | ||
* @return {void} | ||
*/ | ||
componentDidUpdate() { | ||
const includeFiles = Object.keys(groupedFiles).map(extension => { | ||
// Updates consist of simply rendering the container element into the shadow root | ||
// again, as the `this.wrapContainer()` element contains the passed in component's | ||
// children. | ||
render(this.wrapContainer(), this.state.root); | ||
const nodeData = includeMap.find(model => model.extensions.includes(extension)); | ||
const files = groupedFiles[extension].map(model => model.path); | ||
} | ||
if (!nodeData) { | ||
raise(`Files with extension of "${extension}" are unsupported`); | ||
} | ||
/** | ||
* @method attachIncludes | ||
* @param include {Array} | ||
* @return {void} | ||
*/ | ||
attachIncludes(include) { | ||
const containerElement = document.createElement(nodeData.tag); | ||
// Group all of the includes by their extension. | ||
const groupedFiles = groupBy(file => file.extension)(include.map(path => ({ path, extension: path.split('.').pop() }))); | ||
const includeFiles = Object.keys(groupedFiles).map(extension => { | ||
// Apply all of the attributes defined in the `includeMap` to the node. | ||
Object.keys(nodeData.attrs).map(key => containerElement.setAttribute(key, nodeData.attrs[key])); | ||
const nodeData = includeMap.find(model => model.extensions.includes(extension)); | ||
const files = groupedFiles[extension].map(model => model.path); | ||
// Load each file individually and then concatenate them. | ||
return Promise.all(files.map(fetchInclude)).then(fileData => { | ||
containerElement.innerHTML = fileData.reduce((acc, fileDatum) => `${acc} ${fileDatum}`).trim(); | ||
containerElement.innerHTML.length && this.state.root.appendChild(containerElement); | ||
!nodeData && throwError(`Files with extension of "${extension}" are unsupported`); | ||
const containerElement = document.createElement(nodeData.tag); | ||
// Apply all of the attributes defined in the `includeMap` to the node. | ||
Object.keys(nodeData.attrs).map(key => containerElement.setAttribute(key, nodeData.attrs[key])); | ||
// Load each file individually and then concatenate them. | ||
return Promise.all(files.map(fetchInclude)).then(fileData => { | ||
containerElement.innerHTML = fileData.reduce((acc, fileDatum) => `${acc} ${fileDatum}`).trim(); | ||
containerElement.innerHTML.length && this.state.root.appendChild(containerElement); | ||
}); | ||
}); | ||
}); | ||
Promise.all(includeFiles).then(() => this.setState({ resolving: false })); | ||
Promise.all(includeFiles).then(() => this.setState({ resolving: false })); | ||
} | ||
} | ||
/** | ||
* @method throwInvariants | ||
* @return {Boolean|void} | ||
*/ | ||
throwInvariants() { | ||
/** | ||
* @method performSanityChecks | ||
* @return {void} | ||
*/ | ||
performSanityChecks() { | ||
// Ensure that the passed child isn't an array of children. | ||
Array.isArray(this.props.children) && throwError('You must pass a single child rather than multiple children'); | ||
// Ensure that the passed child isn't an array of children. | ||
Array.isArray(this.props.children) && raise('You must pass a single child rather than multiple children'); | ||
if (typeof this.props.children.type !== 'string') { | ||
if (typeof this.props.children.type !== 'string') { | ||
// Ensure that the passed child has a valid node name. | ||
throwError('Passed child must be a concrete HTML element rather than another React component'); | ||
// Ensure that the passed child has a valid node name. | ||
raise('Passed child must be a concrete HTML element rather than another React component'); | ||
} | ||
return true; | ||
} | ||
} | ||
/** | ||
* @method render | ||
* @return {XML} | ||
*/ | ||
render() { | ||
/** | ||
* @method render | ||
* @return {XML} | ||
*/ | ||
render() { | ||
return this.throwInvariants() && do { | ||
// Process the necessary sanity checks. | ||
this.performSanityChecks(); | ||
// Props from the passed component, minus `children` as that's handled by `componentDidMount`. | ||
const child = Children.only(this.props.children); | ||
const childProps = dissoc('children', child.props); | ||
const className = this.state.resolving ? 'resolving' : 'resolved'; | ||
// Take all of the props from the passed in component, minus the `children` props | ||
// as that's handled by `componentDidMount`. | ||
const child = Children.only(this.props.children); | ||
const props = dissoc('children', child.props); | ||
const className = this.state.resolving ? 'resolving' : 'resolved'; | ||
// See: https://github.com/facebook/react/issues/4933 | ||
const classNames = `${childProps.className ? childProps.className : ''} ${className}`.trim(); | ||
const isSupported = child.type in DOM; | ||
const classProp = isSupported ? 'className' : 'class'; | ||
const props = { ...dissoc('className', childProps), [classProp]: classNames }; | ||
// Determine whether to use `class` or `className`, as custom elements do not allow | ||
// for `className`. See: https://github.com/facebook/react/issues/4933 | ||
const classNames = `${props.className ? props.className : ''} ${className}`.trim(); | ||
const isSupportedElement = child.type in DOM; | ||
const propName = isSupportedElement ? 'className' : 'class'; | ||
<child.type {...props} />; | ||
return <child.type {...{ ...dissoc('className', props), [propName]: classNames }} />; | ||
}; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
export default withContext(defaultContextTypes); |
@@ -55,3 +55,3 @@ import test from 'ava'; | ||
t.is(attachShadow.callCount, 1); | ||
t.true(attachShadow.calledWith({ mode: 'open' })); | ||
t.true(attachShadow.calledWith({ mode: 'open', delegatesFocus: false })); | ||
@@ -75,6 +75,15 @@ const host = wrapper.find('section'); | ||
mount(<Clock />); | ||
t.true(attachShadow.calledWith({ mode: 'closed' })); | ||
t.true(attachShadow.calledWith({ mode: 'closed', delegatesFocus: false })); | ||
}); | ||
test('Should be able to change the focus delegation;', t => { | ||
const Clock = t.context.create({ delegatesFocus: true }); | ||
mount(<Clock />); | ||
t.true(attachShadow.calledWith({ mode: 'open', delegatesFocus: true })); | ||
}); | ||
test('Should be able to change the nested node;', t => { | ||
@@ -81,0 +90,0 @@ |
module.exports = { | ||
entry: { | ||
build: ['./example/js/app.js'] | ||
build: ['babel-polyfill', './example/js/Default.js'] | ||
}, | ||
@@ -5,0 +5,0 @@ output: { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
1069883
35
10849
34
4