├── .gitignore
├── .prettierrc
├── .stylelintrc
├── README.md
├── config
├── dev.ts
├── model.ts
├── prod.ts
└── stage.ts
├── development
├── index.ejs
└── webpack.config.js
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── components
│ ├── counter
│ │ ├── index.scss
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ └── messages.ts
│ ├── index.scss
│ ├── ip-check
│ │ ├── index.scss
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ └── messages.ts
│ └── seller-store-editor
│ │ ├── index.scss
│ │ ├── index.test.tsx
│ │ └── index.tsx
├── containers
│ ├── counter
│ │ ├── actions.ts
│ │ ├── index.tsx
│ │ ├── reducer.test.tsx
│ │ ├── reducer.ts
│ │ ├── selector.test.ts
│ │ └── selector.ts
│ ├── ip-check
│ │ ├── __snapshots__
│ │ │ └── index.test.tsx.snap
│ │ ├── actions.ts
│ │ ├── epics.test.ts
│ │ ├── epics.ts
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ ├── selector.test.ts
│ │ └── selector.ts
│ ├── language-provider
│ │ ├── actions.ts
│ │ ├── index.test.tsx
│ │ ├── index.tsx
│ │ ├── reducer.test.ts
│ │ ├── reducer.ts
│ │ ├── selector.test.ts
│ │ └── selector.ts
│ └── seller-store-editor
│ │ ├── __snapshots__
│ │ └── index.test.tsx.snap
│ │ ├── index.test.tsx
│ │ └── index.tsx
├── i18n.test.ts
├── i18n.ts
├── polyfill.ts
├── register.ts
├── services
│ ├── index.ts
│ └── ip-api
│ │ ├── config
│ │ └── ip-api-config.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ └── models
│ │ └── IPQueryResponse.ts
├── store
│ ├── index.ts
│ ├── root-action.ts
│ ├── root-epic.ts
│ ├── root-reducer.ts
│ ├── services.ts
│ ├── types.d.ts
│ └── utils.ts
├── translations
│ ├── de.json
│ └── tr.json
└── webcomponent.tsx
├── tsconfig.json
├── tslint.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # project based ignores
2 | dist/
3 |
4 | # Created by https://www.gitignore.io/api/vim,node,react,macos,intellij+all
5 | # Edit at https://www.gitignore.io/?templates=vim,node,react,macos,intellij+all
6 |
7 | ### Intellij+all ###
8 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
9 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
10 |
11 | # User-specific stuff
12 | .idea/**/workspace.xml
13 | .idea/**/tasks.xml
14 | .idea/**/usage.statistics.xml
15 | .idea/**/dictionaries
16 | .idea/**/shelf
17 |
18 | # Generated files
19 | .idea/**/contentModel.xml
20 |
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 |
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn. Uncomment if using
37 | # auto-import.
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # Crashlytics plugin (for Android Studio and IntelliJ)
66 | com_crashlytics_export_strings.xml
67 | crashlytics.properties
68 | crashlytics-build.properties
69 | fabric.properties
70 |
71 | # Editor-based Rest Client
72 | .idea/httpRequests
73 |
74 | # Android studio 3.1+ serialized cache file
75 | .idea/caches/build_file_checksums.ser
76 |
77 | ### Intellij+all Patch ###
78 | # Ignores the whole .idea folder and all .iml files
79 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
80 |
81 | .idea/
82 |
83 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
84 |
85 | *.iml
86 | modules.xml
87 | .idea/misc.xml
88 | *.ipr
89 |
90 | # Sonarlint plugin
91 | .idea/sonarlint
92 |
93 | ### macOS ###
94 | # General
95 | .DS_Store
96 | .AppleDouble
97 | .LSOverride
98 |
99 | # Icon must end with two \r
100 | Icon
101 |
102 | # Thumbnails
103 | ._*
104 |
105 | # Files that might appear in the root of a volume
106 | .DocumentRevisions-V100
107 | .fseventsd
108 | .Spotlight-V100
109 | .TemporaryItems
110 | .Trashes
111 | .VolumeIcon.icns
112 | .com.apple.timemachine.donotpresent
113 |
114 | # Directories potentially created on remote AFP share
115 | .AppleDB
116 | .AppleDesktop
117 | Network Trash Folder
118 | Temporary Items
119 | .apdisk
120 |
121 | ### Node ###
122 | # Logs
123 | logs
124 | *.log
125 | npm-debug.log*
126 | yarn-debug.log*
127 | yarn-error.log*
128 | lerna-debug.log*
129 |
130 | # Diagnostic reports (https://nodejs.org/api/report.html)
131 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
132 |
133 | # Runtime data
134 | pids
135 | *.pid
136 | *.seed
137 | *.pid.lock
138 |
139 | # Directory for instrumented libs generated by jscoverage/JSCover
140 | lib-cov
141 |
142 | # Coverage directory used by tools like istanbul
143 | coverage
144 | *.lcov
145 |
146 | # nyc test coverage
147 | .nyc_output
148 |
149 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
150 | .grunt
151 |
152 | # Bower dependency directory (https://bower.io/)
153 | bower_components
154 |
155 | # node-waf configuration
156 | .lock-wscript
157 |
158 | # Compiled binary addons (https://nodejs.org/api/addons.html)
159 | build/Release
160 |
161 | # Dependency directories
162 | node_modules/
163 | jspm_packages/
164 |
165 | # TypeScript v1 declaration files
166 | typings/
167 |
168 | # TypeScript cache
169 | *.tsbuildinfo
170 |
171 | # Optional npm cache directory
172 | .npm
173 |
174 | # Optional eslint cache
175 | .eslintcache
176 |
177 | # Optional REPL history
178 | .node_repl_history
179 |
180 | # Output of 'npm pack'
181 | *.tgz
182 |
183 | # Yarn Integrity file
184 | .yarn-integrity
185 |
186 | # dotenv environment variables file
187 | .env
188 | .env.test
189 |
190 | # parcel-bundler cache (https://parceljs.org/)
191 | .cache
192 |
193 | # next.js build output
194 | .next
195 |
196 | # nuxt.js build output
197 | .nuxt
198 |
199 | # react / gatsby
200 | public/
201 |
202 | # vuepress build output
203 | .vuepress/dist
204 |
205 | # Serverless directories
206 | .serverless/
207 |
208 | # FuseBox cache
209 | .fusebox/
210 |
211 | # DynamoDB Local files
212 | .dynamodb/
213 |
214 | ### react ###
215 | .DS_*
216 | **/*.backup.*
217 | **/*.back.*
218 |
219 | node_modules
220 | bower_componets
221 |
222 | *.sublime*
223 |
224 | psd
225 | thumb
226 | sketch
227 |
228 | ### Vim ###
229 | # Swap
230 | [._]*.s[a-v][a-z]
231 | [._]*.sw[a-p]
232 | [._]s[a-rt-v][a-z]
233 | [._]ss[a-gi-z]
234 | [._]sw[a-p]
235 |
236 | # Session
237 | Session.vim
238 | Sessionx.vim
239 |
240 | # Temporary
241 | .netrwhist
242 | *~
243 | # Auto-generated tag files
244 | tags
245 | # Persistent undo
246 | [._]*.un~
247 |
248 | # End of https://www.gitignore.io/api/vim,node,react,macos,intellij+all
249 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "semi": true
6 | }
7 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "stylelint-scss"
4 | ],
5 | "extends": ["stylelint-prettier/recommended"]
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-webcomponent-poc
2 | This repository demonstrates a simple way of setting up a SPA with React/Redux/RxJS/Typescript. Before adopting a different solution, we were using it to embed our React SPA into a Vue application.
3 |
4 | Web Components API was used instead of iframes to re-use existing functionality in the Vue panel with sync calls. When built, this SPA can be used as `` inside your HTML page. See [development/index.ejs](./development/index.ejs) for example.
5 |
6 | ## Summary
7 | Run `npm start` and go to the URL that is printed to the screen. You will see a page that prints your IP to the screen and refreshes it every 1 minute. This page demonstrates managing side effects with redux-observable. You can use the buttons on the page to change your locale. Or change to the other page where you will see a simple counter with redux implemented.
8 |
9 | ## Features
10 | - Shadow DOM for CSS encapsulation
11 | - Redux routing with react-router
12 | - Exposed as Custom Elements
13 | - Hook/FormattedMessage based localization
14 | - Redux with redux-observable for managing side effects
15 | - Unit tests for side-effects/pure components
16 | - Environment based configuration
17 | - Automatic linting with TSLint and Prettier
18 |
19 | ## Structure
20 | ```
21 | .
22 | ├── config # environment based configuration files
23 | ├── development # webpack config and html file for local development
24 | └── src
25 | ├── components # pure components with html/css
26 | ├── containers # components connected to redux
27 | ├── services # API, Utility services
28 | ├── store # redux store registration and root types
29 | ├── translations # translation files for different languages
30 | ├── polyfill.ts # polyfills for intl
31 | ├── webcomponent.tsx # rendering SPA to custom element in shadow DOM
32 | └── register.ts # entrypoint to expose the webcomponent
33 | ```
34 |
35 | ## Commands
36 | - `npm start` starts a local working environment with webpack-dev-server
37 | - `npm run build` builds and outputs JS files prepend with `PROFILE=dev` e.g. to build with different configs
38 | - `npm run test` runs all unit tests
39 | - `npm run format` formats project source code with tslint and prettier
40 |
--------------------------------------------------------------------------------
/config/dev.ts:
--------------------------------------------------------------------------------
1 | import { Config } from './model';
2 |
3 | export default {
4 | ip: {
5 | url: 'http://ip-api.com'
6 | }
7 | } as Config;
8 |
--------------------------------------------------------------------------------
/config/model.ts:
--------------------------------------------------------------------------------
1 | export type Config = {
2 | ip: {
3 | url: string;
4 | };
5 | };
6 |
--------------------------------------------------------------------------------
/config/prod.ts:
--------------------------------------------------------------------------------
1 | import { Config } from './model';
2 |
3 | export default {
4 | ip: {
5 | url: 'http://ip-api.com'
6 | }
7 | } as Config;
8 |
--------------------------------------------------------------------------------
/config/stage.ts:
--------------------------------------------------------------------------------
1 | import { Config } from './model';
2 |
3 | export default {
4 | ip: {
5 | url: 'http://ip-api.com'
6 | }
7 | } as Config;
8 |
--------------------------------------------------------------------------------
/development/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Seller Store Editor WebComponent
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/development/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpackConfig = require('../webpack.config');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const { join } = require('path');
4 |
5 | const HTML_TEMPLATE_PATH = join(__dirname, 'index.ejs');
6 |
7 | module.exports = {
8 | ...webpackConfig,
9 | mode: 'development',
10 | plugins: (webpackConfig.plugins || []).concat([new HtmlWebpackPlugin({ template: HTML_TEMPLATE_PATH })])
11 | };
12 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | clearMocks: true,
4 | coverageDirectory: 'coverage',
5 | collectCoverage: true,
6 | collectCoverageFrom: [
7 | 'src/**/*.{ts,tsx}',
8 | '!src/**/*.test.{ts,tsx}',
9 | '!src/register.ts',
10 | '!src/webcomponent.tsx',
11 | '!src/polyfill.ts',
12 | '!src/store/**',
13 | '!src/services/index.ts'
14 | ],
15 | testRegex: '.*\\.test\\.tsx?$',
16 | setupFiles: ['raf-polyfill']
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-webcomponent-poc",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "webpack-dev-server --config ./development/webpack.config.js",
8 | "build": "./node_modules/.bin/cross-env NODE_ENV=production ./node_modules/.bin/webpack --progress --hide-modules",
9 | "test": "jest",
10 | "lint": "tslint -p tsconfig.json -c tslint.json",
11 | "lint:fix": "npm run lint -- --fix",
12 | "prettier": "npm run prettier:write -- \"src/**/*.{ts,tsx,scss}\"",
13 | "prettier:write": "prettier --write --loglevel=error",
14 | "format": "npm run prettier && npm run lint:fix"
15 | },
16 | "devDependencies": {
17 | "@formatjs/intl-pluralrules": "^1.5.2",
18 | "@formatjs/intl-relativetimeformat": "^4.5.9",
19 | "@testing-library/react": "^9.4.0",
20 | "@types/history": "^4.7.5",
21 | "@types/jest": "^25.1.2",
22 | "@types/react": "^16.9.19",
23 | "@types/react-dom": "^16.9.5",
24 | "@types/react-intl": "^3.0.0",
25 | "@types/react-redux": "^7.1.7",
26 | "@types/react-router-dom": "^5.1.3",
27 | "@types/react-test-renderer": "^16.9.2",
28 | "@types/redux-mock-store": "^1.0.2",
29 | "@types/url-join": "^4.0.0",
30 | "axios": "^0.19.2",
31 | "connected-react-router": "^6.7.0",
32 | "cross-env": "^7.0.0",
33 | "css-loader": "^3.4.2",
34 | "history": "^4.10.1",
35 | "html-webpack-plugin": "^3.2.0",
36 | "husky": "^4.2.3",
37 | "jest": "^25.1.0",
38 | "lint-staged": "^10.0.7",
39 | "nock": "^11.8.2",
40 | "node-sass": "^4.13.1",
41 | "prettier": "^1.19.1",
42 | "raf-polyfill": "^1.0.0",
43 | "react": "^16.12.0",
44 | "react-dom": "^16.12.0",
45 | "react-intl": "^3.12.0",
46 | "react-redux": "^7.1.3",
47 | "react-router": "^5.1.2",
48 | "react-router-dom": "^5.1.2",
49 | "react-shadow": "^17.5.0",
50 | "react-test-renderer": "^16.12.0",
51 | "redux": "^4.0.5",
52 | "redux-mock-store": "^1.5.4",
53 | "redux-observable": "^1.2.0",
54 | "reselect": "^4.0.0",
55 | "rxjs": "^6.5.4",
56 | "sass-loader": "^8.0.2",
57 | "serve": "^11.3.0",
58 | "source-map-loader": "^0.2.4",
59 | "stylelint": "^13.2.0",
60 | "stylelint-config-prettier": "^8.0.1",
61 | "stylelint-prettier": "^1.1.2",
62 | "stylelint-scss": "^3.14.2",
63 | "to-string-loader": "^1.1.6",
64 | "ts-jest": "^25.2.0",
65 | "ts-loader": "^6.2.1",
66 | "ts-mockito": "^2.5.0",
67 | "tslint": "^6.0.0",
68 | "tslint-config-prettier": "^1.18.0",
69 | "typesafe-actions": "^5.1.0",
70 | "typescript": "^3.7.5",
71 | "url-join": "^4.0.1",
72 | "webpack": "^4.41.6",
73 | "webpack-cli": "^3.3.11",
74 | "webpack-dev-server": "^3.10.3"
75 | },
76 | "husky": {
77 | "hooks": {
78 | "pre-commit": "lint-staged"
79 | }
80 | },
81 | "lint-staged": {
82 | "src/**/*.{ts,tsx}": [
83 | "npm run prettier:write",
84 | "npm run lint:fix",
85 | "git add"
86 | ],
87 | "src/**/*.scss": [
88 | "npm run prettier:write",
89 | "git add"
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/counter/index.scss:
--------------------------------------------------------------------------------
1 | .counter {
2 | &__container {
3 | display: flex;
4 | width: 100%;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/counter/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentProps } from 'react';
2 | import { fireEvent, render } from '@testing-library/react';
3 | import Counter from './index';
4 | import IntlProvider from 'react-intl/dist/components/provider';
5 | import { StaticRouter as Router } from 'react-router-dom';
6 |
7 | const scope = `editor.components.counter`;
8 | const messages = {
9 | [`${scope}.helloText`]: 'hello-text',
10 | [`${scope}.switchButtonText`]: 'switch-button',
11 | [`${scope}.incrementButtonText`]: 'increment-button',
12 | [`${scope}.decrementButtonText`]: 'decrement-button',
13 | [`${scope}.setButtonText`]: `set-button`,
14 | [`${scope}.countText`]: 'count:{count}'
15 | };
16 |
17 | const renderComponent = (props: Partial> = {}) =>
18 | render(
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 | describe('', () => {
27 | it('should render four