├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── child.js ├── demo ├── child.html └── communication.html ├── jest.config.js ├── package.json ├── parent.js ├── rollup.config.js ├── src ├── Bus.ts ├── IFramyChild.ts ├── IFramyParent.ts ├── child.spec.js ├── constants.ts ├── error-types.ts ├── helpers.ts └── types.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [[ 3 | "@babel/preset-env", 4 | { 5 | "targets": { 6 | "browsers": [ 7 | "> 1%", 8 | "safari >= 8" 9 | ] 10 | } 11 | } 12 | ], "@babel/typescript"], 13 | "plugins": [ 14 | "@babel/proposal-class-properties", 15 | "@babel/proposal-object-rest-spread" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | node_modules/ 6 | examples/ 7 | src/ 8 | .npm 9 | rollup.config.js 10 | yarn.lock 11 | README.md 12 | .babelrc 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aliaksandr Yankouski 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 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/yankouskia/iframy/pulls) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yankouskia/iframy/blob/master/LICENSE) ![GitHub stars](https://img.shields.io/github/stars/yankouskia/iframy.svg?style=social) 2 | 3 | [![NPM](https://nodei.co/npm/iframy.png?downloads=true)](https://www.npmjs.com/package/iframy) 4 | 5 | # iframy 6 | 7 | Library for rendering cross-domain components and communication between them 8 | 9 | ## Installation 10 | 11 | npm: 12 | 13 | ```sh 14 | npm install iframy --save 15 | ``` 16 | 17 | yarn: 18 | 19 | ```sh 20 | yarn add iframy 21 | ``` 22 | 23 | ## DEMO 24 | 25 | Communication demo 26 | 27 | ## Support 28 | 29 | | IE
Internet Explorer | Edge
Microsoft Edge | FirefoxFirefox Dev
Mozilla Firefox | ChromeChrome DevChrome Canary
Google Chrome | OperaOpera Dev
Opera | SafariSafari TPSafari iOS
Safari | Android WebView
Android WebView 30 | | --- | --- | --- | --- | --- | --- | --- 31 | | 10+ * | 12+ | 8+ | 1+ | 9.5+ | 4+ | Yes 32 | 33 | \* - Only for inline mode 34 | 35 | ## API 36 | 37 | ### Parent 38 | 39 | #### create 40 | 41 | Use method to initiate instance and pass necessary props / iframe configuration 42 | 43 | `dimensions` - object with `width` and `height` properties, applied to iframe 44 | 45 | `props` - any serializable initial data to send to child 46 | 47 | `scrolling` - param to highlight whether content inside iframe should be scrollable 48 | 49 | `url` - url to open inside child iframe 50 | 51 | ```js 52 | import { IFramyParent } from 'iframy/parent'; 53 | 54 | const iframy = IFramyParent.create({ 55 | dimensions: { 56 | width: '80%', 57 | height: '80%', 58 | }, 59 | props: { 60 | name: 'Alex', 61 | }, 62 | scrolling: true, 63 | url: 'https://web-site.com', 64 | }); 65 | ``` 66 | 67 | #### render 68 | 69 | Async method to render iframe into specific container. Used for lazy rendering of component. Once promise is resolved - child component is ready to be used 70 | 71 | `selector` - string / HTMLElement parameter to point container where to render iframe 72 | 73 | ```js 74 | import { IFramyParent } from 'iframy/parent'; 75 | 76 | const iframy = IFramyParent.create({ 77 | dimensions: { 78 | width: '80%', 79 | height: '80%', 80 | }, 81 | props: { 82 | name: 'Alex', 83 | }, 84 | scrolling: true, 85 | url: 'https://web-site.com', 86 | }); 87 | 88 | await iframy.render('#container'); 89 | ``` 90 | 91 | #### emit 92 | 93 | Method to send message to child component 94 | 95 | ```js 96 | iframy.emit('message-type', { any: 'data' }); 97 | ``` 98 | 99 | #### addListener / on 100 | 101 | Method to subscribe to events, being sent from child 102 | 103 | ```js 104 | iframy.addListener('message-type', data => console.log(data)); 105 | 106 | // or use alias 107 | 108 | iframy.on('message-type', data => console.log(data)); 109 | ``` 110 | 111 | #### addListenerOnce / once 112 | 113 | Method to subscribe to events, being sent from child; emitted once and listener is removed after that 114 | 115 | ```js 116 | iframy.addListenerOnce('message-type', data => console.log(data)); 117 | 118 | // or use alias 119 | 120 | iframy.once('message-type', data => console.log(data)); 121 | ``` 122 | 123 | #### removeListener / off 124 | 125 | Method to remove specific listener from correspondent event type from child 126 | 127 | ```js 128 | iframy.removeListener('message-type', listener); 129 | 130 | // or use alias 131 | 132 | iframy.off('message-type', listener); 133 | ``` 134 | 135 | #### removeAllListeners / offAll 136 | 137 | Method to remove all listeners from correspondent event type from child 138 | 139 | ```js 140 | iframy.removeAllListeners('message-type'); 141 | 142 | // or use alias 143 | 144 | iframy.offAll('message-type'); 145 | ``` 146 | 147 | 148 | ### Child 149 | 150 | #### create 151 | 152 | Use method to initialize child component and let parent know, that your iframe is ready 153 | 154 | `api` - object of `{ [key: string]: function }` structure to initialize api, being used by parent 155 | 156 | ```js 157 | import { IFramyChild } from 'iframy/child'; 158 | 159 | const iframy = await IFramyChild.create({ 160 | api: { 161 | sendMessage: data => {; 162 | return `Message: ${data}`; 163 | }, 164 | }, 165 | }); 166 | ``` 167 | 168 | #### props 169 | 170 | Data, passed from parent. Useful to receive initial data from parent window 171 | 172 | ```js 173 | const data = iframy.props; 174 | ``` 175 | 176 | #### emit 177 | 178 | Method to send message to parent component 179 | 180 | ```js 181 | iframy.emit('message-type', { any: 'data' }); 182 | ``` 183 | 184 | #### addListener / on 185 | 186 | Method to subscribe to events, being sent from parent 187 | 188 | ```js 189 | iframy.addListener('message-type', data => console.log(data)); 190 | 191 | // or use alias 192 | 193 | iframy.on('message-type', data => console.log(data)); 194 | ``` 195 | 196 | #### addListenerOnce / once 197 | 198 | Method to subscribe to events, being sent from parent; emitted once and listener is removed after that 199 | 200 | ```js 201 | iframy.addListenerOnce('message-type', data => console.log(data)); 202 | 203 | // or use alias 204 | 205 | iframy.once('message-type', data => console.log(data)); 206 | ``` 207 | 208 | #### removeListener / off 209 | 210 | Method to remove specific listener from correspondent event type from parent 211 | 212 | ```js 213 | iframy.removeListener('message-type', listener); 214 | 215 | // or use alias 216 | 217 | iframy.off('message-type', listener); 218 | ``` 219 | 220 | #### removeAllListeners / offAll 221 | 222 | Method to remove all listeners from correspondent event type from parent 223 | 224 | ```js 225 | iframy.removeAllListeners('message-type'); 226 | 227 | // or use alias 228 | 229 | iframy.offAll('message-type'); 230 | ``` 231 | 232 | ## Examples 233 | 234 | Find example [here](https://github.com/yankouskia/iframy/tree/master/demo) 235 | 236 | ## Contributing 237 | 238 | `iframy` is open-source library, opened for contributions 239 | 240 | ### Tests 241 | 242 | **in progress** 243 | 244 | ### License 245 | 246 | iframy is [MIT licensed](https://github.com/yankouskia/iframy/blob/master/LICENSE) 247 | -------------------------------------------------------------------------------- /child.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).iframy={})}(this,(function(e){"use strict";function t(e,t,r,n,i,o,a){try{var s=e[o](a),u=s.value}catch(e){return void r(e)}s.done?t(u):Promise.resolve(u).then(n,i)}function r(e){return function(){var r=this,n=arguments;return new Promise((function(i,o){var a=e.apply(r,n);function s(e){t(a,i,o,s,u,"next",e)}function u(e){t(a,i,o,s,u,"throw",e)}s(void 0)}))}}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var r=0;r=0;--i){var o=this.tryEntries[i],a=o.completion;if("root"===o.tryLoc)return n("end");if(o.tryLoc<=this.prev){var s=r.call(o,"catchLoc"),u=r.call(o,"finallyLoc");if(s&&u){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&r.call(i,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),L(r),c}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var i=n.arg;L(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:k(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),c}},e}("object"==typeof module?module.exports:{});try{regeneratorRuntime=s}catch(e){Function("r","regeneratorRuntime = r")(s)}var u=function(e){var t=e.id,r=e.type,n=e.data,i=void 0===n?null:n,o=e.meta,a=void 0===o?null:o,s=e.name,u=void 0===s?null:s,c=e.uid,l=void 0===c?null:c;return JSON.stringify({id:t,data:i,meta:a,name:u,type:r,uid:l})},c=function(){function e(){n(this,e),a(this,"listeners",{})}return o(e,[{key:"on",value:function(e,t){this.listeners[e]||(this.listeners[e]=[]),this.listeners[e].push(t)}},{key:"once",value:function(e,t){var r=this;this.listeners[e]||(this.listeners[e]=[]);this.listeners[e].push((function n(i){t(i);for(var o=0;o0&&void 0!==arguments[0]?arguments[0]:{};n(this,e),a(this,"listenersBus",new c),a(this,"firstMessageData",{isReceived:!1,id:""}),a(this,"api",void 0),a(this,"uid",void 0),a(this,"props",void 0),a(this,"on",this.addListener.bind(this)),a(this,"once",this.addListenerOnce.bind(this)),a(this,"off",this.removeListener.bind(this)),a(this,"offAll",this.removeAllListeners.bind(this)),this.globalListener=this.globalListener.bind(this),this.uid=l(),this.api=t.api||{},window.addEventListener("message",this.globalListener)}var t,i,s;return o(e,[{key:"globalListener",value:(s=r(regeneratorRuntime.mark((function e(t){var r,n,i,o,a,s,u,c,l;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:e.prev=0,h=t.data,f=void 0,f=JSON.parse(h),r={id:f.id,data:f.data,meta:f.meta,name:f.name,type:f.type,uid:f.uid},e.next=8;break;case 4:return e.prev=4,e.t0=e.catch(0),console.warn("Message received, but was not parsed"),e.abrupt("return");case 8:if(i=(n=r).id,o=n.data,a=n.name,"@if-init-req"!==(s=n.type)){e.next=14;break}return u=o.props,this.props=u,this.firstMessageData={id:i,isReceived:!0},e.abrupt("return");case 14:if("@if-p-to-ch"===s&&this.listenersBus.emit(a,o),"@if-api-req"!==s){e.next=27;break}return e.prev=16,e.next=19,this.api[a](o);case 19:c=e.sent,this.sendMessage({id:i,data:c,name:a,type:"@if-api-res"}),e.next=27;break;case 23:e.prev=23,e.t1=e.catch(16),l=e.t1.message,this.sendMessage({id:i,meta:{error:"regular-error",message:l},name:a,type:"@if-api-res"});case 27:case"end":return e.stop()}var h,f}),e,this,[[0,4],[16,23]])}))),function(e){return s.apply(this,arguments)})},{key:"sendMessage",value:function(e){var t,r=e.id,n=e.data,i=e.name,o=e.type;try{t=u({id:r,data:n,name:i,type:o,uid:this.uid})}catch(e){console.warn("Message was not serialized successfully, please check the data you passed"),t=u({id:r,meta:{errorType:"coerce-error"},name:i,type:o,uid:this.uid})}window.parent.postMessage(t,"*")}},{key:"initialize",value:(i=r(regeneratorRuntime.mark((function e(){var t,r=this;return regeneratorRuntime.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t=function e(t){requestAnimationFrame((function(){if(r.firstMessageData.isReceived)return t();e(t)}))},e.next=3,new Promise((function(e){return t(e)}));case 3:this.sendMessage({data:Object.keys(this.api),id:this.firstMessageData.id,type:"@if-init-res"});case 4:case"end":return e.stop()}}),e,this)}))),function(){return i.apply(this,arguments)})},{key:"addListener",value:function(e,t){this.listenersBus.on(e,t)}},{key:"addListenerOnce",value:function(e,t){this.listenersBus.once(e,t)}},{key:"removeListener",value:function(e,t){this.listenersBus.off(e,t)}},{key:"removeAllListeners",value:function(e){this.listenersBus.offAll(e)}},{key:"emit",value:function(e,t){this.sendMessage({data:t,name:e,type:"@if-ch-to-p",uid:this.uid})}}],[{key:"create",value:(t=r(regeneratorRuntime.mark((function t(r){var n;return regeneratorRuntime.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=new e(r),t.next=3,n.initialize();case 3:return t.abrupt("return",n);case 4:case"end":return t.stop()}}),t)}))),function(e){return t.apply(this,arguments)})}]),e}();e.IFramyChild=h,Object.defineProperty(e,"__esModule",{value:!0})})); 2 | -------------------------------------------------------------------------------- /demo/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Communication example 8 | 9 | 10 | 11 |
12 |

Child window

13 |
14 | Send message to Parent 15 | 16 | 17 |
18 |
19 | API response (string to concat): 20 | 21 |
22 |
23 | Received message: 24 | 25 |
26 |
27 | 58 | 59 | 83 | 84 | -------------------------------------------------------------------------------- /demo/communication.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Communication example 8 | 9 | 10 | 11 |
12 |

Parent window

13 |
14 | Send message to Child 15 | 16 | 17 |
18 |
19 | Call API (concat strings) with payload: 20 | 21 | 22 | Response: 23 | 24 |
25 |
26 | Received message: 27 | 28 |
29 |
30 |
31 | 32 | 63 | 96 | 97 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: false, 3 | collectCoverageFrom: [ 4 | '**/src/**.{js}', 5 | ], 6 | coverageDirectory: './test-coverage', 7 | coverageReporters: ['json', 'html'], 8 | moduleFileExtensions: [ 9 | 'js', 10 | 'json', 11 | ], 12 | modulePaths: ['./src'], 13 | testMatch: ['/src/**/*.spec.js'], 14 | transform: { 15 | '^.+\\.(js|jsx)$': 'babel-jest', 16 | }, 17 | verbose: true, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iframy", 3 | "version": "0.2.0", 4 | "description": "Library for creating & controlling cross-domain components", 5 | "main": "parent.js", 6 | "repository": "git@github.com:yankouskia/iframy.git", 7 | "author": "yankouskia ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@babel/core": "^7.6.2", 11 | "@babel/plugin-proposal-class-properties": "^7.8.3", 12 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 13 | "@babel/preset-env": "^7.6.2", 14 | "@babel/preset-typescript": "^7.8.3", 15 | "babel-jest": "^24.9.0", 16 | "codecov": "^3.6.1", 17 | "jest": "^24.9.0", 18 | "regenerator-runtime": "^0.13.3", 19 | "rollup": "^1.21.4", 20 | "rollup-plugin-auto-external": "^2.0.0", 21 | "rollup-plugin-babel": "^4.3.3", 22 | "rollup-plugin-commonjs": "^10.1.0", 23 | "rollup-plugin-node-resolve": "^5.2.0", 24 | "rollup-plugin-replace": "^2.2.0", 25 | "rollup-plugin-terser": "^5.2.0", 26 | "typescript": "^3.7.4" 27 | }, 28 | "scripts": { 29 | "clean": "rm -rf parent.js && rm -rf child.js && rm -rf parent.js.map && rm -rf child.js.map", 30 | "build": "yarn clean && NODE_ENV=production rollup -c", 31 | "dev": "yarn clean && NODE_ENV=dev rollup -c", 32 | "test": "jest", 33 | "test:cover": "jest --coverage" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parent.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).iframy={})}(this,(function(t){"use strict";function e(t,e,r,n,i,o,a){try{var s=t[o](a),u=s.value}catch(t){return void r(t)}s.done?e(u):Promise.resolve(u).then(n,i)}function r(t){return function(){var r=this,n=arguments;return new Promise((function(i,o){var a=t.apply(r,n);function s(t){e(a,i,o,s,u,"next",t)}function u(t){e(a,i,o,s,u,"throw",t)}s(void 0)}))}}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var r=0;r=0;--i){var o=this.tryEntries[i],a=o.completion;if("root"===o.tryLoc)return n("end");if(o.tryLoc<=this.prev){var s=r.call(o,"catchLoc"),u=r.call(o,"finallyLoc");if(s&&u){if(this.prev=0;--n){var i=this.tryEntries[n];if(i.tryLoc<=this.prev&&r.call(i,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),x(r),c}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var i=n.arg;x(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:k(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),c}},t}("object"==typeof module?module.exports:{});try{regeneratorRuntime=s}catch(t){Function("r","regeneratorRuntime = r")(s)}var u=function(t){var e=t.id,r=t.type,n=t.data,i=void 0===n?null:n,o=t.meta,a=void 0===o?null:o,s=t.name,u=void 0===s?null:s,c=t.uid,h=void 0===c?null:c;return JSON.stringify({id:e,data:i,meta:a,name:u,type:r,uid:h})},c=function(){function t(){n(this,t),a(this,"listeners",{})}return o(t,[{key:"on",value:function(t,e){this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(e)}},{key:"once",value:function(t,e){var r=this;this.listeners[t]||(this.listeners[t]=[]);this.listeners[t].push((function n(i){e(i);for(var o=0;o { 22 | listener(data); 23 | 24 | for(let i = 0; i < this.listeners[type].length; i++) { 25 | if (this.listeners[type][i] === newListener) { 26 | this.listeners[type].splice(i, 1); 27 | } 28 | } 29 | }; 30 | 31 | this.listeners[type].push(newListener); 32 | } 33 | 34 | off(type: string, listener: Listener) { 35 | if (!this.listeners[type]) return; 36 | 37 | for(let i = 0; i < this.listeners[type].length; i++) { 38 | if (this.listeners[type][i] === listener) { 39 | this.listeners[type].splice(i, 1); 40 | } 41 | } 42 | } 43 | 44 | offAll(type: string) { 45 | this.listeners[type] = []; 46 | } 47 | 48 | emit(type: string, data: any) { 49 | if(this.listeners[type]) { 50 | this.listeners[type].forEach(l => l(data)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/IFramyChild.ts: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import { parseMessage, createMessage } from './helpers'; 3 | import { INIT_EVENT_TYPE_REQUEST, IFRAMY_ID_KEY, INIT_EVENT_TYPE_RESPONSE, EMIT_CHILD_TO_PARENT_EVENT, EMIT_PARENT_TO_CHILD_EVENT, CALL_API_REQUEST_EVENT, CALL_API_RESPONSE_EVENT } from './constants'; 4 | import { MessageData, Listener } from './types'; 5 | import { COERCING_ERROR, REGULAR_ERROR } from './error-types'; 6 | import { Bus } from './Bus'; 7 | 8 | type ApiListenersType = { 9 | [key: string]: (data?: any) => any, 10 | }; 11 | 12 | type InitData = { 13 | api?: ApiListenersType, 14 | }; 15 | 16 | const extractID = () => { 17 | const urlParams = new URLSearchParams(window.location.search); 18 | const id = urlParams.get(IFRAMY_ID_KEY); 19 | 20 | if (!id) throw new Error('Please recheck configuration. Child iframe should be opened automatically, no query params should be changed / removed manually'); 21 | return id; 22 | } 23 | 24 | export class IFramyChild { 25 | private listenersBus = new Bus(); 26 | private firstMessageData = { 27 | isReceived: false, 28 | id: '', 29 | }; 30 | private api: ApiListenersType; 31 | private uid: string; 32 | 33 | public props: any; 34 | 35 | private constructor(data: InitData = {}) { 36 | this.globalListener = this.globalListener.bind(this); 37 | this.uid = extractID(); 38 | this.api = data.api || {}; 39 | 40 | window.addEventListener('message', this.globalListener); 41 | } 42 | 43 | private async globalListener(event: MessageEvent) { 44 | let messageData: MessageData; 45 | 46 | try { 47 | messageData = parseMessage(event.data); 48 | } catch (e) { 49 | console.warn('Message received, but was not parsed'); 50 | return; 51 | } 52 | 53 | const { 54 | id, 55 | data, 56 | name, 57 | type, 58 | } = messageData; 59 | 60 | if (type === INIT_EVENT_TYPE_REQUEST) { 61 | const { props } = data; 62 | this.props = props; 63 | this.firstMessageData = { 64 | id, 65 | isReceived: true, 66 | }; 67 | 68 | return; 69 | } 70 | 71 | if (type === EMIT_PARENT_TO_CHILD_EVENT) { 72 | this.listenersBus.emit(name, data); 73 | } 74 | 75 | if (type === CALL_API_REQUEST_EVENT) { 76 | try { 77 | const result = await this.api[name](data); 78 | this.sendMessage({ 79 | id, 80 | data: result, 81 | name, 82 | type: CALL_API_RESPONSE_EVENT, 83 | }); 84 | } catch (e) { 85 | const { message } = e; 86 | 87 | this.sendMessage({ 88 | id, 89 | meta: { error: REGULAR_ERROR, message }, 90 | name, 91 | type: CALL_API_RESPONSE_EVENT, 92 | }); 93 | } 94 | } 95 | } 96 | 97 | private sendMessage({ 98 | id, 99 | data, 100 | name, 101 | type, 102 | }: MessageData) { 103 | let msg: string; 104 | 105 | try { 106 | msg = createMessage({ 107 | id, 108 | data, 109 | name, 110 | type, 111 | uid: this.uid, 112 | }); 113 | } catch (e) { 114 | console.warn('Message was not serialized successfully, please check the data you passed'); 115 | msg = createMessage({ 116 | id, 117 | meta: { 118 | errorType: COERCING_ERROR, 119 | }, 120 | name, 121 | type, 122 | uid: this.uid, 123 | }); 124 | } 125 | 126 | window.parent.postMessage(msg, '*'); 127 | } 128 | 129 | private async initialize() { 130 | const callback = (res: (value?: unknown) => void) => { 131 | requestAnimationFrame(() => { 132 | if (this.firstMessageData.isReceived) return res(); 133 | callback(res); 134 | }); 135 | }; 136 | 137 | await new Promise(res => callback(res)); 138 | 139 | this.sendMessage({ 140 | data: Object.keys(this.api), 141 | id: this.firstMessageData.id, 142 | type: INIT_EVENT_TYPE_RESPONSE, 143 | }); 144 | } 145 | 146 | public static async create(data: InitData) { 147 | const iframy = new IFramyChild(data); 148 | 149 | await iframy.initialize(); 150 | 151 | return iframy; 152 | } 153 | 154 | public addListener(type: string, listener: Listener) { 155 | this.listenersBus.on(type, listener); 156 | } 157 | 158 | public addListenerOnce(type: string, listener: Listener) { 159 | this.listenersBus.once(type, listener); 160 | } 161 | 162 | public removeListener(type: string, listener: Listener) { 163 | this.listenersBus.off(type, listener); 164 | } 165 | 166 | public removeAllListeners(type: string) { 167 | this.listenersBus.offAll(type); 168 | } 169 | 170 | public on = this.addListener.bind(this); 171 | public once = this.addListenerOnce.bind(this); 172 | public off = this.removeListener.bind(this); 173 | public offAll = this.removeAllListeners.bind(this); 174 | 175 | public emit(type: string, data: any) { 176 | this.sendMessage({ 177 | data, 178 | name: type, 179 | type: EMIT_CHILD_TO_PARENT_EVENT, 180 | uid: this.uid, 181 | }); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/IFramyParent.ts: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | import { parseMessage, createMessage } from './helpers'; 3 | import { IFRAMY_ID_KEY, INIT_EVENT_TYPE_RESPONSE, INIT_EVENT_TYPE_REQUEST, EMIT_PARENT_TO_CHILD_EVENT, EMIT_CHILD_TO_PARENT_EVENT, CALL_API_REQUEST_EVENT, CALL_API_RESPONSE_EVENT } from './constants'; 4 | import { MessageData, Listener } from './types'; 5 | import { Bus } from './Bus'; 6 | import { COERCING_ERROR, REGULAR_ERROR } from './error-types'; 7 | 8 | type Dimensions = { 9 | width?: string, 10 | height?: string, 11 | }; 12 | 13 | type InitData = { 14 | dimensions?: Dimensions, 15 | props?: any, 16 | scrolling?: boolean, 17 | url: string, 18 | }; 19 | 20 | export class IFramyParent { 21 | private internalBus = new Bus(); 22 | private listenersBus = new Bus(); 23 | private dimensions: Dimensions; 24 | private props: any; 25 | private scrolling: boolean; 26 | private url: string; 27 | private frame: HTMLIFrameElement; 28 | private uid: string; 29 | public API: { 30 | [key: string]: Listener, 31 | } = {}; 32 | 33 | private constructor(data: InitData) { 34 | const { 35 | dimensions = {}, 36 | props = {}, 37 | scrolling = false, 38 | url, 39 | } = data; 40 | 41 | this.dimensions = dimensions; 42 | this.props = props; 43 | this.scrolling = scrolling; 44 | this.url = url; 45 | 46 | this.uid = this.generateID(); 47 | 48 | this.frame = document.createElement('iframe'); 49 | this.frame.setAttribute('src', `${this.url}?${IFRAMY_ID_KEY}=${this.uid}`); 50 | this.dimensions.width && this.frame.setAttribute('width', this.dimensions.width); 51 | this.dimensions.height && this.frame.setAttribute('height', this.dimensions.height); 52 | this.frame.setAttribute('scrolling', this.scrolling ? 'yes' : 'no'); 53 | 54 | this.globalListener = this.globalListener.bind(this); 55 | window.addEventListener('message', this.globalListener); 56 | } 57 | 58 | private generateID() { 59 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 60 | } 61 | 62 | private async waitForMessage(type: string, id?: string): Promise { 63 | return new Promise(resolve => { 64 | const resolver = (data: any) => { 65 | const { id: responseID, uid } = data; 66 | if (uid !== this.uid) return; 67 | if (id && id !== responseID) return; 68 | 69 | this.internalBus.off(type, resolver); 70 | return resolve(data); 71 | }; 72 | 73 | this.internalBus.on(type, resolver); 74 | }); 75 | } 76 | 77 | private globalListener(event: MessageEvent) { 78 | let messageData: MessageData; 79 | 80 | try { 81 | messageData = parseMessage(event.data); 82 | } catch (e) { 83 | console.warn('Message received, but was not parsed'); 84 | return; 85 | } 86 | 87 | const { 88 | id, 89 | data, 90 | name, 91 | type, 92 | uid, 93 | } = messageData; 94 | 95 | if(uid !== this.uid) return; 96 | 97 | this.internalBus.emit(type, { 98 | id, 99 | data, 100 | name, 101 | type, 102 | uid, 103 | }); 104 | 105 | if (type === EMIT_CHILD_TO_PARENT_EVENT) { 106 | this.listenersBus.emit(name, data); 107 | } 108 | } 109 | 110 | private sendMessage({ 111 | id, 112 | data, 113 | name, 114 | type, 115 | }: MessageData) { 116 | let msg: string; 117 | 118 | try { 119 | msg = createMessage({ 120 | id, 121 | data, 122 | name, 123 | type, 124 | }); 125 | } catch (e) { 126 | console.warn('Message was not serialized successfully, please check the data you passed'); 127 | msg = createMessage({ 128 | id, 129 | meta: { 130 | errorType: COERCING_ERROR, 131 | }, 132 | name, 133 | type, 134 | }); 135 | } 136 | 137 | this.frame.contentWindow.postMessage(msg, '*'); 138 | } 139 | 140 | private exposeApi(apiNames: string[]) { 141 | for (let i = 0; i < apiNames.length; i++) { 142 | const name = apiNames[i]; 143 | const id = this.generateID(); 144 | 145 | const fn = async (data: any) => { 146 | this.sendMessage({ 147 | data, 148 | id, 149 | name, 150 | type: CALL_API_REQUEST_EVENT, 151 | }); 152 | 153 | const { data: response, meta = {} } = await this.waitForMessage(CALL_API_RESPONSE_EVENT, id); 154 | const { error } = meta; 155 | 156 | if (!error) return response; 157 | 158 | if (error === COERCING_ERROR) { 159 | throw new Error('Message was not serialized successfully in child component'); 160 | } 161 | 162 | if(error === REGULAR_ERROR) { 163 | throw new Error(meta.message || 'Error occured inside child compomnent'); 164 | } 165 | 166 | throw new Error('Unknown error. Please check method implementation in child component'); 167 | }; 168 | 169 | this.API[name] = fn; 170 | } 171 | } 172 | 173 | public static create(data: InitData) { 174 | return new IFramyParent(data); 175 | } 176 | 177 | public async render(selector: string|HTMLElement) { 178 | let element: HTMLElement; 179 | 180 | if (selector instanceof HTMLElement) { 181 | element = selector; 182 | } else { 183 | element = document.querySelector(selector); 184 | } 185 | 186 | if (!element) throw new Error('Parent element does not exist'); 187 | 188 | element.appendChild(this.frame); 189 | 190 | await new Promise(loadResolve => { 191 | const handler = () => { 192 | this.frame.removeEventListener('load', handler); 193 | loadResolve(); 194 | }; 195 | 196 | this.frame.addEventListener('load', handler); 197 | }); 198 | 199 | this.sendMessage({ 200 | data: { props: this.props }, 201 | type: INIT_EVENT_TYPE_REQUEST, 202 | }); 203 | 204 | const { data } = await this.waitForMessage(INIT_EVENT_TYPE_RESPONSE); 205 | this.exposeApi(data); 206 | 207 | return this; 208 | } 209 | 210 | public addListener(type: string, listener: Listener) { 211 | this.listenersBus.on(type, listener); 212 | } 213 | 214 | public addListenerOnce(type: string, listener: Listener) { 215 | this.listenersBus.once(type, listener); 216 | } 217 | 218 | public removeListener(type: string, listener: Listener) { 219 | this.listenersBus.off(type, listener); 220 | } 221 | 222 | public removeAllListeners(type: string) { 223 | this.listenersBus.offAll(type); 224 | } 225 | 226 | public on = this.addListener.bind(this); 227 | public once = this.addListenerOnce.bind(this); 228 | public off = this.removeListener.bind(this); 229 | public offAll = this.removeAllListeners.bind(this); 230 | 231 | public emit(type: string, data: any) { 232 | this.sendMessage({ 233 | data, 234 | name: type, 235 | type: EMIT_PARENT_TO_CHILD_EVENT, 236 | }); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/child.spec.js: -------------------------------------------------------------------------------- 1 | describe('child', () => { 2 | it('calcs sum of 1 and 1', () => { 3 | expect(1 + 1).toEqual(2); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const IFRAMY_ID_KEY = '@if-uid'; 2 | 3 | export const INIT_EVENT_TYPE_REQUEST = '@if-init-req'; 4 | export const INIT_EVENT_TYPE_RESPONSE = '@if-init-res'; 5 | 6 | export const EMIT_CHILD_TO_PARENT_EVENT = '@if-ch-to-p'; 7 | export const EMIT_PARENT_TO_CHILD_EVENT = '@if-p-to-ch'; 8 | 9 | export const CALL_API_REQUEST_EVENT = '@if-api-req'; 10 | export const CALL_API_RESPONSE_EVENT = '@if-api-res'; 11 | -------------------------------------------------------------------------------- /src/error-types.ts: -------------------------------------------------------------------------------- 1 | export const REGULAR_ERROR = 'regular-error'; 2 | export const COERCING_ERROR = 'coerce-error'; 3 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { MessageData } from './types'; 2 | 3 | /** 4 | * Method to create string message to pass via postMessage API 5 | * @param id uniq identifier for each message 6 | * @param type type for each message, used from limited enum 7 | * @param data payload to pass in message (could be return value for API, or argument in event) 8 | * @param meta meta info to pass specific info like errors to other window 9 | * @param name name of specific event or function name 10 | * @returns json view of message structure 11 | */ 12 | export const createMessage = ({ 13 | id, 14 | type, 15 | data = null, 16 | meta = null, 17 | name = null, 18 | uid = null, 19 | }: MessageData): string => JSON.stringify({ 20 | id, 21 | data, 22 | meta, 23 | name, 24 | type, 25 | uid, 26 | }); 27 | 28 | /** 29 | * Method to create string message to pass via postMessage API 30 | * @param msg json view of message structure 31 | * @returns object message structure 32 | */ 33 | export const parseMessage = (msg: string) => { 34 | const { 35 | id, 36 | data, 37 | meta, 38 | name, 39 | type, 40 | uid, 41 | } = JSON.parse(msg); 42 | 43 | return { 44 | id, 45 | data, 46 | meta, 47 | name, 48 | type, 49 | uid, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type MessageData = { 2 | type: string, 3 | id?: string, 4 | data?: any, 5 | meta?: any, 6 | name?: string, 7 | uid?: string, 8 | }; 9 | 10 | export type Listener = (data?: any) => any; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist", 5 | "module": "es6", 6 | "noImplicitAny": true, 7 | "outDir": "./dist", 8 | "target": "es5" 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules"] 12 | } 13 | --------------------------------------------------------------------------------