{"id":22662724,"url":"https://github.com/ngxs-labs/emitter","last_synced_at":"2026-01-18T07:45:17.379Z","repository":{"id":33186092,"uuid":"149480457","full_name":"ngxs-labs/emitter","owner":"ngxs-labs","description":":octopus: New pattern that provides the opportunity to feel free from actions ","archived":false,"fork":false,"pushed_at":"2025-05-13T10:04:42.000Z","size":8560,"stargazers_count":110,"open_issues_count":11,"forks_count":5,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-11-22T21:04:03.066Z","etag":null,"topics":["emitter","ngxs","receiver","stage-3"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ngxs-labs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-09-19T16:28:34.000Z","updated_at":"2025-07-24T14:35:47.000Z","dependencies_parsed_at":"2024-06-18T17:06:09.614Z","dependency_job_id":"5b8c79d3-fbf1-45b0-96ad-58fc5e854801","html_url":"https://github.com/ngxs-labs/emitter","commit_stats":{"total_commits":774,"total_committers":7,"mean_commits":"110.57142857142857","dds":"0.33850129198966405","last_synced_commit":"3c48293f4040ab5da8af7d5caf6f08700a5bae86"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/ngxs-labs/emitter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngxs-labs%2Femitter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngxs-labs%2Femitter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngxs-labs%2Femitter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngxs-labs%2Femitter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ngxs-labs","download_url":"https://codeload.github.com/ngxs-labs/emitter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ngxs-labs%2Femitter/sbom","scorecard":{"id":683712,"data":{"date":"2025-08-11","repo":{"name":"github.com/ngxs-labs/emitter","commit":"d6bf73bf30cf58d8ff25adf4770adb706b2e5793"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"name":"Code-Review","score":0,"reason":"Found 1/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/ngxs-labs/emitter/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/ngxs-labs/emitter/ci.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/ngxs-labs/emitter/ci.yml/master?enable=pin","Info:   0 out of   3 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 25 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"65 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25","Warn: Project is vulnerable to: GHSA-jr5f-v2jv-69x6","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-cwfw-4gq5-mrqx","Warn: Project is vulnerable to: GHSA-g95f-p29q-9xw4","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-ghr5-ch3p-vcr6","Warn: Project is vulnerable to: GHSA-67mh-4wv8-2f99","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-74fj-2j2h-c42q","Warn: Project is vulnerable to: GHSA-pw2r-vq6v-hr8c","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-rc47-6667-2j5j","Warn: Project is vulnerable to: GHSA-c7qv-q95q-8v27","Warn: Project is vulnerable to: GHSA-4www-5p9h-95mh","Warn: Project is vulnerable to: GHSA-9gqv-wp59-fq42","Warn: Project is vulnerable to: GHSA-78xj-cgh5-2h22","Warn: Project is vulnerable to: GHSA-2p57-rm9w-gvfp","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-593f-38f6-jp5m","Warn: Project is vulnerable to: GHSA-x2rg-q646-7m2v","Warn: Project is vulnerable to: GHSA-jgmv-j7ww-jx2x","Warn: Project is vulnerable to: GHSA-76p3-8jx3-jpfq","Warn: Project is vulnerable to: GHSA-3rfm-jhwj-7488","Warn: Project is vulnerable to: GHSA-hhq3-ff78-jv3g","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-7fh5-64p2-3v2j","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-64vr-g452-qvp3","Warn: Project is vulnerable to: GHSA-9cwx-2883-4wfx","Warn: Project is vulnerable to: GHSA-vg6x-rcgg-rjx6","Warn: Project is vulnerable to: GHSA-x574-m823-4x7w","Warn: Project is vulnerable to: GHSA-4r4m-qw57-chr8","Warn: Project is vulnerable to: GHSA-xcj6-pq6g-qj4x","Warn: Project is vulnerable to: GHSA-356w-63v5-8wf4","Warn: Project is vulnerable to: GHSA-859w-5945-r5v3","Warn: Project is vulnerable to: GHSA-4vvj-4cpr-p986","Warn: Project is vulnerable to: GHSA-4v9v-hfq4-rm2v","Warn: Project is vulnerable to: GHSA-9jgg-88mc-972h","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T00:00:03.160Z","repository_id":33186092,"created_at":"2025-08-22T00:00:03.160Z","updated_at":"2025-08-22T00:00:03.160Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28533172,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T00:39:45.795Z","status":"online","status_checked_at":"2026-01-18T02:00:07.578Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["emitter","ngxs","receiver","stage-3"],"created_at":"2024-12-09T12:02:02.973Z","updated_at":"2026-01-18T07:45:17.327Z","avatar_url":"https://github.com/ngxs-labs.png","language":"TypeScript","funding_links":[],"categories":["State Management"],"sub_categories":["NGXS"],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/ngxs-labs/emitter/master/docs/assets/emitter.png\"\u003e\n\u003c/p\u003e\n\n---\n\n\u003e ER is a new pattern that provides the opportunity to feel free from actions\n\n[![@ngxs-labs/emitter](https://github.com/ngxs-labs/emitter/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/ngxs-labs/emitter/actions/workflows/ci.yml)\n[![NPM](https://badge.fury.io/js/%40ngxs-labs%2Femitter.svg)](https://www.npmjs.com/package/@ngxs-labs/emitter)\n[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/ngxs-labs/emitter/blob/master/LICENSE)\n\n[🚀 See it in action on Stackblitz](https://stackblitz.com/edit/ngxs-emitter-example)\n\nThis package allows you to get rid of actions. You can use decorators to register actions directly in your state, you don't have to create any actions in your project (until you really need them), as they don't give any profit, only bring extra boilerplate files.\n\n## Concepts\n\nCompare these diagrams, we've simplified Redux flow and threw out unnecessary middleware:\n\n![ER Flow](https://raw.githubusercontent.com/ngxs-labs/emitter/master/docs/assets/redux-er.png)\n\n## 📦 Install\n\nTo install `@ngxs-labs/emitter` run the following command:\n\n```console\nnpm install @ngxs-labs/emitter\n# or if you use yarn\nyarn add @ngxs-labs/emitter\n```\n\n## 🔨 Usage\n\nImport the module into your root application module:\n\n```typescript\nimport { NgModule } from '@angular/core';\nimport { NgxsModule } from '@ngxs/store';\nimport { NgxsEmitPluginModule } from '@ngxs-labs/emitter';\n\n@NgModule({\n  imports: [NgxsModule.forRoot(states), NgxsEmitPluginModule.forRoot()]\n})\nexport class AppModule {}\n```\n\n## Receiver\n\nReceiver is a basic building block. `@Receiver()` is a function that allows you to decorate static methods in your states for further passing this method to the emitter:\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\n@State\u003cnumber\u003e({\n  name: 'counter',\n  defaults: 0\n})\nexport class CounterState {\n  @Receiver()\n  static increment({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() + 1);\n  }\n\n  @Receiver()\n  static decrement({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() - 1);\n  }\n}\n```\n\n## Emitter\n\nEmitter is basically a bridge between your component and receivers. `@Emitter()` is a function that decorates properties defining new getter and gives you an access to the emittable interface:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { Select } from '@ngxs/store';\nimport { Emitter, Emittable } from '@ngxs-labs/emitter';\n\nimport { Observable } from 'rxjs';\n\nimport { CounterState } from './counter.state';\n\n@Component({\n  selector: 'app-counter',\n  template: `\n    \u003cng-container *ngIf=\"counter$ | async as counter\"\u003e\n      \u003ch3\u003eCounter is {{ counter }}\u003c/h3\u003e\n    \u003c/ng-container\u003e\n\n    \u003cbutton (click)=\"increment.emit()\"\u003eIncrement\u003c/button\u003e\n    \u003cbutton (click)=\"decrement.emit()\"\u003eDecrement\u003c/button\u003e\n  `\n})\nexport class CounterComponent {\n  @Select(CounterState)\n  counter$: Observable\u003cnumber\u003e;\n\n  // Use in components to emit asynchronously payload\n  @Emitter(CounterState.increment)\n  increment: Emittable;\n\n  @Emitter(CounterState.decrement)\n  decrement: Emittable;\n}\n```\n\nAlternatively you can use `EmitterService` instead of decorating properties:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { Select } from '@ngxs/store';\nimport { EmitterService, Emittable } from '@ngxs-labs/emitter';\n\nimport { Observable } from 'rxjs';\n\nimport { CounterState } from './counter.state';\n\n@Component({\n  selector: 'app-counter',\n  template: `\n    \u003cng-container *ngIf=\"counter$ | async as counter\"\u003e\n      \u003ch3\u003eCounter is {{ counter }}\u003c/h3\u003e\n    \u003c/ng-container\u003e\n\n    \u003cbutton (click)=\"increment()\"\u003eIncrement\u003c/button\u003e\n    \u003cbutton (click)=\"decrement()\"\u003eDecrement\u003c/button\u003e\n  `\n})\nexport class CounterComponent {\n  @Select(CounterState)\n  counter$: Observable\u003cnumber\u003e;\n\n  constructor(private emitter: EmitterService) {}\n\n  increment(): void {\n    this.emitter.action(CounterState.increment).emit();\n  }\n\n  decrement(): void {\n    this.emitter.action(CounterState.decrement).emit();\n  }\n}\n```\n\n## Custom types\n\nYou can define custom types for debbuging purposes (works with `@ngxs/logger-plugin`):\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\n@State\u003cnumber\u003e({\n  name: 'counter',\n  defaults: 0\n})\nexport class CounterState {\n  @Receiver({ type: '[Counter] Increment value' })\n  static increment({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() + 1);\n  }\n\n  @Receiver({ type: '[Counter] Decrement value' })\n  static decrement({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() - 1);\n  }\n}\n```\n\n## Payload type safety\n\n`Emittable` and `EmitterAction` are generics which allow you to type the payload, which is `void` (alias not present) by default.\nFor example, `Emittable` is the same as `Emittable\u003cvoid\u003e`. Also it's possible to type it like `Emittable\u003cnumber\u003e` or `Emittable\u003cany\u003e` (if you want it to accept all possible values).\n\n```typescript\nimport { Component } from '@angular/core';\nimport { Select } from '@ngxs/store';\nimport { Emitter, Emittable } from '@ngxs-labs/emitter';\n\nimport { Observable } from 'rxjs';\n\nimport { CustomCounter, CounterState } from './counter.state';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    {{ counter$ | async | json }}\n    \u003cbutton (click)=\"update()\"\u003eupdate\u003c/button\u003e\n  `\n})\nexport class AppComponent {\n  @Select(CounterState)\n  counter$: Observable\u003cCustomCounter\u003e;\n\n  @Emitter(CounterState.update)\n  private update: Emittable\u003cCustomCounter\u003e;\n\n  update(): void {\n    this.update.emit({ value: 5 });\n  }\n}\n```\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver, EmitterAction } from '@ngxs-labs/emitter';\n\nexport interface CustomCounter {\n  value: number;\n}\n\n@State\u003cCustomCounter\u003e({\n  name: 'counter',\n  defaults: {\n    value: 0\n  }\n})\nexport class CounterState {\n  @Receiver({ payload: { value: -1 } }) // default value if payload emitted as undefined\n  static update(\n    { setState }: StateContext\u003cCustomCounter\u003e,\n    { payload }: EmitterAction\u003cCustomCounter\u003e\n  ) {\n    setState({ value: payload.value });\n  }\n}\n```\n\n## Actions\n\nIf you still need actions - it is possible to pass an action as an argument into `@Receiver()` decorator:\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\nexport class Increment {\n  static readonly type = '[Counter] Increment value';\n}\n\nexport class Decrement {\n  static readonly type = '[Counter] Decrement value';\n}\n\n@State\u003cnumber\u003e({\n  name: 'counter',\n  defaults: 0\n})\nexport class CounterState {\n  @Receiver({ action: Increment })\n  static increment({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() + 1);\n  }\n\n  @Receiver({ action: Decrement })\n  static decrement({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() - 1);\n  }\n}\n```\n\nAlso it's possible to pass multiple actions:\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\nexport class Increment {\n  static readonly type = '[Counter] Increment value';\n}\n\nexport class Decrement {\n  static readonly type = '[Counter] Decrement value';\n}\n\n@State\u003cnumber\u003e({\n  name: 'counter',\n  defaults: 0\n})\nexport class CounterState {\n  @Receiver({ action: [Increment, Decrement] })\n  static increment({ setState, getState }: StateContext\u003cnumber\u003e, action: Increment | Decrement) {\n    const state = getState();\n\n    if (action instanceof Increment) {\n      setState(state + 1);\n    } else if (action instanceof Decrement) {\n      setState(state - 1);\n    }\n  }\n}\n```\n\n## Emitting multiple value\n\nIt's also possible to emit multiple values, just define your state:\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\n@State\u003cstring[]\u003e({\n  name: 'animals',\n  defaults: []\n})\nexport class AnimalsState {\n  @Receiver()\n  static addAnimal(\n    { setState, getState }: StateContext\u003cstring[]\u003e,\n    { payload }: EmitterAction\u003cstring\u003e\n  ) {\n    setState([...getState(), payload]);\n  }\n}\n```\n\nAnd use `emitMany` method from `Emittable` object:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { Select } from '@ngxs/store';\nimport { Emitter, Emittable } from '@ngxs-labs/emitter';\n\nimport { Observable } from 'rxjs';\n\nimport { AnimalsState } from './animals.state';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003cp *ngFor=\"let animal of animals$ | async\"\u003e{{ animal }}\u003c/p\u003e\n    \u003cbutton (click)=\"addAnimals()\"\u003eAdd animals\u003c/button\u003e\n  `\n})\nexport class AppComponent {\n  @Select(AnimalsState)\n  animals$: Observable\u003cstring[]\u003e;\n\n  @Emitter(AnimalsState.addAnimal)\n  private addAnimal: Emittable\u003cstring\u003e;\n\n  addAnimals(): void {\n    this.addAnimal.emitMany(['panda', 'zebra', 'monkey']);\n  }\n}\n```\n\n## Dependency injection\n\nAssume you have to make some API request and load some data from your server, it is very easy to use services with static methods, Angular provides an `Injector` class for getting instances by reference:\n\n```typescript\nimport { Injector } from '@angular/core';\n\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\nimport { tap } from 'rxjs/operators';\n\ninterface Todo {\n  id: number;\n  title: string;\n  completed: boolean;\n}\n\n@State\u003cTodo[]\u003e({\n  name: 'todos',\n  defaults: []\n})\nexport class TodosState {\n  // ApiService is defined somewhere...\n  private static api: ApiService;\n\n  constructor(injector: Injector) {\n    TodosState.api = injector.get\u003cApiService\u003e(ApiService);\n  }\n\n  @Receiver()\n  static getTodos({ setState }: StateContext\u003cTodo[]\u003e) {\n    // If `ApiService.prototype.getTodos` returns an `Observable` - just use `tap` operator\n    return this.api.getTodos().pipe(tap(todos =\u003e setState(todos)));\n  }\n\n  // OR\n\n  @Receiver()\n  static getTodos({ setState }: StateContext\u003cTodo[]\u003e) {\n    // If `ApiService.prototype.getTodos` returns a `Promise` - just use `then`\n    return this.api.getTodos().then(todos =\u003e setState(todos));\n  }\n}\n```\n\nIf you work with promises - we advice you to use `async/await` approach, because method marked with `async` keyword will automatically return a `Promise`, you will not get confused if you missed `return` keyword somewhere:\n\n```typescript\nimport { Injector } from '@angular/core';\n\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\nexport type AppInformationStateModel = null | {\n  version: string;\n  shouldUseGraphQL: boolean;\n};\n\n@State\u003cAppInformationStateModel\u003e({\n  name: 'information',\n  defaults: null\n})\nexport class AppInformationState {\n  private static appService: AppService;\n\n  constructor(injector: Injector) {\n    AppInformationState.appService = injector.get\u003cAppService\u003e(AppService);\n  }\n\n  @Receiver({ type: '[App information] Get app information' })\n  static async getAppInformation({ setState }: StateContext\u003cAppInformationStateModel\u003e) {\n    setState(await this.appService.getAppInformation());\n  }\n}\n```\n\n## Lifecycle\n\nAs you may know - actions in NGXS have own lifecycle. We also provide RxJS operators that give you the ability to react to actions at different points in their existence:\n\n- `ofEmittableDispatched`: triggers when an emittable target has been dispatched\n- `ofEmittableSuccessful`: triggers when an emittable target has been completed successfully\n- `ofEmittableCanceled`: triggers when an emittable target has been canceled\n- `ofEmittableErrored`: triggers when an emittable target has caused an error to be thrown\n\nBelow is just a simple example how to use those operators:\n\n```typescript\nimport { State, StateContext } from '@ngxs/store';\nimport { Receiver } from '@ngxs-labs/emitter';\n\nimport { throwError } from 'rxjs';\n\n@State\u003cnumber\u003e({\n  name: 'counter',\n  defaults: 0\n})\nexport class CounterState {\n  @Receiver()\n  static increment({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() + 1);\n  }\n\n  @Receiver()\n  static decrement({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() - 1);\n  }\n\n  @Receiver()\n  static multiplyBy2({ setState, getState }: StateContext\u003cnumber\u003e) {\n    setState(getState() * 2);\n  }\n\n  @Receiver()\n  static throwError() {\n    return throwError(new Error('Whoops!'));\n  }\n}\n```\n\nImport operators in your component and pipe `Actions` service:\n\n```typescript\nimport { Component } from '@angular/core';\nimport { Actions } from '@ngxs/store';\nimport {\n  Emitter,\n  Emittable,\n  ofEmittableDispatched,\n  ofEmittableActionContext\n} from '@ngxs-labs/emitter';\n\nimport { CounterState } from './counter.state';\n\n@Component({\n  selector: 'app-root',\n  template: ``\n})\nexport class AppComponent {\n  @Emitter(CounterState.increment)\n  private increment: Emittable;\n\n  @Emitter(CounterState.decrement)\n  private decrement: Emittable;\n\n  @Emitter(CounterState.throwError)\n  private throwError: Emittable;\n\n  constructor(actions$: Actions) {\n    actions$.pipe(ofEmittableDispatched(CounterState.increment)).subscribe(() =\u003e {\n      console.log('CounterState.increment has been intercepted');\n    });\n\n    setInterval(() =\u003e {\n      this.increment.emit();\n      this.decrement.emit();\n      this.throwError.emit();\n    }, 1000);\n  }\n}\n```\n\n## 💡 TDD\n\nIt's very easy to write unit tests using ER concept, because we provide our module out of the box that makes all providers and stores available for dependency injection. So you can avoid creating mocked components with properties decorated with `@Emitter()` decorator, just use the `StoreTestBed` service to get any emittable object:\n\n```typescript\nimport { EmitterService } from '@ngxs-labs/emitter';\nimport { StoreTestBedModule } from '@ngxs-labs/emitter/testing';\n\nit('should increment state', () =\u003e {\n  @State\u003cnumber\u003e({\n    name: 'counter',\n    defaults: 0\n  })\n  class CounterState {\n    @Receiver()\n    static increment({ setState, getState }: StateContext\u003cnumber\u003e) {\n      setState(getState() + 1);\n    }\n  }\n\n  TestBed.configureTestingModule({\n    imports: [StoreTestBedModule.configureTestingModule([CounterState])]\n  });\n\n  const store: Store = TestBed.inject(Store);\n  const emitter: EmitterService = TestBed.inject(EmitterService);\n\n  emitter.action(CounterState.increment).emit();\n\n  const counter = store.selectSnapshot\u003cnumber\u003e(({ counter }) =\u003e counter);\n  expect(counter).toBe(1);\n});\n```\n\n## Interaction\n\nYou can easily provide an interaction between different states using ER. Imagine such simple state that stores information if success and error messages exist:\n\n```typescript\ntype AppStatusStateModel = {\n  successMessage: string | null;\n  errorMessage: string | null;\n};\n\n@State({\n  name: 'appStatusState',\n  defaults: {\n    successMessage: null,\n    errorMessage: null\n  }\n})\nexport class AppStatusState {\n  @Receiver({ type: '[AppStatus] Success' })\n  static success(\n    { setState }: StateContext\u003cAppStatusStateModel\u003e,\n    { payload }: EmitterAction\u003cstring\u003e\n  ) {\n    setState({\n      successMessage: payload,\n      errorMessage: null\n    });\n  }\n\n  @Receiver({ type: '[AppStatus] Failure' })\n  static failure(\n    { setState }: StateContext\u003cAppStatusStateModel\u003e,\n    { payload }: EmitterAction\u003cstring\u003e\n  ) {\n    setState({\n      successMessage: null,\n      errorMessage: payload\n    });\n  }\n}\n```\n\nYou want to emit events from another state that makes requests:\n\n```typescript\n@State({ name: 'appState' })\nclass AppState {\n  private static tagService: TagService;\n\n  @Emitter(AppStatusState.success)\n  private static success: Emittable\u003cstring\u003e;\n\n  @Emitter(AppStatusState.failure)\n  private static failure: Emittable\u003cstring\u003e;\n\n  constructor(injector: Injector) {\n    AppState.tagService = injector.get\u003cTagService\u003e(TagService);\n  }\n\n  @Receiver({ type: '[AppState] Add tag to the DB' })\n  static addTag(_, { payload }: EmitterAction\u003cstring\u003e) {\n    return this.tagService.addOne(payload).pipe(\n      tap(response =\u003e this.success.emit(response.message)),\n      catchError(error =\u003e {\n        this.failure.emit(error.message);\n        return of(error);\n      })\n    );\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngxs-labs%2Femitter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fngxs-labs%2Femitter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fngxs-labs%2Femitter/lists"}