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/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 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 { + 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 1828f1cb..f1fc8dba 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 * as Main from '../plugin'; +import { createApp, inject } from 'vue'; +import { SocketExtensionKey } from '..'; +import Main from '../plugin'; import io from '../__mocks__/socket.io-client'; it('should be vue plugin (is an object with `install` method)', () => { @@ -96,6 +97,17 @@ 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); + }); }); 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 dae05332..5a0c909c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ -import * as plugin from './plugin'; +import plugin from './plugin'; export default plugin; +export { SocketExtensionKey, useSocket, onSocketEvent } from './composables'; diff --git a/src/plugin.js b/src/plugin.js index f84525c3..a4a7728b 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -5,6 +5,7 @@ import GlobalEmitter from './GlobalEmitter'; import createMixin from './createMixin'; import { isSocketIo } from './utils'; import defaults from './defaults'; +import { SocketExtensionKey } from './composables'; /** * @param {Vue} app @@ -67,6 +68,7 @@ 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 }; +export default { defaults, install }; diff --git a/types/index.d.ts b/types/index.d.ts index 12698862..0bee048a 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 +export declare const onSocketEvent: (event: string, fn: Function) => void