From 5198d608074c598621695a72b4618492540412a4 Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Mon, 1 Mar 2021 21:05:16 +0100 Subject: [PATCH 1/6] provide useSocket() function This can be used in Vue's setup() function to inject the socket instance where $socket is not available: setup() { const socket = useSocket() } --- src/index.js | 3 ++- src/plugin.js | 11 +++++++++-- types/index.d.ts | 14 +++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index dae05332..bbd9ac1f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ -import * as plugin from './plugin'; +import plugin, { useSocket, SocketExtensionKey } from './plugin'; export default plugin; +export { useSocket, SocketExtensionKey }; diff --git a/src/plugin.js b/src/plugin.js index f84525c3..39fed2d8 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { ref } from 'vue'; +import { ref, inject } from 'vue'; import Observe from './Observe'; import GlobalEmitter from './GlobalEmitter'; import createMixin from './createMixin'; @@ -53,6 +53,8 @@ function defineSocketIoClient(socket, obj) { }); } +const SocketExtensionKey = Symbol('$socket'); + function install(app, socket, options) { if (!isSocketIo(socket)) { throw new Error('[vue-socket.io-ext] you have to pass `socket.io-client` instance to the plugin'); @@ -67,6 +69,11 @@ function install(app, socket, options) { app.config.optionMergeStrategies.sockets = (toVal, fromVal) => ({ ...toVal, ...fromVal }); Observe(socket, options); app.mixin(createMixin(GlobalEmitter)); + app.provide(SocketExtensionKey, $socket); } -export { defaults, install }; +const useSocket = () => inject(SocketExtensionKey); + +export default { defaults, install }; + +export { useSocket, SocketExtensionKey }; diff --git a/types/index.d.ts b/types/index.d.ts index 12698862..c2d1dc0f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { PluginInstallFunction } from 'vue'; +import { InjectionKey, PluginInstallFunction } from 'vue'; import { VueDecorator } from 'vue-class-component'; import * as SocketIOClient from 'socket.io-client'; // augment typings of Vue.js @@ -23,3 +23,15 @@ declare class VueSocketIOExt { export default VueSocketIOExt; export const Socket: (eventName?: string) => VueDecorator; + +export interface SocketExtension { + client: SocketIOClient.Socket; + $subscribe: (event: string, fn: Function) => void; + $unsubscribe: (event: string) => void; + connected: boolean; + disconnected: boolean; +} + +export declare const SocketExtensionKey: InjectionKey +export declare const useSocket: () => SocketExtension + From 126e8f3ebf030694a8b6daf1daf09de5a4ed50b3 Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Mon, 1 Mar 2021 22:06:04 +0100 Subject: [PATCH 2/6] Make tests pass with new export signature The plugin now provides a default and named exports. So the tests now must explicitly test the default export. --- src/__tests__/plugin.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/plugin.spec.js b/src/__tests__/plugin.spec.js index 1828f1cb..be4d21b2 100644 --- a/src/__tests__/plugin.spec.js +++ b/src/__tests__/plugin.spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import { createApp } from 'vue'; -import * as Main from '../plugin'; +import Main from '../plugin'; import io from '../__mocks__/socket.io-client'; it('should be vue plugin (is an object with `install` method)', () => { From 3d71cc3f006908c7401f899645717800a71bf2ef Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Mon, 1 Mar 2021 22:11:27 +0100 Subject: [PATCH 3/6] Add unit tests fo $socket injection --- src/__tests__/plugin.spec.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/__tests__/plugin.spec.js b/src/__tests__/plugin.spec.js index be4d21b2..5ff4ba52 100644 --- a/src/__tests__/plugin.spec.js +++ b/src/__tests__/plugin.spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; -import { createApp } from 'vue'; -import Main from '../plugin'; +import { createApp, inject } from 'vue'; +import { SocketExtensionKey } from '..'; +import Main, { useSocket } from '../plugin'; import io from '../__mocks__/socket.io-client'; it('should be vue plugin (is an object with `install` method)', () => { @@ -96,6 +97,28 @@ describe('.install()', () => { componentListener, }); }); + + it('provides $socket to be injectable via the SocketExtensionKey symbol within setup()', () => { + let injectedSocketExtension; + const wrapper = mount({ + render: () => null, + setup() { + injectedSocketExtension = inject(SocketExtensionKey); + }, + }, { global: { plugins: [[Main, io('ws://localhost')]] } }); + expect(injectedSocketExtension).toBe(wrapper.vm.$socket); + }); + + it('provides a useSocket() helper function to inject $socket', () => { + let injectedSocketExtension; + const wrapper = mount({ + render: () => null, + setup() { + injectedSocketExtension = useSocket(); + }, + }, { global: { plugins: [[Main, io('ws://localhost')]] } }); + expect(injectedSocketExtension).toBe(wrapper.vm.$socket); + }); }); describe('.defaults', () => { From f58493941d1423aa98857cf629d4a43178eaad78 Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Tue, 2 Mar 2021 19:10:43 +0100 Subject: [PATCH 4/6] Add composable onSocketEvent() This will subscribe to a socket.io event before the component is mounted and will unsubscribe before the component is unmounted. Usage: setup() { onSocketEvent('my-event', (data) => { ... }) } --- src/__tests__/composables.spec.js | 44 +++++++++++++++++++++++++++++++ src/__tests__/plugin.spec.js | 13 +-------- src/composables.js | 14 ++++++++++ src/index.js | 4 +-- src/plugin.js | 9 ++----- types/index.d.ts | 2 +- 6 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 src/__tests__/composables.spec.js create mode 100644 src/composables.js diff --git a/src/__tests__/composables.spec.js b/src/__tests__/composables.spec.js new file mode 100644 index 00000000..ededd0b8 --- /dev/null +++ b/src/__tests__/composables.spec.js @@ -0,0 +1,44 @@ +import { mount } from '@vue/test-utils'; +import { useSocket, onSocketEvent } from '../composables'; +import Main from '../plugin'; +import io from '../__mocks__/socket.io-client'; + +it('useSocket() injects $socket', () => { + let injectedSocketExtension; + const wrapper = mount({ + render: () => null, + setup() { + injectedSocketExtension = useSocket(); + }, + }, { global: { plugins: [[Main, io('ws://localhost')]] } }); + expect(injectedSocketExtension).toBe(wrapper.vm.$socket); +}); + +describe('onSocketEvent()', () => { + it('subscribes to the event', () => { + const spy = jest.fn(); + const socket = io('ws://localhost'); + mount({ + render: () => null, + setup() { + onSocketEvent('event', spy); + }, + }, { global: { plugins: [[Main, socket]] } }); + socket.fireServerEvent('event', 'data'); + expect(spy).toHaveBeenCalledWith('data'); + }); + + it('unsubscribes before unmounted', () => { + const spy = jest.fn(); + const socket = io('ws://localhost'); + const wrapper = mount({ + render: () => null, + setup() { + onSocketEvent('event', spy); + }, + }, { global: { plugins: [[Main, socket]] } }); + wrapper.unmount(); + socket.fireServerEvent('event', 'data'); + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/plugin.spec.js b/src/__tests__/plugin.spec.js index 5ff4ba52..f1fc8dba 100644 --- a/src/__tests__/plugin.spec.js +++ b/src/__tests__/plugin.spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import { createApp, inject } from 'vue'; import { SocketExtensionKey } from '..'; -import Main, { useSocket } from '../plugin'; +import Main from '../plugin'; import io from '../__mocks__/socket.io-client'; it('should be vue plugin (is an object with `install` method)', () => { @@ -108,17 +108,6 @@ describe('.install()', () => { }, { global: { plugins: [[Main, io('ws://localhost')]] } }); expect(injectedSocketExtension).toBe(wrapper.vm.$socket); }); - - it('provides a useSocket() helper function to inject $socket', () => { - let injectedSocketExtension; - const wrapper = mount({ - render: () => null, - setup() { - injectedSocketExtension = useSocket(); - }, - }, { global: { plugins: [[Main, io('ws://localhost')]] } }); - expect(injectedSocketExtension).toBe(wrapper.vm.$socket); - }); }); describe('.defaults', () => { diff --git a/src/composables.js b/src/composables.js new file mode 100644 index 00000000..e483934f --- /dev/null +++ b/src/composables.js @@ -0,0 +1,14 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { inject, onBeforeMount, onBeforeUnmount } from 'vue'; + +const SocketExtensionKey = Symbol('$socket'); + +const useSocket = () => inject(SocketExtensionKey); + +function onSocketEvent(event, callback) { + const socket = useSocket(); + onBeforeMount(() => socket.$subscribe(event, callback)); + onBeforeUnmount(() => socket.$unsubscribe(event)); +} + +export { SocketExtensionKey, useSocket, onSocketEvent }; diff --git a/src/index.js b/src/index.js index bbd9ac1f..5a0c909c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import plugin, { useSocket, SocketExtensionKey } from './plugin'; +import plugin from './plugin'; export default plugin; -export { useSocket, SocketExtensionKey }; +export { SocketExtensionKey, useSocket, onSocketEvent } from './composables'; diff --git a/src/plugin.js b/src/plugin.js index 39fed2d8..a4a7728b 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -1,10 +1,11 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { ref, inject } from 'vue'; +import { ref } from 'vue'; import Observe from './Observe'; import GlobalEmitter from './GlobalEmitter'; import createMixin from './createMixin'; import { isSocketIo } from './utils'; import defaults from './defaults'; +import { SocketExtensionKey } from './composables'; /** * @param {Vue} app @@ -53,8 +54,6 @@ function defineSocketIoClient(socket, obj) { }); } -const SocketExtensionKey = Symbol('$socket'); - function install(app, socket, options) { if (!isSocketIo(socket)) { throw new Error('[vue-socket.io-ext] you have to pass `socket.io-client` instance to the plugin'); @@ -72,8 +71,4 @@ function install(app, socket, options) { app.provide(SocketExtensionKey, $socket); } -const useSocket = () => inject(SocketExtensionKey); - export default { defaults, install }; - -export { useSocket, SocketExtensionKey }; diff --git a/types/index.d.ts b/types/index.d.ts index c2d1dc0f..0bee048a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -34,4 +34,4 @@ export interface SocketExtension { export declare const SocketExtensionKey: InjectionKey export declare const useSocket: () => SocketExtension - +export declare const onSocketEvent: (event: string, fn: Function) => void From 472ee65670318d5f7e7c358c0322605b14fb6939 Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Tue, 2 Mar 2021 22:23:42 +0100 Subject: [PATCH 5/6] Add documentation for composition API --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83b55f1e..4541252e 100755 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ app.use(VueSocketIOExt, socket); ## :rocket: Usage -#### On Vue.js component +#### On Vue.js component using the options API Define your listeners under `sockets` section, and they will be executed on corresponding `socket.io` events automatically. @@ -119,6 +119,39 @@ createApp({ **Note**: Don't use arrow functions for methods or listeners if you are going to emit `socket.io` events inside. You will end up with using incorrect `this`. More info about this [here](https://github.com/probil/vue-socket.io-extended/issues/61) +#### On Vue.js component using the Composition API + +When using the `setup` option of the Composition API, `this` is not available. In order to use socket.io, two composables can be used. `useSocket()` gives you the same object as `this.$socket` would be. With `onSocketEvent(event, callback)` you can subscribe to a `socket.io` event. Subscription will happen before the component is mounted and the component will automatically unsubscribe right before it is unmounted. + +```js +import { useSocket, onSocketEvent } from 'vue-socket.io-extended' + +defineComponent({ + setup() { + const socket = useSocket(); + + onSocketEvent('connect', () => { + console.log('socket connected') + }); + + onSocketEvent('customEmit', (val) => { + console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)') + }); + + const clickButton = (val) => { + // socket.client is the `socket.io-client` instance + socket.client.emit('emit_method', val); + }; + + return { + clickButton + } + } +}) +``` + +**Note**: Don't subscribe / unsubscribe from events (via `$subscribe()` / `$unsubscribe` provided by `useSocket()`) directly in `setup()`. Always do this from within a lifecycle hook like `onBeforeMount` or `onBeforeUnmount` or in a method that is called once the component is created. The `onSocketEvent` composable will do this automatically for you. The reason is, that the moment `setup()` is executed the component is not yet instantiated and `$subscribe` /`$unsubscribe` therefore can't bind to a component instance. + #### Dynamic socket event listeners (changed in v4) Create a new listener From 5ccd96f95e1badef7ed4bf943e44d4e0c566feb4 Mon Sep 17 00:00:00 2001 From: Tobias Grimm Date: Tue, 2 Mar 2021 22:54:05 +0100 Subject: [PATCH 6/6] Add dist to .gitignore --- .gitignore | 1 + dist/vue-socket.io-ext.esm.js | 1 - dist/vue-socket.io-ext.min.js | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 dist/vue-socket.io-ext.esm.js delete mode 100644 dist/vue-socket.io-ext.min.js diff --git a/.gitignore b/.gitignore index 83fdd4d2..37b07946 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ npm-debug.log .idea/ +dist # Release notes RELEASE_NOTE*.md diff --git a/dist/vue-socket.io-ext.esm.js b/dist/vue-socket.io-ext.esm.js deleted file mode 100644 index fdadd8e2..00000000 --- a/dist/vue-socket.io-ext.esm.js +++ /dev/null @@ -1 +0,0 @@ -function e(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function n(n){for(var r=1;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function o(e){return function(e){if(Array.isArray(e))return e}(e)||i(e)||a(e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function c(e){return function(e){if(Array.isArray(e))return u(e)}(e)||i(e)||a(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}function a(e,t){if(e){if("string"==typeof e)return u(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?u(e,t):void 0}}function u(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?t-1:0),r=1;r0?f.set(t,n):f.delete(t)},_listeners:f=new Map(s)},m=function(e){return Object.keys(e._mutations)},g=function(e){return Object.keys(e._actions)},O=function(e){return e.split("/").pop()},w=function(e,t){if("string"!=typeof e&&!Array.isArray(e))throw new TypeError("Expected the input to be `string | string[]`");t=Object.assign({pascalCase:!1},t);var n;return 0===(e=Array.isArray(e)?e.map((function(e){return e.trim()})).filter((function(e){return e.length})).join("-"):e.trim()).length?"":1===e.length?t.pascalCase?e.toUpperCase():e.toLowerCase():(e!==e.toLowerCase()&&(e=function(e){for(var t=!1,n=!1,r=!1,o=0;o1&&void 0!==arguments[1]?arguments[1]:{},i=t.store,a=r(t,["store"]),u=n(n({},k),a),s=b(u.eventToActionTransformer,y(u.actionPrefix)),f=b(u.eventToMutationTransformer,y(u.mutationPrefix));function l(e,t){if(i){var n=f(e),r=s(e),o=m(i),c=g(i),a=p(t);o.filter((function(e){return O(e)===n})).forEach((function(e){return i.commit(e,a)})),c.filter((function(e){return O(e)===r})).forEach((function(e){return i.dispatch(e,a)}))}}function h(){d(e,"onevent",(function(e){var t=o(e.data),n=t[0],r=t.slice(1);v.emit.apply(v,[n].concat(c(r))),l(n,r)})),A.forEach((function(t){e.on(t,(function(){for(var e=arguments.length,n=new Array(e),r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function c(e){return function(e){if(Array.isArray(e))return e}(e)||a(e)||u(e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(e){return function(e){if(Array.isArray(e))return s(e)}(e)||a(e)||u(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}function u(e,t){if(e){if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?t-1:0),r=1;r0?l.set(t,n):l.delete(t)},_listeners:l=new Map(f)},v=function(e){return Object.keys(e._mutations)},g=function(e){return Object.keys(e._actions)},O=function(e){return e.split("/").pop()},w=function(e,t){if("string"!=typeof e&&!Array.isArray(e))throw new TypeError("Expected the input to be `string | string[]`");t=Object.assign({pascalCase:!1},t);var n;return 0===(e=Array.isArray(e)?e.map((function(e){return e.trim()})).filter((function(e){return e.length})).join("-"):e.trim()).length?"":1===e.length?t.pascalCase?e.toUpperCase():e.toLowerCase():(e!==e.toLowerCase()&&(e=function(e){for(var t=!1,n=!1,r=!1,o=0;o1&&void 0!==arguments[1]?arguments[1]:{},n=t.store,a=o(t,["store"]),u=r(r({},_),a),s=d(u.eventToActionTransformer,y(u.actionPrefix)),f=d(u.eventToMutationTransformer,y(u.mutationPrefix));function l(e,t){if(n){var r=f(e),o=s(e),c=v(n),i=g(n),a=b(t);c.filter((function(e){return O(e)===r})).forEach((function(e){return n.commit(e,a)})),i.filter((function(e){return O(e)===o})).forEach((function(e){return n.dispatch(e,a)}))}}function p(){h(e,"onevent",(function(e){var t=c(e.data),n=t[0],r=t.slice(1);m.emit.apply(m,[n].concat(i(r))),l(n,r)})),A.forEach((function(t){e.on(t,(function(){for(var e=arguments.length,n=new Array(e),r=0;r