diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..7216fd2146
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,43 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.0.1-beta",
+ "configurations": [
+ {
+ "name": "Launch Source",
+ "type": "firefox",
+ "request": "launch",
+ "reAttach": false,
+ "reloadOnChange": {
+ "watch": ["${workspaceFolder}/src/**/*.js", "${workspaceFolder}/**/*.html"],
+ "ignore": "**/node_modules/**"
+ },
+ "internalConsoleOptions": "openOnSessionStart",
+ "firefoxArgs": ["-devtools"],
+ "url": "${workspaceFolder}/index.html",
+ "webRoot": "${workspaceFolder}/src"
+ },
+ {
+ "name": "Launch Test",
+ "type": "firefox",
+ "request": "launch",
+ "reAttach": false,
+ "reloadOnChange": {
+ "watch": ["${workspaceFolder}/test/**/*.js", "${workspaceFolder}/test/**/*.html"],
+ "ignore": "**/node_modules/**"
+ },
+ "internalConsoleOptions": "openOnSessionStart",
+ "url": "${workspaceFolder}/test/index.html",
+ "webRoot": "${workspaceFolder}/test"
+ },
+ {
+ "name": "Launch TestChrome",
+ "type": "chrome",
+ "request": "launch",
+ "internalConsoleOptions": "openOnSessionStart",
+ "url": "http://localhost/${workspaceFolder}/test/index.html",
+ "webRoot": "${workspaceFolder}/test"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..7c049862ed
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "cSpell.words": [
+ ],
+ "editor.tabSize": 2,
+ "editor.detectIndentation": false
+}
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..90aa933923
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Silvan StrĂ¼bi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..c1fce1c5d6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "event-driven-web-components-realworld-example-app",
+ "version": "0.0.1-beta",
+ "description": "Exemplary real world application built with Vanilla JS Web Components in an Event Driven Architecture",
+ "main": "./src/index.html",
+ "scripts": {
+ "test": "standard --fix"
+ },
+ "author": "weedshaker@gmail.com",
+ "license": "MIT",
+ "devDependencies": {
+ "standard": "^14.3.3"
+ }
+}
diff --git a/test/es/Test.js b/test/es/Test.js
new file mode 100644
index 0000000000..a2a9a06bf0
--- /dev/null
+++ b/test/es/Test.js
@@ -0,0 +1,157 @@
+// @ts-check
+
+/* global customElements */
+/* global self */
+
+export default class Test {
+ /**
+ * Creates an instance of Test
+ * @param {string} name
+ * @param {string} [namespace = ''] // is very important if the same test runs more than once in the same session
+ * @memberof Test
+ */
+ constructor (name, namespace = '') {
+ this.namespace = namespace
+
+ this.summaries = document.createElement('div')
+ this.summaries.innerHTML = `${name} had test runs done from which passed and failed`
+ document.getElementById('summary').appendChild(this.summaries)
+ this.summarySpaceDone = this.summaries.getElementsByClassName('hasDone')[0]
+ this.summarySpacePassed = this.summaries.getElementsByClassName('hasPassed')[0]
+ this.summarySpaceFailed = this.summaries.getElementsByClassName('hasFailed')[0]
+ this.counter = 0
+ this.passedCounter = 0
+ this.failedCounter = 0
+
+ const results = document.createElement('div')
+ results.innerHTML = `
+
+
+ `
+ document.getElementById('results').appendChild(results)
+ document.getElementById('results').appendChild(document.createElement('hr'))
+ this.resultSpace = results.getElementsByClassName('result')[0]
+ this.testSpace = results.getElementsByClassName('test')[0]
+
+ self.onerror = (message, source, lineno, colno, error) => {
+ const errorEl = document.createElement('div')
+ errorEl.classList.add(errorEl.textContent = 'failed')
+ errorEl.textContent += `message: ${message}, source: ${source}, lineno: ${lineno}, colno: ${colno}, error: ${error}}`
+ this.summaries.appendChild(errorEl)
+ }
+ }
+
+ /**
+ * runs a web-component test by first importing the needed module, define it and test it
+ *
+ * @param {string} testName
+ * @param {string} [moduleName='default']
+ * @param {string} modulePath
+ * @param {(HTMLElement)=>boolean} testFunction
+ * @param {string} [attributes='']
+ * @param {(Function)=>Function} [extendsFunction=(Function)=>Function]
+ * @memberof Test
+ * @return {Promise}
+ */
+ runTest (testName, moduleName = 'default', modulePath, testFunction, attributes = '', extendsFunction = func => func) {
+ testName = this.namespace ? `${testName}-${this.namespace}` : testName
+ return import(modulePath).then(module => {
+ // test shadowRoot
+ try {
+ if (!customElements.get(testName)) customElements.define(testName, extendsFunction(!module[moduleName].toString().includes('=>') ? class extends module[moduleName] {} : module[moduleName]()))
+ } catch (error) {
+ console.error(`Note! testName: ${testName} must be lower case with hyphen separated!`, error)
+ }
+ return this.test(testName, testFunction, attributes, null)
+ })
+ }
+
+ /**
+ * test a web-component
+ *
+ * @param {string} testName
+ * @param {(HTMLElement)=>boolean} testFunction
+ * @param {string} [attributes='']
+ * @param {Element} [testEl = null]
+ * @param {boolean} hidden
+ * @return {Element | null}
+ */
+ test (testName, testFunction, attributes = '', testEl = null, hidden = false) {
+ if (!testEl) {
+ const container = document.createElement('div')
+ container.innerHTML = `<${testName} ${attributes}><${testName}>${testName}>`
+ testEl = container.getElementsByTagName(testName)[0]
+ this.testSpace.appendChild(testEl)
+ // @ts-ignore
+ testEl.hidden = hidden
+ } else {
+ const placeHolder = document.createElement('div')
+ placeHolder.textContent = `reused: <${testEl.tagName.toLowerCase()}> for ${testName} test`
+ placeHolder.classList.add('placeHolder')
+ this.testSpace.appendChild(placeHolder)
+ placeHolder.hidden = hidden
+ }
+ if (testEl) {
+ const resultEl = document.createElement('div')
+ if (testFunction(testEl)) {
+ resultEl.classList.add(resultEl.textContent = 'passed')
+ if (!hidden) this.passedCounter++
+ } else {
+ resultEl.classList.add(resultEl.textContent = 'failed')
+ if (!hidden) this.failedCounter++
+ }
+ testEl.className = ''
+ testEl.classList.add(resultEl.textContent)
+ resultEl.textContent += `: ${testName.replace(`-${this.namespace}`, '')}`
+ this.resultSpace.appendChild(resultEl)
+ resultEl.hidden = hidden
+ if (hidden) console.info(`${testFunction(testEl) ? 'passed' : 'failed'} hidden test: ${testName} on element: ${testEl.tagName.toLowerCase()}`)
+ }
+ if (!hidden) this.counter++
+ this.updateSummary()
+ return testEl
+ }
+
+ updateSummary () {
+ if (Number(this.summarySpaceFailed.textContent) > 0) this.summaries.classList.add('failed')
+ if (Number(this.summarySpacePassed.textContent) === Number(this.summarySpaceDone.textContent)) {
+ this.summaries.classList.add('passed')
+ } else {
+ this.summaries.classList.remove('passed')
+ }
+ }
+
+ set counter (number) {
+ this._counter = number
+ this.summarySpaceDone.textContent = number
+ }
+
+ get counter () {
+ return this._counter
+ }
+
+ set passedCounter (number) {
+ this._passedCounter = number
+ this.summarySpacePassed.textContent = number
+ }
+
+ get passedCounter () {
+ return this._passedCounter
+ }
+
+ set failedCounter (number) {
+ this._failedCounter = number
+ this.summarySpaceFailed.textContent = number
+ }
+
+ get failedCounter () {
+ return this._failedCounter
+ }
+}
diff --git a/test/es/tests/components/prototypes/MasterIntersectionObserver.js b/test/es/tests/components/prototypes/MasterIntersectionObserver.js
new file mode 100644
index 0000000000..ef9fa24479
--- /dev/null
+++ b/test/es/tests/components/prototypes/MasterIntersectionObserver.js
@@ -0,0 +1,52 @@
+// @ts-nocheck
+
+/* global CustomEvent */
+/* global self */
+
+import Test from '../../../Test.js'
+
+let counter = 0
+
+/**
+ * MasterIntersectionObserver Tests
+ *
+ * @param {string} testTitle
+ * @param {string} moduleName
+ * @param {string} modulePath
+ * @param {string} [namespace = '']
+ */
+export const test = (testTitle = 'MasterIntersectionObserver', moduleName = 'MasterIntersectionObserver', modulePath = '../../src/es/components/prototypes/MasterIntersectionObserver.js', namespace = counter) => {
+ // test modulePath must be from Test.js perspective
+ const test = new Test(testTitle, namespace)
+
+ // INTERSECTION -----------------------------------------------------------------------------------------------
+ let eventDispatched = false
+ let eventReceived = false
+ test.runTest('intersection-observer-setup', moduleName, modulePath,
+ el => el,
+ undefined,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(Object.assign(masterArgs, { intersectionObserverInit: {} }), ...args)
+ this.css = `this{
+ position: absolute;
+ top: 2000px;
+ }`
+ }
+ }
+ ).then(el => {
+ document.body.addEventListener('HelloFromComponent', event => (eventDispatched = true))
+ el.addCustomEventListener(document.body, 'HelloFromBody', event => (eventReceived = true))
+ el.dispatchCustomEvent(el.getCustomEvent('HelloFromComponent'))
+ document.body.dispatchEvent(new CustomEvent('HelloFromBody', { bubbles: true, cancelable: true, detail: null, composed: true }))
+ test.test('intersection-observer-not-dispatched-nor-received', el => !eventDispatched && !eventReceived, undefined, el)
+ self.scrollTo(0, document.body.scrollHeight + 200)
+ setTimeout(() => {
+ test.test('intersection-observer-dispatched-received', el => eventDispatched && eventReceived, undefined, el)
+ el.css = ''
+ self.scrollTo(0, 0)
+ }, 50)
+ })
+ // ------------------------------------------------------------------------------------------------------------
+ counter++
+}
diff --git a/test/es/tests/components/prototypes/MasterMutationObserver.js b/test/es/tests/components/prototypes/MasterMutationObserver.js
new file mode 100644
index 0000000000..e16cd5fd2b
--- /dev/null
+++ b/test/es/tests/components/prototypes/MasterMutationObserver.js
@@ -0,0 +1,86 @@
+// @ts-nocheck
+
+import Test from '../../../Test.js'
+
+let counter = 0
+
+/**
+ * MasterMutationObserver Tests
+ *
+ * @param {string} testTitle
+ * @param {string} moduleName
+ * @param {string} modulePath
+ * @param {string} [namespace = '']
+ */
+export const test = (testTitle = 'MasterMutationObserver', moduleName = 'MasterMutationObserver', modulePath = '../../src/es/components/prototypes/MasterMutationObserver.js', namespace = counter) => {
+ // test modulePath must be from Test.js perspective
+ const test = new Test(testTitle, namespace)
+
+ // MUTATION------------------------------------------------------------------------------------------------------
+ let gotAttributeMutation = false
+ let gotChildListMutation = false
+ test.runTest('mutation-observer-by-attribute-setup', moduleName, modulePath,
+ el => el,
+ `mutationObserverInit="{
+ 'attributes': true,
+ 'characterData': true,
+ 'childList': true
+ }"`,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(masterArgs, ...args)
+ }
+
+ connectedCallback () {
+ super.connectedCallback()
+ this.setAttribute('hello', 'world')
+ this.root.appendChild(document.createElement('span'))
+ }
+
+ mutationCallback (mutationList, observer) {
+ super.mutationCallback(mutationList, observer)
+ mutationList.forEach(mutation => {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'hello') gotAttributeMutation = true
+ if (mutation.type === 'childList') gotChildListMutation = true
+ })
+ }
+ }
+ ).then(el => {
+ test.test('mutation-observer-by-attribute', el => gotAttributeMutation && gotChildListMutation, undefined, el)
+ })
+ let gotAttributeMutation2 = false
+ let gotChildListMutation2 = false
+ test.runTest('mutation-observer-by-extends-setup', moduleName, modulePath,
+ el => el,
+ undefined,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(Object.assign(masterArgs, {
+ mutationObserverInit: {
+ attributes: true,
+ characterData: true,
+ childList: true
+ }
+ }), ...args)
+ }
+
+ connectedCallback () {
+ super.connectedCallback()
+ this.setAttribute('hello', 'world')
+ this.root.appendChild(document.createElement('span'))
+ }
+
+ mutationCallback (mutationList, observer) {
+ super.mutationCallback(mutationList, observer)
+ mutationList.forEach(mutation => {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'hello') gotAttributeMutation2 = true
+ if (mutation.type === 'childList') gotChildListMutation2 = true
+ })
+ }
+ }
+ ).then(el => {
+ test.test('mutation-observer-by-extends', el => gotAttributeMutation2 && gotChildListMutation2, undefined, el)
+ })
+ // ------------------------------------------------------------------------------------------------------------
+ counter++
+}
diff --git a/test/es/tests/components/prototypes/MasterShadow.js b/test/es/tests/components/prototypes/MasterShadow.js
new file mode 100644
index 0000000000..cf475a0c46
--- /dev/null
+++ b/test/es/tests/components/prototypes/MasterShadow.js
@@ -0,0 +1,152 @@
+// @ts-nocheck
+
+import Test from '../../../Test.js'
+
+let counter = 0
+
+/**
+ * MasterShadow Tests
+ *
+ * @param {string} testTitle
+ * @param {string} moduleName
+ * @param {string} modulePath
+ * @param {string} [namespace = '']
+ */
+export const test = (testTitle = 'MasterShadow', moduleName = 'MasterShadow', modulePath = '../../src/es/components/prototypes/MasterShadow.js', namespace = counter) => {
+ // test modulePath must be from Test.js perspective
+ const test = new Test(testTitle, namespace)
+
+ // MODE -------------------------------------------------------------------------------------------------------
+ // MODE: OPEN
+ const shadowRootOpenFunc = el => el.shadowRoot !== null && el.shadow !== null && el.shadow === el.shadowRoot && el.hasShadow && el.cssSelector === ':host'
+ test.runTest('shadow-root-by-attribute-open', moduleName, modulePath,
+ shadowRootOpenFunc,
+ 'mode=open'
+ )
+ test.runTest('shadow-root-by-extends-open', moduleName, modulePath,
+ shadowRootOpenFunc,
+ undefined,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(Object.assign(masterArgs, { mode: 'open' }), ...args)
+ }
+ }
+ )
+ // MODE: CLOSED
+ const shadowRootClosedFunc = el => el.shadowRoot === null && el.shadow !== null && el.shadow !== el.shadowRoot && el.hasShadow && el.cssSelector === ':host'
+ test.runTest('shadow-root-by-attribute-closed', moduleName, modulePath,
+ shadowRootClosedFunc,
+ 'mode=closed'
+ )
+ test.runTest('shadow-root-by-extends-closed', moduleName, modulePath,
+ shadowRootClosedFunc,
+ undefined,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(Object.assign(masterArgs, { mode: 'closed' }), ...args)
+ }
+ }
+ )
+ // MODE: FALSE
+ const shadowRootFalseFunc = el => el.shadowRoot === null && el.shadow === null && el.shadow === el.shadowRoot && !el.hasShadow && el.cssSelector.includes('shadow-root-'.toUpperCase())
+ test.runTest('shadow-root-by-attribute-false', moduleName, modulePath,
+ shadowRootFalseFunc,
+ 'mode=false'
+ )
+ test.runTest('shadow-root-by-extends-false', moduleName, modulePath,
+ shadowRootFalseFunc,
+ undefined,
+ (subclass) => class extends subclass {
+ constructor (masterArgs = {}, ...args) {
+ super(Object.assign(masterArgs, { mode: 'false' }), ...args)
+ }
+ }
+ )
+ // ------------------------------------------------------------------------------------------------------------
+ // CSS --------------------------------------------------------------------------------------------------------
+ test.runTest('shadow-root-css-setup', moduleName, modulePath,
+ el => el
+ ).then(el => {
+ el.css = `this {
+ height: 200px;
+ /* reset all css inherited from Test css */
+ border: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ }`
+ test.test('shadow-root-css-height', el => el.offsetHeight === 200, undefined, el)
+ el.css = `this {
+ width: 300px;
+ }`
+ test.test('shadow-root-css-width', el => el.offsetWidth === 300, undefined, el)
+ el.css = ''
+ test.test('shadow-root-css-cleanup1', el => el.root.textContent === ``, undefined, el)
+ // safari does not update the css on dom until the html got mutated, should not be an issue in production
+ // test.test('shadow-root-css-cleanup-safari', el => el.offsetHeight !== 200 && el.offsetWidth !== 300, undefined, el, true)
+ test.test('shadow-root-css-cleanup2', el => el.offsetHeight !== 200 && el.offsetWidth !== 300, undefined, el)
+ })
+ // ------------------------------------------------------------------------------------------------------------
+ // HTML -------------------------------------------------------------------------------------------------------
+ const htmlString = 'Hello World'
+ let oldInnerHTML = ''
+ test.runTest('shadow-root-html-setup', moduleName, modulePath,
+ el => {
+ oldInnerHTML = el.html
+ return true
+ }
+ ).then(el => {
+ el.html = ''
+ test.test('shadow-root-html-empty', el => el.root.innerHTML === '', undefined, el)
+ el.html = htmlString
+ test.test('shadow-root-html-string', el => el.root.innerHTML === htmlString, undefined, el)
+ el.html = ''
+ el.html = [document.createElement('span'), document.createElement('span')]
+ test.test('shadow-root-html-collection', el => el.root.innerHTML === '', undefined, el)
+ el.html = oldInnerHTML
+ // IMPORT -----------------------------------------------------------------------------------------------------
+ el.constructor.importCustomElementsAndDefine([
+ {
+ path: '../../components/prototypes/MasterShadow.js',
+ name: 'shadow-root-import',
+ moduleName: 'MasterShadow'
+ }
+ ])[0].then(elements => elements.forEach(element => test.test('shadow-root-import', el => el.nodeName === 'shadow-root-import'.toUpperCase(), undefined, element)))
+ // ------------------------------------------------------------------------------------------------------------
+ })
+ // ------------------------------------------------------------------------------------------------------------
+ // timeout ----------------------------------------------------------------------------------------------------
+ test.runTest('shadow-root-add-event-listener-setup', moduleName, modulePath,
+ el => el
+ ).then(el => {
+ // 'shadow-root-add-event-listener-initial-timeout'
+ let container1 = []
+ el.addEventListenerInitialTimeout(document.body, 'TestListenerEvent1', (event, events) => {
+ container1 = [event, events]
+ }, undefined, 100)
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent1'))
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent1'))
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent1'))
+ setTimeout(() => {
+ test.test('shadow-root-add-event-listener-initial-timeout1', () => !container1[0] && !container1[1], undefined, el)
+ }, 10)
+ setTimeout(() => {
+ test.test('shadow-root-add-event-listener-initial-timeout2', () => container1[0] && container1[1].length === 3, undefined, el)
+ }, 120)
+ // 'shadow-root-add-event-listener-initial-once'
+ let container2 = []
+ el.addEventListenerInitialOnce(document.body, 'TestListenerEvent2', (event, events) => {
+ container2 = [event, events]
+ }, undefined, 100)
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent2'))
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent2'))
+ document.body.dispatchEvent(el.getCustomEvent('TestListenerEvent2'))
+ setTimeout(() => {
+ test.test('shadow-root-add-event-listener-initial-once1', () => container2[0] && !container2[1], undefined, el)
+ }, 10)
+ setTimeout(() => {
+ test.test('shadow-root-add-event-listener-initial-once2', () => container2[0] && container2[1].length === 2, undefined, el)
+ }, 120)
+ })
+ // ------------------------------------------------------------------------------------------------------------
+ counter++
+}
diff --git a/test/index.html b/test/index.html
new file mode 100644
index 0000000000..3de0f33e02
--- /dev/null
+++ b/test/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+ Tests
+
+
+
+
+ Summary:
+
+
+
+
+
\ No newline at end of file