UNPKG

@vue-ioc/core

Version:

IoC and DI for Vue powered by InversifyJS and inspired by Angular @Module syntactic sugar.

285 lines (222 loc) 8.21 kB
# vue-ioc IoC and DI for [Vue](https://vuejs.org/) powered by [InversifyJS](http://inversify.io/) and inspired by [Angular @Module](https://angular.io/guide/ngmodules) syntactic sugar. ## Required Opinionated Stack * [Vue](https://vuejs.org) 2.x * [vue-class-component](https://github.com/vuejs/vue-class-component) * [TypeScript](https://www.typescriptlang.org/) with config flags: `experimentalDecorators: true` and `emitDecoratorMetadata: true` ## Features 1. **Hierarchical IoC Container** defined in `@Module` by `providers` (using [InversifyJS](http://inversify.io/) under the hood). The hierarchy is bound to Vue components tree. 2. **Autostart** - instantiating top level services when container has been started (for background tasks similar to `@Effects` from `ngrx`). 3. **@InjectReactive()** - makes injected dependency 'deeply reactive' in Vue template. 4. **Instance Handlers** - `@PostConstruct` and `@BeforeDestroy` - decorators for methods called when instance is created or destroyed by container. 5. **Custom Instance Handlers** ie. `@OnEvent('submitForm')` 6. **providedIn 'root' or 'self'** - select where will be bound @Injectable (friendly for tree shakeable singletons and lazy loaded @Modules) ## Planned features (not ready yet) 7. **State Injectors** for [Vuex](https://vuex.vuejs.org/) and [MobX](https://mobx.js.org/). 8. **vue-cli** integration ## Caveats / Limitations * `@Module` can be bound only to Vue component. * You can't use `@Inject()` for Vue component constructor arguments (because you can't define own constructor using `vue-class-component`). Only Vue component class fields are supported. ## Installation `vue-ioc` uses following `peerDependencies`: * `inversify` * `reflect-metadata` * `vue-class-component` * `vue` ```bash # NPM npm install @vue-ioc/core inversify reflect-metadata vue-class-component vue --save # Yarn yarn add @vue-ioc/core inversify reflect-metadata vue-class-component vue ``` You must explicitly install `vue-ioc` via `Vue.use()` in your app main entrypoint: ```typescript // main.ts import Vue from 'vue' import {VueIocPlugin} from '@vue-ioc/core' Vue.use(VueIocPlugin) ``` ## Quick Start [![Edit vue-ioc basic example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/k28n6rp36v?fontsize=14) Create simple injectable service `HttpService`: ```typescript // ./services/HttpService.ts import {Injectable} from '@vue-ioc/core'; @Injectable() export class HttpService { public get(url: string): Promise<any> { return fetch(url).then(rs => rs.json()); } } ``` Add root container using `@Module` decorator at your top level App component and setup providers: ```vue // App.vue <template> <div> <HelloWorld/> </div> </template> <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' import {Module} from '@vue-ioc/core'; import {HttpService} from './services/HttpService'; import HelloWorld from './components/HelloWorld.vue'; @Module({ providers: [ HttpService ] }) @Component({ components: { HelloWorld } }) export default class App extends Vue { } </script> ``` Inject `HttpService` to `<HelloWorld>` component: ```vue // ./components/HelloWorld.vue <template> <div> <h2 v-if="user">Hello {{user.firstName}} {{user.lastName}}!</h2> </div> </template> <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' import {Inject} from '@vue-ioc/core'; import {HttpService} from '../services/HttpService'; @Component() export default class HelloWorld extends Vue { @Inject() public httpService: HttpService; public user = null; public async created() { this.user = await this.httpService.get('./hello.json'); } } </script> ``` ## Providers ```typescript @Module({ providers: [ // useClass { provide: HttpService, useClass: HttpService }, // useClass shortcut HttpService, // useValue { provide: HttpService, useValue: httpService }, // useFactory { provide: HttpService, useFactory: (injector) => /* ...injector.get(FooService)... */ } ] }) ``` ## providedIn Default value: `'self'` Available values: `'root', 'self'` `@Injectable( { providedIn: 'root' } )` will bind service in root (App) container always as singleton. You may also override this setting at `@Module` providers level in both directions: ```typescript @Module({ providers: [ { provide: HttpService, useClass: HttpService, providedIn: 'root' }, // overrides @Injectable() { provide: OtherService, useClass: OtherService, providedIn: 'self' }, // overrides @Injectable( {providedIn: 'root'} ) ] }) ``` ## Custom Instance Handlers `@PostConstruct()` and `@BeforeDestroy()` are two built in instance listeners. You may create custom instance handlers like `@OnEvent('submitForm')` by creating a decorator using `createInstanceHandlerDecorator` 1. Prepare the most basic EventBus implementation: ```typescript // bus/EventBus.ts import Vue from 'vue'; import {Injectable} from '@vue-ioc/core'; @Injectable() export class EventBus { private bus: Vue = new Vue(); dispatch(name: string, data: any) { this.bus.$emit(name, data); } addListener(name: string, listener: (data: any) => void) { this.bus.$on(name, listener) } removeListener(name: string, listener: (data: any) => void) { this.bus.$off(name, listener) } } ``` 2. Create `@OnEvent(name:string)` decorator ```typescript // bus/OnEvent.ts import {createInstanceHandlerDecorator} from '@vue-ioc/core'; import {EventBus} from './EventBus'; export function OnEvent(name: string) { return createInstanceHandlerDecorator(({injector, instance, method}) => { // attach handler - a place where listeners should be attached const bus: EventBus = injector.get(EventBus); // you have access to injector where all services can be retrieved const boundMethod = instance[method].bind(instance); // bound method to `this` of instance bus.addListener(name, boundMethod); return () => { // detach handler - a place where all listeners should be detached bus.removeListener(name, boundMethod); }; }); } ``` 3. Dispatch event from view: ```typescript // view/Form.vue <template> <div> <button @click="submitForm">Submit</button> </div> </template> <script lang="ts"> import Vue from 'vue' import Component from 'vue-class-component' import {Inject} from '@vue-ioc/core'; import {EventBus} from '../bus/EventBus'; @Component() export default class SomeForm extends Vue { @Inject() public bus!: EventBus; public submitForm() { this.bus.dispatch('submitForm', { firstName: 'John', lastName: 'Doe'}) } } </script> ``` 4. Handle event in external action: ```typescript // actions/SubmitForm.ts import {OnEvent} from '../bus/OnEvent' import {Injectable} from '@vue-ioc/core'; @Injectable() export class SubmitForm { @OnEvent('submitForm') perform (data) { // do something with data } } ``` ## Inversify Container Options vue-ioc uses following default options for creating Inversify containers: { autoBindInjectable: false, defaultScope: 'Singleton', skipBaseClassChecks: true, } To override or add other options, please use `containerOptions` of plugin options: ```typescript Vue.use(VueIocPlugin, { containerOptions: { // options of Inversify container: // https://github.com/inversify/InversifyJS/blob/master/wiki/container_api.md#container-options } }) ```