├── .babelrc ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── LICENSE ├── README.md ├── dist └── IframeComm.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── Demo.js ├── Demo.test.js ├── IframeComm.js ├── IframeComm.test.js └── stories │ └── index.js ├── storybook-static ├── favicon.ico ├── iframe.html ├── index.html └── static │ ├── manager.3c744e6541ad696e6e59.bundle.js │ ├── manager.3c744e6541ad696e6e59.bundle.js.map │ ├── preview.9f856c001e9f0142c59b.bundle.js │ └── preview.9f856c001e9f0142c59b.bundle.js.map └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@kadira/storybook/addons"; 2 | import "@kadira/storybook-addon-notes/register"; 3 | import "@kadira/storybook-addon-knobs/register"; 4 | import "@kadira/storybook-addon-options/register"; 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@kadira/storybook"; 2 | import { setOptions } from "@kadira/storybook-addon-options"; 3 | setOptions({ 4 | name: "Cool", 5 | downPanelInRight: true 6 | }); 7 | 8 | function loadStories() { 9 | require("../src/stories"); 10 | } 11 | 12 | configure(loadStories, module); 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Petar Bojinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React iFrame communication 2 | ============ 3 | 4 | > A React component for communicating between a parent window and an iframe. 5 | 6 | ## Demo 7 | 8 | Live Demo: [https://pbojinov.github.io/react-iframe-comm/](https://pbojinov.github.io/react-iframe-comm/) 9 | 10 | Or locally run: 11 | 12 | ``` 13 | npm install 14 | npm run storybook 15 | ``` 16 | 17 | Then open [http://localhost:9009/](http://localhost:9009/) in your browser. 18 | 19 | 20 | ## Installation 21 | 22 | The easiest way to use React Iframe Communication is to install it from NPM. 23 | 24 | ``` 25 | npm install react-iframe-comm --save 26 | ``` 27 | 28 | At this point you can import `react-iframe-comm` in your application as follows: 29 | 30 | ```javascript 31 | import IframeComm from 'react-iframe-comm'; 32 | ``` 33 | 34 | ## Usage 35 | 36 | 37 | ```javascript 38 | import React from "react"; 39 | import IframeComm from "react-iframe-comm"; 40 | 41 | const Demo = ({}) => { 42 | 43 | // the html attributes to create the iframe with 44 | // make sure you use camelCase attribute names 45 | const attributes = { 46 | src: "https://pbojinov.github.io/iframe-communication/iframe.html", 47 | width: "100%", 48 | height: "175", 49 | frameBorder: 1, // show frame border just for fun... 50 | }; 51 | 52 | // the postMessage data you want to send to your iframe 53 | // it will be send after the iframe has loaded 54 | const postMessageData = "hello iframe"; 55 | 56 | // parent received a message from iframe 57 | const onReceiveMessage = () => { 58 | console.log("onReceiveMessage"); 59 | }; 60 | 61 | // iframe has loaded 62 | const onReady = () => { 63 | console.log("onReady"); 64 | }; 65 | 66 | return ( 67 | 73 | ); 74 | }; 75 | 76 | export default Demo; 77 | 78 | ``` 79 | 80 | ## Configuration Options 81 | 82 | 83 | ```javascript 84 | IframeComm.propTypes = { 85 | /* 86 | Iframe Attributes 87 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#Attributes 88 | React Supported Attributes 89 | https://facebook.github.io/react/docs/dom-elements.html#all-supported-html-attributes 90 | Note: attributes are camelCase, not all lowercase as usually defined. 91 | */ 92 | attributes: PropTypes.shape({ 93 | allowFullScreen: PropTypes.oneOfType([ 94 | PropTypes.string, 95 | PropTypes.bool 96 | ]), 97 | frameBorder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 98 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 99 | name: PropTypes.string, 100 | scrolling: PropTypes.string, 101 | // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ 102 | sandbox: PropTypes.string, 103 | srcDoc: PropTypes.string, 104 | src: PropTypes.string.isRequired, 105 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 106 | }), 107 | 108 | // Callback function called when iFrame sends the parent window a message. 109 | handleReceiveMessage: PropTypes.func, 110 | 111 | /* 112 | Callback function called when iframe loads. 113 | We're simply listening to the iframe's `window.onload`. 114 | To ensure communication code in your iframe is totally loaded, 115 | you can implement a syn-ack TCP-like handshake using `postMessageData` and `handleReceiveMessage`. 116 | */ 117 | handleReady: PropTypes.func, 118 | 119 | /* 120 | You can pass it anything you want, we'll serialize to a string 121 | preferablly use a simple string message or an object. 122 | If you use an object, you need to follow the same naming convention 123 | in the iframe so you can parse it accordingly. 124 | */ 125 | postMessageData: PropTypes.any.isRequired, 126 | 127 | /* 128 | Enable use of the browser's built-in structured clone algorithm for serialization 129 | by settings this to `false`. 130 | Default is `true`, using our built in logic for serializing everything to a string. 131 | */ 132 | serializeMessage: PropTypes.bool, 133 | 134 | /* 135 | Always provide a specific targetOrigin, not *, if you know where the other window's document should be located. Failing to provide a specific target discloses the data you send to any interested malicious site. 136 | */ 137 | targetOrigin: PropTypes.string 138 | }; 139 | ``` 140 | 141 | ## License 142 | 143 | The MIT License (MIT) - 2017 144 | -------------------------------------------------------------------------------- /dist/IframeComm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 10 | 11 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 12 | 13 | var _react = require("react"); 14 | 15 | var _react2 = _interopRequireDefault(_react); 16 | 17 | var _propTypes = require("prop-types"); 18 | 19 | var _propTypes2 = _interopRequireDefault(_propTypes); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 26 | 27 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 28 | 29 | var IframeComm = function (_Component) { 30 | _inherits(IframeComm, _Component); 31 | 32 | function IframeComm() { 33 | _classCallCheck(this, IframeComm); 34 | 35 | var _this = _possibleConstructorReturn(this, (IframeComm.__proto__ || Object.getPrototypeOf(IframeComm)).call(this)); 36 | 37 | _this.onReceiveMessage = _this.onReceiveMessage.bind(_this); 38 | _this.onLoad = _this.onLoad.bind(_this); 39 | _this.sendMessage = _this.sendMessage.bind(_this); 40 | return _this; 41 | } 42 | 43 | _createClass(IframeComm, [{ 44 | key: "componentDidMount", 45 | value: function componentDidMount() { 46 | window.addEventListener("message", this.onReceiveMessage); 47 | this._frame.addEventListener("load", this.onLoad); 48 | } 49 | }, { 50 | key: "componentWillUnmount", 51 | value: function componentWillUnmount() { 52 | window.removeEventListener("message", this.onReceiveMessage, false); 53 | } 54 | }, { 55 | key: "componentWillReceiveProps", 56 | value: function componentWillReceiveProps(nextProps) { 57 | if (this.props.postMessageData !== nextProps.postMessageData) { 58 | // send a message if postMessageData changed 59 | this.sendMessage(nextProps.postMessageData); 60 | } 61 | } 62 | }, { 63 | key: "onReceiveMessage", 64 | value: function onReceiveMessage(event) { 65 | var handleReceiveMessage = this.props.handleReceiveMessage; 66 | 67 | if (handleReceiveMessage) { 68 | handleReceiveMessage(event); 69 | } 70 | } 71 | }, { 72 | key: "onLoad", 73 | value: function onLoad() { 74 | var handleReady = this.props.handleReady; 75 | 76 | if (handleReady) { 77 | handleReady(); 78 | } 79 | // TODO: Look into doing a syn-ack TCP-like handshake 80 | // to make sure iFrame is ready to REALLY accept messages, not just loaded. 81 | // send intial props when iframe loads 82 | this.sendMessage(this.props.postMessageData); 83 | } 84 | }, { 85 | key: "serializePostMessageData", 86 | value: function serializePostMessageData(data) { 87 | // Rely on the browser's built-in structured clone algorithm for serialization of the 88 | // message as described in 89 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage 90 | if (!this.props.serializeMessage) { 91 | return data; 92 | } 93 | 94 | // To be on the safe side we can also ignore the browser's built-in serialization feature 95 | // and serialize the data manually. 96 | if ((typeof data === "undefined" ? "undefined" : _typeof(data)) === "object") { 97 | return JSON.stringify(data); 98 | } else if (typeof data === "string") { 99 | return data; 100 | } else { 101 | return "" + data; 102 | } 103 | } 104 | }, { 105 | key: "sendMessage", 106 | value: function sendMessage(postMessageData) { 107 | // Using postMessage data from props will result in a subtle but deadly bug, 108 | // where old data from props is being sent instead of new postMessageData. 109 | // This is because data sent from componentWillReceiveProps is not yet in props but only in nextProps. 110 | var targetOrigin = this.props.targetOrigin; 111 | 112 | var serializedData = this.serializePostMessageData(postMessageData); 113 | this._frame.contentWindow.postMessage(serializedData, targetOrigin); 114 | } 115 | }, { 116 | key: "render", 117 | value: function render() { 118 | var _this2 = this; 119 | 120 | var attributes = this.props.attributes; 121 | // define some sensible defaults for our iframe attributes 122 | 123 | var defaultAttributes = { 124 | allowFullScreen: false, 125 | frameBorder: 0 126 | }; 127 | // then merge in the user's attributes with our defaults 128 | var mergedAttributes = Object.assign({}, defaultAttributes, attributes); 129 | return _react2.default.createElement("iframe", _extends({ 130 | ref: function ref(el) { 131 | _this2._frame = el; 132 | } 133 | }, mergedAttributes)); 134 | } 135 | }]); 136 | 137 | return IframeComm; 138 | }(_react.Component); 139 | 140 | IframeComm.defaultProps = { 141 | serializeMessage: true, 142 | targetOrigin: "*", 143 | postMessageData: "" 144 | }; 145 | 146 | IframeComm.propTypes = { 147 | /* 148 | Iframe Attributes 149 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#Attributes 150 | React Supported Attributes 151 | https://facebook.github.io/react/docs/dom-elements.html#all-supported-html-attributes 152 | Note: attributes are camelCase, not all lowercase as usually defined. 153 | */ 154 | attributes: _propTypes2.default.shape({ 155 | allowFullScreen: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.bool]), 156 | frameBorder: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]), 157 | height: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]), 158 | name: _propTypes2.default.string, 159 | scrolling: _propTypes2.default.string, 160 | // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ 161 | sandbox: _propTypes2.default.string, 162 | srcDoc: _propTypes2.default.string, 163 | src: _propTypes2.default.string.isRequired, 164 | width: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.number]) 165 | }), 166 | 167 | // Callback function called when iFrame sends the parent window a message. 168 | handleReceiveMessage: _propTypes2.default.func, 169 | 170 | /* 171 | Callback function called when iframe loads. 172 | We're simply listening to the iframe's `window.onload`. 173 | To ensure communication code in your iframe is totally loaded, 174 | you can implement a syn-ack TCP-like handshake using `postMessageData` and `handleReceiveMessage`. 175 | */ 176 | handleReady: _propTypes2.default.func, 177 | 178 | /* 179 | You can pass it anything you want, we'll serialize to a string 180 | preferablly use a simple string message or an object. 181 | If you use an object, you need to follow the same naming convention 182 | in the iframe so you can parse it accordingly. 183 | */ 184 | postMessageData: _propTypes2.default.any.isRequired, 185 | 186 | /* 187 | Enable use of the browser's built-in structured clone algorithm for serialization 188 | by settings this to `false`. 189 | Default is `true`, using our built in logic for serializing everything to a string. 190 | */ 191 | serializeMessage: _propTypes2.default.bool, 192 | 193 | /* 194 | Always provide a specific targetOrigin, not *, if you know where the other window's document should be located. Failing to provide a specific target discloses the data you send to any interested malicious site. 195 | */ 196 | targetOrigin: _propTypes2.default.string 197 | }; 198 | 199 | exports.default = IframeComm; 200 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-iframe-comm", 3 | "version": "1.2.2", 4 | "description": "A React component for communicating between a parent window and an iframe.", 5 | "main": "dist/IframeComm.js", 6 | "author": "Petar Bojinov ", 7 | "bugs": { 8 | "url": "https://github.com/pbojinov/react-iframe-comm/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/pbojinov/react-iframe-comm.git" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "iframe", 17 | "communication", 18 | "comm", 19 | "postMessage", 20 | "react-component" 21 | ], 22 | "license": "MIT", 23 | "dependencies": { 24 | "@kadira/storybook-deployer": "^1.2.0", 25 | "react": "^15.4.2", 26 | "react-dom": "^15.4.2", 27 | "prop-types": "^15.5.7" 28 | }, 29 | "devDependencies": { 30 | "@kadira/storybook": "^2.21.0", 31 | "@kadira/storybook-addon-knobs": "^1.7.1", 32 | "@kadira/storybook-addon-notes": "^1.0.1", 33 | "@kadira/storybook-addon-options": "^1.0.2", 34 | "babel-core": "^6.24.0", 35 | "babel-loader": "^6.4.1", 36 | "babel-preset-es2015": "^6.24.0", 37 | "babel-preset-react": "^6.23.0", 38 | "babel-preset-stage-0": "^6.22.0", 39 | "react-scripts": "0.9.5" 40 | }, 41 | "scripts": { 42 | "start": "react-scripts start", 43 | "/*--build--*/": "react-scripts build", 44 | "build": " babel src/IframeComm.js --out-file dist/IframeComm.js", 45 | "test": "react-scripts test --env=jsdom", 46 | "eject": "react-scripts eject", 47 | "storybook": "start-storybook -p 9009 -s public", 48 | "build-storybook": "build-storybook -s public", 49 | "deploy-storybook": "storybook-to-ghpages" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbojinov/react-iframe-comm/ab4b076ea17672483b21b1720b1983fd993fe980/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React App 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IframeComm from "react-iframe-comm"; // loads build file 3 | 4 | const Demo = ({}) => { 5 | // the html attributes to create the iframe with 6 | // make sure you use camelCase attribute names 7 | const attributes = { 8 | src: "https://pbojinov.github.io/iframe-communication/iframe.html", 9 | width: "100%", 10 | height: "175" 11 | }; 12 | 13 | // the postMessage data you want to send to your iframe 14 | // it will be send after the iframe has loaded 15 | const postMessageData = "hello iframe"; 16 | 17 | // parent received a message from iframe 18 | const onReceiveMessage = () => { 19 | console.log("handleReceiveMessage"); 20 | }; 21 | 22 | // iframe has loaded 23 | const onReady = () => { 24 | console.log("onReady"); 25 | }; 26 | 27 | return ( 28 | 34 | ); 35 | }; 36 | 37 | export default Demo; 38 | -------------------------------------------------------------------------------- /src/Demo.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Demo from "./Demo"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/IframeComm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from 'prop-types'; 3 | 4 | class IframeComm extends Component { 5 | constructor() { 6 | super(); 7 | this.onReceiveMessage = this.onReceiveMessage.bind(this); 8 | this.onLoad = this.onLoad.bind(this); 9 | this.sendMessage = this.sendMessage.bind(this); 10 | } 11 | componentDidMount() { 12 | window.addEventListener("message", this.onReceiveMessage); 13 | this._frame.addEventListener("load", this.onLoad); 14 | } 15 | componentWillUnmount() { 16 | window.removeEventListener("message", this.onReceiveMessage, false); 17 | } 18 | componentWillReceiveProps(nextProps) { 19 | if (this.props.postMessageData !== nextProps.postMessageData) { 20 | // send a message if postMessageData changed 21 | this.sendMessage(nextProps.postMessageData); 22 | } 23 | } 24 | onReceiveMessage(event) { 25 | const { handleReceiveMessage } = this.props; 26 | if (handleReceiveMessage) { 27 | handleReceiveMessage(event); 28 | } 29 | } 30 | onLoad() { 31 | const { handleReady } = this.props; 32 | if (handleReady) { 33 | handleReady(); 34 | } 35 | // TODO: Look into doing a syn-ack TCP-like handshake 36 | // to make sure iFrame is ready to REALLY accept messages, not just loaded. 37 | // send intial props when iframe loads 38 | this.sendMessage(this.props.postMessageData); 39 | } 40 | serializePostMessageData(data) { 41 | // Rely on the browser's built-in structured clone algorithm for serialization of the 42 | // message as described in 43 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage 44 | if (!this.props.serializeMessage) { 45 | return data; 46 | } 47 | 48 | // To be on the safe side we can also ignore the browser's built-in serialization feature 49 | // and serialize the data manually. 50 | if (typeof data === "object") { 51 | return JSON.stringify(data); 52 | } else if (typeof data === "string") { 53 | return data; 54 | } else { 55 | return `${data}`; 56 | } 57 | } 58 | sendMessage(postMessageData) { 59 | // Using postMessage data from props will result in a subtle but deadly bug, 60 | // where old data from props is being sent instead of new postMessageData. 61 | // This is because data sent from componentWillReceiveProps is not yet in props but only in nextProps. 62 | const { targetOrigin } = this.props; 63 | const serializedData = this.serializePostMessageData(postMessageData); 64 | this._frame.contentWindow.postMessage(serializedData, targetOrigin); 65 | } 66 | render() { 67 | const { attributes } = this.props; 68 | // define some sensible defaults for our iframe attributes 69 | const defaultAttributes = { 70 | allowFullScreen: false, 71 | frameBorder: 0 72 | }; 73 | // then merge in the user's attributes with our defaults 74 | const mergedAttributes = Object.assign( 75 | {}, 76 | defaultAttributes, 77 | attributes 78 | ); 79 | return ( 80 |