From 8823c680ed20065794a6d4a97b40cd51c38877dc Mon Sep 17 00:00:00 2001 From: Ben Sparks Date: Thu, 15 Oct 2020 17:04:01 -0700 Subject: [PATCH 1/4] add custom event functionality and some tests --- src/index.js | 40 +++++++++++++++++++++++++- src/index.test.jsx | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 42318d7..099f4af 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,25 @@ export default function register(Component, tagName, propNames, options) { inst._vdomComponent = Component; inst._root = options && options.shadow ? inst.attachShadow({ mode: 'open' }) : inst; + + inst._customEvents = {}; + const customEvents = + options && (options.customEvents || Component.customEvents); + if (customEvents) { + Object.keys(customEvents).forEach((eventName) => { + // external event name can be provided, defaults to convention of "(on)EventName" + const emitName = customEvents[eventName] || eventName.slice(2); + const handler = (payload) => inst.dispatch(emitName, payload); + // later to propagate to props + inst._customEvents[eventName] = handler; + Object.defineProperty(inst, eventName, { + get() { + return handler; + }, + }); + }); + } + return inst; } PreactElement.prototype = Object.create(HTMLElement.prototype); @@ -13,6 +32,7 @@ export default function register(Component, tagName, propNames, options) { PreactElement.prototype.connectedCallback = connectedCallback; PreactElement.prototype.attributeChangedCallback = attributeChangedCallback; PreactElement.prototype.disconnectedCallback = disconnectedCallback; + PreactElement.prototype.dispatch = dispatch; propNames = propNames || @@ -78,7 +98,7 @@ function connectedCallback() { this._vdom = h( ContextProvider, - { ...this._props, context }, + { ...this._props, ...this._customEvents, context }, toVdom(this, this._vdomComponent) ); (this.hasAttribute('hydrate') ? hydrate : render)(this._vdom, this._root); @@ -161,3 +181,21 @@ function toVdom(element, nodeName) { const wrappedChildren = nodeName ? h(Slot, null, children) : children; return h(nodeName || element.nodeName.toLowerCase(), props, wrappedChildren); } + +function dispatch(eventName, payload) { + return new Promise((resolve, reject) => { + const callback = (result, error) => { + if (error !== undefined) { + reject(error); + return; + } + resolve(result); + }; + this.dispatchEvent( + new CustomEvent(eventName, { + bubbles: true, + detail: { callback, payload }, + }) + ); + }); +} diff --git a/src/index.test.jsx b/src/index.test.jsx index 00da646..1308cda 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -132,6 +132,77 @@ describe('web components', () => { }); }); + describe('DOM Events', () => { + function DummyEvented({ onMyEvent, onMyEventSuccess, onMyEventFailed }) { + const clickHandler = () => { + onMyEvent('payload').then(onMyEventSuccess, onMyEventFailed); + }; + return ( +
+ +
+ ); + } + const onMyEvent = 'myEvent'; + const onMyEventSuccess = 'myEventSuccess'; + const onMyEventFailed = 'myEventFailed'; + registerElement(DummyEvented, 'x-dummy-evented', [], { + customEvents: { onMyEvent, onMyEventSuccess, onMyEventFailed }, + }); + + it('should allow you to expose custom events', (done) => { + const el = document.createElement('x-dummy-evented'); + root.appendChild(el); + + el.addEventListener(onMyEvent, (e) => { + assert.equal(e.detail.payload, 'payload'); + done(); + }); + + act(() => { + el.querySelector('button').click(); + }); + }); + + it('should enable async events (resolved)', (done) => { + const el = document.createElement('x-dummy-evented'); + root.appendChild(el); + + el.addEventListener(onMyEvent, (e) => { + const callback = e.detail.callback; + callback('success'); + }); + + el.addEventListener(onMyEventSuccess, (e) => { + assert.equal(e.detail.payload, 'success'); + done(); + }); + + act(() => { + el.querySelector('button').click(); + }); + }); + + it('should enable async events (rejected)', (done) => { + const el = document.createElement('x-dummy-evented'); + root.appendChild(el); + + el.addEventListener(onMyEvent, (e) => { + const callback = e.detail.callback; + callback(null, 'failed!'); + }); + + el.addEventListener(onMyEventFailed, (e) => { + assert.equal(e.detail.payload, 'failed!'); + done(); + }); + + act(() => { + el.querySelector('button').click(); + }); + }); + }); + function Foo({ text, children }) { return ( From 7e444e6c97793f07ce66c06435c75217c8290eab Mon Sep 17 00:00:00 2001 From: Ben Sparks Date: Fri, 16 Oct 2020 08:19:25 -0700 Subject: [PATCH 2/4] test static property option --- src/index.js | 5 ++--- src/index.test.jsx | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 099f4af..64bef15 100644 --- a/src/index.js +++ b/src/index.js @@ -9,11 +9,10 @@ export default function register(Component, tagName, propNames, options) { inst._customEvents = {}; const customEvents = - options && (options.customEvents || Component.customEvents); + (options && options.customEvents) || Component.customEvents; if (customEvents) { Object.keys(customEvents).forEach((eventName) => { - // external event name can be provided, defaults to convention of "(on)EventName" - const emitName = customEvents[eventName] || eventName.slice(2); + const emitName = customEvents[eventName] || eventName; const handler = (payload) => inst.dispatch(emitName, payload); // later to propagate to props inst._customEvents[eventName] = handler; diff --git a/src/index.test.jsx b/src/index.test.jsx index 1308cda..b3fc182 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -132,7 +132,7 @@ describe('web components', () => { }); }); - describe('DOM Events', () => { + describe('Custom Events', () => { function DummyEvented({ onMyEvent, onMyEventSuccess, onMyEventFailed }) { const clickHandler = () => { onMyEvent('payload').then(onMyEventSuccess, onMyEventFailed); @@ -146,10 +146,17 @@ describe('web components', () => { const onMyEvent = 'myEvent'; const onMyEventSuccess = 'myEventSuccess'; const onMyEventFailed = 'myEventFailed'; + DummyEvented.customEvents = { + onMyEvent, + onMyEventSuccess, + onMyEventFailed, + }; registerElement(DummyEvented, 'x-dummy-evented', [], { customEvents: { onMyEvent, onMyEventSuccess, onMyEventFailed }, }); + registerElement(DummyEvented, 'x-dummy-evented1'); + it('should allow you to expose custom events', (done) => { const el = document.createElement('x-dummy-evented'); root.appendChild(el); @@ -201,6 +208,20 @@ describe('web components', () => { el.querySelector('button').click(); }); }); + + it('should allow you to expose custom events via the static property', (done) => { + const el = document.createElement('x-dummy-evented1'); + root.appendChild(el); + + el.addEventListener(onMyEvent, (e) => { + assert.equal(e.detail.payload, 'payload'); + done(); + }); + + act(() => { + el.querySelector('button').click(); + }); + }); }); function Foo({ text, children }) { From 5b5a55f2618dea7e0a4ef32627655683f9b10b9f Mon Sep 17 00:00:00 2001 From: Ben Sparks Date: Fri, 16 Oct 2020 08:56:07 -0700 Subject: [PATCH 3/4] update documentation --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 660e04b..dbaa21b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,60 @@ FullName.propTypes = { register(FullName, 'full-name'); ``` +### Custom Events + +If you want to be able to emit custom events from your web component then you can add a `customEvents` object to the options. +Alternatively they can be supplied on the component. The events will be added to the props. They are async (Promise) methods +that the outside can respond to via a callback. Whatever you pass in to the method will be the `payload` in the event detail. + +```js +function MyAsyncComponent({ onError, onLoaded, src }) { + const [posts, setPosts] = useState(null); + + useEffect(() => { + if (!src) return; + axios.get(src).then(res => { + setPosts(res.data); + onLoaded(`Loaded ${res.data.length} posts`) + .then(res => { + console.log('got ack from host, do something...', res); + }); + }, onError); + }, [src]); + + return ( +
+ { posts ? + posts.map(post => ) : + Loading... + } +
+ ); +} +register(MyAsyncComponent, 'x-my-async', ['src'], { + customEvents: { onLoaded: 'loaded', onError: 'error' } +}); +``` + +Later in the consuming HTML page: + +```html + + +``` ## Related From 9d05052bb477eadfc3df243a8e3800c988906d6f Mon Sep 17 00:00:00 2001 From: Ben Sparks Date: Fri, 16 Oct 2020 09:12:41 -0700 Subject: [PATCH 4/4] fixed lint, all tests passed --- src/index.test.jsx | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/index.test.jsx b/src/index.test.jsx index b3fc182..d29ba26 100644 --- a/src/index.test.jsx +++ b/src/index.test.jsx @@ -134,9 +134,9 @@ describe('web components', () => { describe('Custom Events', () => { function DummyEvented({ onMyEvent, onMyEventSuccess, onMyEventFailed }) { - const clickHandler = () => { + function clickHandler() { onMyEvent('payload').then(onMyEventSuccess, onMyEventFailed); - }; + } return (
@@ -157,7 +157,11 @@ describe('web components', () => { registerElement(DummyEvented, 'x-dummy-evented1'); - it('should allow you to expose custom events', (done) => { + it('should allow you to expose custom events', async () => { + let done; + const promise = new Promise((resolve) => { + done = resolve; + }); const el = document.createElement('x-dummy-evented'); root.appendChild(el); @@ -168,10 +172,15 @@ describe('web components', () => { act(() => { el.querySelector('button').click(); + return promise; }); }); - it('should enable async events (resolved)', (done) => { + it('should enable async events (resolved)', async () => { + let done; + const promise = new Promise((resolve) => { + done = resolve; + }); const el = document.createElement('x-dummy-evented'); root.appendChild(el); @@ -187,10 +196,15 @@ describe('web components', () => { act(() => { el.querySelector('button').click(); + return promise; }); }); - it('should enable async events (rejected)', (done) => { + it('should enable async events (rejected)', async () => { + let done; + const promise = new Promise((resolve) => { + done = resolve; + }); const el = document.createElement('x-dummy-evented'); root.appendChild(el); @@ -206,10 +220,15 @@ describe('web components', () => { act(() => { el.querySelector('button').click(); + return promise; }); }); - it('should allow you to expose custom events via the static property', (done) => { + it('should allow you to expose custom events via the static property', async () => { + let done; + const promise = new Promise((resolve) => { + done = resolve; + }); const el = document.createElement('x-dummy-evented1'); root.appendChild(el); @@ -220,6 +239,7 @@ describe('web components', () => { act(() => { el.querySelector('button').click(); + return promise; }); }); });