diff --git a/README.md b/README.md
index 94a1897..b396260 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
diff --git a/src/index.js b/src/index.js
index 42318d7..64bef15 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,6 +6,24 @@ 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) => {
+ const emitName = customEvents[eventName] || eventName;
+ 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 +31,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 +97,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 +180,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..d29ba26 100644
--- a/src/index.test.jsx
+++ b/src/index.test.jsx
@@ -132,6 +132,118 @@ describe('web components', () => {
});
});
+ describe('Custom Events', () => {
+ function DummyEvented({ onMyEvent, onMyEventSuccess, onMyEventFailed }) {
+ function clickHandler() {
+ onMyEvent('payload').then(onMyEventSuccess, onMyEventFailed);
+ }
+ return (
+
+ click
+
+ );
+ }
+ 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', async () => {
+ let done;
+ const promise = new Promise((resolve) => {
+ done = resolve;
+ });
+ 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();
+ return promise;
+ });
+ });
+
+ 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);
+
+ 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();
+ return promise;
+ });
+ });
+
+ 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);
+
+ 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();
+ return promise;
+ });
+ });
+
+ 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);
+
+ el.addEventListener(onMyEvent, (e) => {
+ assert.equal(e.detail.payload, 'payload');
+ done();
+ });
+
+ act(() => {
+ el.querySelector('button').click();
+ return promise;
+ });
+ });
+ });
+
function Foo({ text, children }) {
return (