├── .nvmrc ├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .eslintignore ├── .npmrc ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── until-destroy.yml ├── .prettierignore ├── apps ├── integration │ ├── src │ │ ├── polyfills.ts │ │ ├── test-setup.ts │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── array-of-subscriptions │ │ │ │ ├── document-click │ │ │ │ │ ├── document-click.component.html │ │ │ │ │ └── document-click.component.ts │ │ │ │ ├── array-of-subscriptions.module.ts │ │ │ │ ├── array-of-subscriptions.component.html │ │ │ │ └── array-of-subscriptions.component.ts │ │ │ ├── app.component.scss │ │ │ ├── enums │ │ │ │ └── notification.enum.ts │ │ │ ├── app.component.ts │ │ │ ├── navbar │ │ │ │ ├── navbar.component.html │ │ │ │ ├── navbar.module.ts │ │ │ │ └── navbar.component.ts │ │ │ ├── destroyable-provider │ │ │ │ ├── connection │ │ │ │ │ ├── connection.directive.ts │ │ │ │ │ └── connection.service.ts │ │ │ │ ├── destroyable-provider.module.ts │ │ │ │ ├── destroyable-provider.component.html │ │ │ │ └── destroyable-provider.component.ts │ │ │ ├── logger │ │ │ │ └── logger.factory.ts │ │ │ ├── pipe │ │ │ │ ├── pipe.module.ts │ │ │ │ ├── pipe.component.html │ │ │ │ ├── pipe.component.ts │ │ │ │ └── i18n.pipe.ts │ │ │ ├── custom-method │ │ │ │ ├── custom-method.module.ts │ │ │ │ ├── interval.service.ts │ │ │ │ ├── custom-method.component.html │ │ │ │ └── custom-method.component.ts │ │ │ ├── directive │ │ │ │ ├── directive.module.ts │ │ │ │ ├── directive.component.html │ │ │ │ ├── directive.component.ts │ │ │ │ └── http.directive.ts │ │ │ ├── multiple-custom-methods │ │ │ │ ├── multiple-custom-methods.module.ts │ │ │ │ ├── multiple-custom-methods.component.html │ │ │ │ ├── issue-66.service.ts │ │ │ │ └── multiple-custom-methods.component.ts │ │ │ ├── inheritance │ │ │ │ ├── inheritance.module.ts │ │ │ │ ├── issue-97 │ │ │ │ │ └── issue-97.component.ts │ │ │ │ ├── inheritance.component.html │ │ │ │ ├── inheritance.component.ts │ │ │ │ └── issue-61 │ │ │ │ │ └── issue-61.component.ts │ │ │ ├── app.module.ts │ │ │ └── app.component.spec.ts │ │ ├── environments │ │ │ ├── environment.ts │ │ │ └── environment.prod.ts │ │ ├── main.ts │ │ └── index.html │ ├── tsconfig.editor.json │ ├── tsconfig.spec.json │ ├── tsconfig.app.json │ ├── .browserslistrc │ ├── tsconfig.json │ ├── jest.config.ts │ ├── webpack.config.js │ ├── .eslintrc.json │ └── project.json └── integration-e2e │ ├── src │ ├── plugins │ │ └── index.js │ ├── support │ │ └── commands.js │ └── integration │ │ ├── pipe.spec.js │ │ ├── directive.spec.js │ │ ├── custom-method.spec.js │ │ ├── destroyable-provider.spec.js │ │ ├── array-of-subscriptions.spec.js │ │ ├── inheritance.spec.js │ │ └── multiple-custom-methods.spec.js │ ├── tsconfig.json │ ├── tsconfig.e2e.json │ ├── .eslintrc.json │ ├── cypress.json │ └── project.json ├── .commitlintrc.json ├── libs ├── until-destroy │ ├── src │ │ ├── test-setup.ts │ │ ├── index.ts │ │ └── lib │ │ │ ├── ivy.ts │ │ │ ├── internals.ts │ │ │ ├── until-destroy.ts │ │ │ ├── until-destroyed.ts │ │ │ └── checker.ts │ ├── ng-package.json │ ├── tests │ │ ├── utils.ts │ │ ├── until-destroy.spec.ts │ │ └── until-destroyed.spec.ts │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── project.json │ ├── tsconfig.lib.json │ ├── jest.config.ts │ ├── .eslintrc.json │ └── package.json └── until-destroy-migration │ ├── src │ ├── test-setup.ts │ └── index.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── tests │ ├── fixtures │ │ ├── filled-onDestroy.service.ts │ │ ├── single-import.component.ts │ │ └── several-imports.component.ts │ └── run.spec.ts │ ├── tsconfig.json │ ├── package.json │ ├── jest.config.ts │ ├── project.json │ └── .eslintrc.json ├── jest.config.ts ├── .gitignore ├── tsconfig.base.json ├── jest.preset.js ├── LICENSE ├── .eslintrc.json ├── nx.json ├── CONTRIBUTING.md ├── .all-contributorsrc ├── package.json ├── README.md └── CHANGELOG.md /.nvmrc: -------------------------------------------------------------------------------- 1 | node 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @NetanelBasal @arturovt 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | 4 | /.nx/cache -------------------------------------------------------------------------------- /apps/integration/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (on, config) => {}; 2 | -------------------------------------------------------------------------------- /apps/integration/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/until-destroy/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /apps/integration/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/integration/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /apps/integration/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nx/jest'); 2 | 3 | module.exports = { projects: getJestProjects() }; 4 | -------------------------------------------------------------------------------- /libs/until-destroy/src/index.ts: -------------------------------------------------------------------------------- 1 | export { UntilDestroy } from './lib/until-destroy'; 2 | export { untilDestroyed } from './lib/until-destroyed'; 3 | -------------------------------------------------------------------------------- /apps/integration/src/app/array-of-subscriptions/document-click/document-click.component.html: -------------------------------------------------------------------------------- 1 |

Click on the document and see clientX is {{ clientX$ | async }}

2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | node_modules 3 | .vscode 4 | /dist-test 5 | .idea 6 | dist 7 | .cache 8 | migrations.json 9 | .pnpm-debug.log 10 | 11 | .nx/cache -------------------------------------------------------------------------------- /apps/integration/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/until-destroy/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/until-destroy", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/integration-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/integration/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep button { 2 | color: #fff; 3 | margin-top: 5px; 4 | margin-right: 5px; 5 | padding: 5px; 6 | box-sizing: border-box; 7 | border: 0; 8 | cursor: pointer; 9 | background-color: #007ad9; 10 | } 11 | -------------------------------------------------------------------------------- /libs/until-destroy/tests/utils.ts: -------------------------------------------------------------------------------- 1 | export function callNgOnDestroy(component: any): void { 2 | // The TS compiler will whine that this property doesn't on the component itself, 3 | // but this method will be added by our decorator. 4 | component.ngOnDestroy(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/integration-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "sourceMap": false, 6 | "skipLibCheck": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/libs/until-destroy-migration" 6 | }, 7 | "files": ["src/index.ts"], 8 | "exclude": ["jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/integration/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "types": ["jest", "node"] 6 | }, 7 | "files": ["src/test-setup.ts"], 8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/until-destroy/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "types": ["jest", "node"] 6 | }, 7 | "files": ["src/test-setup.ts"], 8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/integration/src/app/enums/notification.enum.ts: -------------------------------------------------------------------------------- 1 | export const enum NotificationClass { 2 | Danger = 'notification is-danger', 3 | Success = 'notification is-success' 4 | } 5 | 6 | export const enum NotificationText { 7 | Subscribed = 'Subscribed ⚠️', 8 | Unsubscribed = 'Unsubscribed ❤️‍🔥' 9 | } 10 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "types": ["jest", "node"] 6 | }, 7 | "files": ["src/test-setup.ts"], 8 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tests/fixtures/filled-onDestroy.service.ts: -------------------------------------------------------------------------------- 1 | import { untilDestroyed } from 'ngx-take-until-destroy'; 2 | import { OnDestroy } from '@angular/core'; 3 | 4 | export class FilledOnDestroyService implements OnDestroy { 5 | ngOnDestroy() { 6 | throw new Error('Method not implemented.'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/integration/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2022", 6 | "types": ["node"], 7 | "useDefineForClassFields": false 8 | }, 9 | "files": ["src/main.ts", "src/polyfills.ts"], 10 | "exclude": ["jest.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/integration/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class AppComponent {} 10 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/support/commands.js: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('shouldHaveSuccessClass', { prevSubject: 'element' }, $element => 2 | cy.wrap($element).should('have.class', 'is-success') 3 | ); 4 | 5 | Cypress.Commands.add('shouldHaveDangerClass', { prevSubject: 'element' }, $element => 6 | cy.wrap($element).should('have.class', 'is-danger') 7 | ); 8 | -------------------------------------------------------------------------------- /libs/until-destroy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "target": "es2020" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/integration/src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /apps/integration-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {}, 5 | "overrides": [ 6 | { 7 | "files": ["src/plugins/index.js"], 8 | "rules": { 9 | "@typescript-eslint/no-var-requires": "off", 10 | "no-undef": "off" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/integration/src/app/destroyable-provider/connection/connection.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | import { ConnectionService } from './connection.service'; 4 | 5 | @Directive({ 6 | selector: '[connection]', 7 | providers: [ConnectionService] 8 | }) 9 | export class ConnectionDirective { 10 | constructor(connectionService: ConnectionService) {} 11 | } 12 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/pipe.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('pipe', () => { 4 | it('should click the button and show that pipe has unsubscribed from subscription', () => { 5 | cy.visit('/pipe') 6 | .get('[data-cy="toggle-pipe"]') 7 | .click() 8 | .get('[data-cy="pipe-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/integration/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /apps/integration/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | last 2 Chrome versions 9 | -------------------------------------------------------------------------------- /apps/integration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "target": "es2020" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/until-destroy/src/lib/ivy.ts: -------------------------------------------------------------------------------- 1 | import { Type, ɵNG_PIPE_DEF, ɵPipeDef } from '@angular/core'; 2 | 3 | const NG_PIPE_DEF = ɵNG_PIPE_DEF as 'ɵpipe'; 4 | 5 | // Angular doesn't expose publicly `PipeType` but it actually has it. 6 | export interface PipeType extends Type { 7 | ɵpipe: ɵPipeDef; 8 | } 9 | 10 | export function isPipe(target: any): target is PipeType { 11 | return !!target[NG_PIPE_DEF]; 12 | } 13 | -------------------------------------------------------------------------------- /apps/integration-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "chromeWebSecurity": false, 4 | "modifyObstructiveCode": false, 5 | "fileServerFolder": ".", 6 | "fixturesFolder": "./src/fixtures", 7 | "integrationFolder": "./src/integration", 8 | "pluginsFile": "./src/plugins/index", 9 | "supportFile": "./src/support/commands", 10 | "screenshotsFolder": "../../dist/cypress/apps/integration-e2e/screenshots" 11 | } 12 | -------------------------------------------------------------------------------- /apps/integration/src/app/logger/logger.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class LoggerFactory { 5 | createLogger(name: string, color: string): Pick { 6 | return { 7 | log: (messages: Parameters) => 8 | console.log(`%c[${name}] ${messages}`, `font-size: 12px; color: ${color}`) 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/integration/src/app/navbar/navbar.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { NavbarComponent } from './navbar.component'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, RouterModule], 9 | declarations: [NavbarComponent], 10 | exports: [NavbarComponent] 11 | }) 12 | export class NavbarModule {} 13 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/directive.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('directive', () => { 4 | it('should click the button and shown that http directive has unsubscribed from subscription', () => { 5 | cy.visit('/directive') 6 | .get('[data-cy="toggle-http-directive"]') 7 | .click() 8 | .get('[data-cy="directive-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tests/fixtures/single-import.component.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'rxjs'; 2 | import { untilDestroyed } from 'ngx-take-until-destroy'; 3 | import { OnDestroy } from '@angular/core'; 4 | 5 | // test comment 6 | @Component({ template: '' }) 7 | export class SingleImportComponent extends BaseComponent implements OnDestroy { 8 | create() { 9 | console.log('create'); 10 | } 11 | 12 | public ngOnDestroy() {} 13 | } 14 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/custom-method.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('custom method', () => { 4 | it('should click the button and show that the interval service has unsubscribed from subscription', () => { 5 | cy.visit('/custom-method') 6 | .get('[data-cy="destroy-service"]') 7 | .click() 8 | .get('[data-cy="interval-service-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tests/fixtures/several-imports.component.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'rxjs'; 2 | import { untilDestroyed } from 'ngx-take-until-destroy'; 3 | import { OnDestroy, OnChanges, Component } from '@angular/core'; 4 | 5 | @Component({ template: '' }) 6 | export class SeveralImportsComponent extends BaseComponent implements OnDestroy, OnChanges { 7 | ngOnChanges() { 8 | console.log('OnChanges'); 9 | } 10 | 11 | ngOnDestroy() {} 12 | } 13 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/destroyable-provider.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('destroyable provider', () => { 4 | it('should click the button and show that the destroyable provider has unsubscribed from subscriptions', () => { 5 | cy.visit('/destroyable-provider') 6 | .get('[data-cy="toggle-provider"]') 7 | .click() 8 | .get('[data-cy="provider-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/array-of-subscriptions.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('array of subscriptions', () => { 4 | it('should click the button and show that the document click component has unsubscribed from subscription', () => { 5 | cy.visit('/array-of-subscriptions') 6 | .get('[data-cy="toggle-document-click"]') 7 | .click() 8 | .get('[data-cy="document-click-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "strict": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "target": "es2020" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/integration/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @ngneat/until-destroy integration example 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@ngneat/until-destroy": ["libs/until-destroy/src/index.ts"], 6 | "@ngneat/until-destroy-migration": ["libs/until-destroy-migration/src/index.ts"] 7 | }, 8 | "target": "es5", 9 | "outDir": "dist/", 10 | "lib": ["dom", "es2017"], 11 | "moduleResolution": "node", 12 | "strict": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/integration/src/app/pipe/pipe.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { I18nPipe } from './i18n.pipe'; 6 | import { PipeComponent } from './pipe.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | RouterModule.forChild([ 12 | { 13 | path: '', 14 | component: PipeComponent 15 | } 16 | ]) 17 | ], 18 | declarations: [I18nPipe, PipeComponent] 19 | }) 20 | export class PipeModule {} 21 | -------------------------------------------------------------------------------- /apps/integration/src/app/custom-method/custom-method.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { CustomMethodComponent } from './custom-method.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | RouterModule.forChild([ 11 | { 12 | path: '', 13 | component: CustomMethodComponent 14 | } 15 | ]) 16 | ], 17 | declarations: [CustomMethodComponent] 18 | }) 19 | export class CustomMethodModule {} 20 | -------------------------------------------------------------------------------- /apps/integration/src/app/directive/directive.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { HttpDirective } from './http.directive'; 6 | import { DirectiveComponent } from './directive.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | RouterModule.forChild([ 12 | { 13 | path: '', 14 | component: DirectiveComponent 15 | } 16 | ]) 17 | ], 18 | declarations: [HttpDirective, DirectiveComponent] 19 | }) 20 | export class DirectiveModule {} 21 | -------------------------------------------------------------------------------- /apps/integration/src/app/multiple-custom-methods/multiple-custom-methods.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { MultipleCustoMethodsComponent } from './multiple-custom-methods.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | CommonModule, 10 | RouterModule.forChild([ 11 | { 12 | path: '', 13 | component: MultipleCustoMethodsComponent 14 | } 15 | ]) 16 | ], 17 | declarations: [MultipleCustoMethodsComponent] 18 | }) 19 | export class MultipleCustoMethodsModule {} 20 | -------------------------------------------------------------------------------- /apps/integration/src/app/pipe/pipe.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that streams are unsubscribed when the pipe is 6 | destroyed. 7 |

8 | 9 |
{{ '' | i18n }}
10 | 11 |
12 | {{ pipeUnsubscribedText$ | async }} 13 |
14 |
15 |
16 | 17 | 20 |
21 | -------------------------------------------------------------------------------- /apps/integration/src/app/destroyable-provider/destroyable-provider.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { DestroyableProviderComponent } from './destroyable-provider.component'; 6 | import { ConnectionDirective } from './connection/connection.directive'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | RouterModule.forChild([ 12 | { 13 | path: '', 14 | component: DestroyableProviderComponent 15 | } 16 | ]) 17 | ], 18 | declarations: [DestroyableProviderComponent, ConnectionDirective] 19 | }) 20 | export class DestroyableProviderModule {} 21 | -------------------------------------------------------------------------------- /apps/integration/src/app/array-of-subscriptions/array-of-subscriptions.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { ArrayOfSubscriptionsComponent } from './array-of-subscriptions.component'; 6 | import { DocumentClickComponent } from './document-click/document-click.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | RouterModule.forChild([ 12 | { 13 | path: '', 14 | component: ArrayOfSubscriptionsComponent 15 | } 16 | ]) 17 | ], 18 | declarations: [DocumentClickComponent, ArrayOfSubscriptionsComponent] 19 | }) 20 | export class ArrayOfSubscriptionsModule {} 21 | -------------------------------------------------------------------------------- /apps/integration/src/app/inheritance/inheritance.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { InheritanceComponent } from './inheritance.component'; 6 | 7 | import { Issue61Component } from './issue-61/issue-61.component'; 8 | import { Issue97Component } from './issue-97/issue-97.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | RouterModule.forChild([ 14 | { 15 | path: '', 16 | component: InheritanceComponent, 17 | }, 18 | ]), 19 | ], 20 | declarations: [InheritanceComponent, Issue61Component, Issue97Component], 21 | }) 22 | export class InheritanceModule {} 23 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/inheritance.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('inheritance', () => { 4 | it('should click the button to toggle the issue#61 and show that subscriptions have been unsubscribed', () => { 5 | cy.visit('/inheritance') 6 | .get('[data-cy="toggle-issue-61"]') 7 | .click() 8 | .get('[data-cy="issue-61-status"]') 9 | .shouldHaveSuccessClass(); 10 | }); 11 | 12 | it('should click the button to toggle the issue#97 and show that subscriptions have been unsubscribed', () => { 13 | cy.visit('/inheritance') 14 | .get('[data-cy="toggle-issue-97"]') 15 | .click() 16 | .get('[data-cy="issue-97-status"]') 17 | .shouldHaveSuccessClass(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/integration/src/app/array-of-subscriptions/array-of-subscriptions.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that subscriptions, stored in array, are 6 | unsubscribed. 7 |

8 | 9 | 10 | 11 |
12 | {{ documentClickUnsubscribedText$ | async }} 13 |
14 |
15 |
16 | 17 | 20 |
21 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngneat/until-destroy-migration", 3 | "description": "The script that helps to migrate from View Engine to Ivy", 4 | "version": "10.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/ngneat/until-destroy.git" 8 | }, 9 | "license": "MIT", 10 | "homepage": "https://github.com/ngneat/until-destroy#readme", 11 | "bugs": { 12 | "url": "https://github.com/ngneat/until-destroy/issues" 13 | }, 14 | "maintainers": [ 15 | "Netanel Basal", 16 | "Artur Androsovych" 17 | ], 18 | "sideEffects": false, 19 | "dependencies": { 20 | "glob": "^7.1.6", 21 | "minimist": "1.2.6", 22 | "ts-morph": "^7.1.2" 23 | }, 24 | "bin": "index.js" 25 | } 26 | -------------------------------------------------------------------------------- /apps/integration/src/app/directive/directive.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that streams are unsubscribed when directives are 6 | destroyed. 7 |

8 | 9 |
10 | {{ response }} 11 |
12 | 13 |
14 | {{ directiveUnsubscribedText$ | async }} 15 |
16 |
17 |
18 | 19 | 22 |
23 | -------------------------------------------------------------------------------- /libs/until-destroy/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "until-destroy", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/until-destroy/src", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/angular:package", 9 | "options": { 10 | "tsConfig": "libs/until-destroy/tsconfig.lib.json", 11 | "project": "libs/until-destroy/ng-package.json" 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nx/jest:jest", 16 | "options": { 17 | "jestConfig": "libs/until-destroy/jest.config.ts" 18 | } 19 | }, 20 | "lint": { 21 | "executor": "@nx/eslint:lint", 22 | "outputs": ["{options.outputFile}"] 23 | } 24 | }, 25 | "tags": ["lib"], 26 | "implicitDependencies": [] 27 | } 28 | -------------------------------------------------------------------------------- /libs/until-destroy/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "es2015", 6 | "inlineSources": true, 7 | "declaration": true, 8 | "declarationMap": false, 9 | "importHelpers": true, 10 | "lib": ["dom", "es2018"], 11 | "types": ["node"], 12 | "useDefineForClassFields": false 13 | }, 14 | "angularCompilerOptions": { 15 | "compilationMode": "partial", 16 | "annotateForClosureCompiler": true, 17 | "skipTemplateCodegen": true, 18 | "strictMetadataEmit": true, 19 | "fullTemplateTypeCheck": true, 20 | "strictInjectionParameters": true, 21 | "enableResourceInlining": true 22 | }, 23 | "include": ["**/*.ts"], 24 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /apps/integration/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | displayName: 'integration', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../coverage/apps/integration', 8 | snapshotSerializers: [ 9 | 'jest-preset-angular/build/serializers/no-ng-attributes', 10 | 'jest-preset-angular/build/serializers/ng-snapshot', 11 | 'jest-preset-angular/build/serializers/html-comment', 12 | ], 13 | transform: { 14 | '^.+.(ts|mjs|js|html)$': [ 15 | 'jest-preset-angular', 16 | { 17 | stringifyContentPathRegex: '\\.(html|svg)$', 18 | tsconfig: '/tsconfig.spec.json', 19 | }, 20 | ], 21 | }, 22 | transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/until-destroy/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | displayName: 'until-destroy', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../coverage/libs/until-destroy', 8 | snapshotSerializers: [ 9 | 'jest-preset-angular/build/serializers/no-ng-attributes', 10 | 'jest-preset-angular/build/serializers/ng-snapshot', 11 | 'jest-preset-angular/build/serializers/html-comment', 12 | ], 13 | transform: { 14 | '^.+.(ts|mjs|js|html)$': [ 15 | 'jest-preset-angular', 16 | { 17 | stringifyContentPathRegex: '\\.(html|svg)$', 18 | tsconfig: '/tsconfig.spec.json', 19 | }, 20 | ], 21 | }, 22 | transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/until-destroy/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/no-output-native": ["off"], 13 | "@typescript-eslint/no-empty-function": ["off"], 14 | "@typescript-eslint/no-non-null-assertion": ["off"], 15 | "@nx/enforce-module-boundaries": [ 16 | "error", 17 | { 18 | "allow": [] 19 | } 20 | ] 21 | } 22 | }, 23 | { 24 | "files": ["*.html"], 25 | "extends": ["plugin:@nx/angular-template"], 26 | "rules": {} 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | displayName: 'until-destroy-migration', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../coverage/libs/until-destroy-migration', 8 | transform: { 9 | '^.+.(ts|mjs|js|html)$': [ 10 | 'jest-preset-angular', 11 | { 12 | tsconfig: '/tsconfig.spec.json', 13 | stringifyContentPathRegex: '\\.(html|svg)$', 14 | }, 15 | ], 16 | }, 17 | transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/serializers/no-ng-attributes', 20 | 'jest-preset-angular/build/serializers/ng-snapshot', 21 | 'jest-preset-angular/build/serializers/html-comment', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/integration/src/app/destroyable-provider/destroyable-provider.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that streams are unsubscribed when 6 | `ngOnDestroy()` is called on providers that are bound to components and directives 7 | (that are declared in `providers` property of components and directives). 8 |

9 | 10 | 11 | 12 |
13 | {{ providerStatusText$ | async }} 14 |
15 |
16 |
17 | 18 | 21 |
22 | -------------------------------------------------------------------------------- /apps/integration/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // We have locally installed RxJS@7, which has breaking changes compared to RxJS@6. 4 | // For instance, operators can be imported directly from `rxjs`. RxJS@6 operators can be 5 | // imported only from `rxjs/operators`. This Webpack config aliases locally installed RxJS@7 to 6 | // RxJS@6 to ensure that no operators are imported from `rxjs`; the build will fail. 7 | // This might happen when some IDE (like VSCode) autocompletes imports for RxJS operators. 8 | 9 | module.exports = (config, options, { target }) => { 10 | if (target === 'build-rxjs-6') { 11 | config.resolve.alias = { 12 | ...config.resolve.alias, 13 | // Note: `require.resolve` will resolve to `node_modules/.pnpm` folder. 14 | rxjs: path.join(__dirname, '../../node_modules/rxjs-6'), 15 | }; 16 | } 17 | 18 | return config; 19 | }; 20 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "until-destroy-migration", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/until-destroy-migration/src", 6 | "targets": { 7 | "build": { 8 | "executor": "nx:run-commands", 9 | "options": { 10 | "commands": [ 11 | "tsc -p libs/until-destroy-migration/tsconfig.lib.json", 12 | "cpx libs/until-destroy-migration/package.json dist/libs/until-destroy-migration" 13 | ] 14 | } 15 | }, 16 | "test": { 17 | "executor": "@nx/jest:jest", 18 | "options": { 19 | "jestConfig": "libs/until-destroy-migration/jest.config.ts" 20 | } 21 | }, 22 | "lint": { 23 | "executor": "@nx/eslint:lint", 24 | "outputs": ["{options.outputFile}"] 25 | } 26 | }, 27 | "tags": ["lib"], 28 | "implicitDependencies": [] 29 | } 30 | -------------------------------------------------------------------------------- /libs/until-destroy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "name": "@ngneat/until-destroy", 4 | "description": "RxJS operator that unsubscribes when Angular component is destroyed", 5 | "version": "10.0.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/ngneat/until-destroy.git" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/ngneat/until-destroy#readme", 12 | "bugs": { 13 | "url": "https://github.com/ngneat/until-destroy/issues" 14 | }, 15 | "maintainers": [ 16 | "Netanel Basal", 17 | "Artur Androsovych" 18 | ], 19 | "keywords": [ 20 | "Angular easy unsubscribe", 21 | "RxJS easy unsubscribe", 22 | "RxJS operator unsubscribe", 23 | "Angular unsubscribe" 24 | ], 25 | "sideEffects": false, 26 | "peerDependencies": { 27 | "@angular/core": ">=13", 28 | "rxjs": "^6.4.0 || ^7.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/integration-e2e/src/integration/multiple-custom-methods.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('multiple custom methods', () => { 4 | it('should toggle streams and show the appropriate statuses', () => { 5 | cy.visit('/multiple-custom-methods'); 6 | 7 | const repetable = () => { 8 | cy.get('[data-cy="start-first"]').click(); 9 | cy.get('[data-cy="start-second"]').click(); 10 | 11 | cy.get('[data-cy="issue-66-status-first-stream"]').shouldHaveDangerClass(); 12 | cy.get('[data-cy="issue-66-status-second-stream"]').shouldHaveDangerClass(); 13 | 14 | cy.get('[data-cy="stop-first"]').click(); 15 | cy.get('[data-cy="stop-second"]').click(); 16 | 17 | cy.get('[data-cy="issue-66-status-first-stream"]').shouldHaveSuccessClass(); 18 | cy.get('[data-cy="issue-66-status-second-stream"]').shouldHaveSuccessClass(); 19 | }; 20 | 21 | repetable(); 22 | repetable(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/integration/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], 6 | transform: { 7 | '^.+\\.(ts|js|html)$': 'ts-jest', 8 | }, 9 | resolver: '@nx/jest/plugins/resolver', 10 | moduleFileExtensions: ['ts', 'js', 'html'], 11 | coverageReporters: ['html', 'lcov'], 12 | /* TODO: Update to latest Jest snapshotFormat 13 | * By default Nx has kept the older style of Jest Snapshot formats 14 | * to prevent breaking of any existing tests with snapshots. 15 | * It's recommend you update to the latest format. 16 | * You can do this by removing snapshotFormat property 17 | * and running tests with --update-snapshot flag. 18 | * Example: "nx affected --targets=test --update-snapshot" 19 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 20 | */ 21 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 22 | }; 23 | -------------------------------------------------------------------------------- /libs/until-destroy-migration/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngneat", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngneat", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/integration/src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-navbar', 5 | templateUrl: './navbar.component.html', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class NavbarComponent { 9 | links = [ 10 | { 11 | title: 'Pipe example', 12 | url: '/pipe' 13 | }, 14 | { 15 | title: 'Custom method example', 16 | url: '/custom-method' 17 | }, 18 | { 19 | title: 'Directive example', 20 | url: '/directive' 21 | }, 22 | { 23 | title: 'Inheritance example', 24 | url: '/inheritance' 25 | }, 26 | { 27 | title: 'Destroyable provider example', 28 | url: '/destroyable-provider' 29 | }, 30 | { 31 | title: 'Array of subscriptions example', 32 | url: '/array-of-subscriptions' 33 | }, 34 | { 35 | title: 'Multiple custom methods example', 36 | url: '/multiple-custom-methods' 37 | } 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /apps/integration/src/app/pipe/pipe.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | import { NotificationClass, NotificationText } from '../enums/notification.enum'; 6 | 7 | @Component({ 8 | selector: 'app-pipe', 9 | templateUrl: './pipe.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class PipeComponent { 13 | shown = true; 14 | 15 | pipeUnsubscribed$ = new BehaviorSubject(false); 16 | 17 | pipeUnsubscribedClass$ = this.pipeUnsubscribed$.pipe( 18 | map(pipeUnsubscribed => 19 | pipeUnsubscribed ? NotificationClass.Success : NotificationClass.Danger 20 | ) 21 | ); 22 | 23 | pipeUnsubscribedText$ = this.pipeUnsubscribed$.pipe( 24 | map(pipeUnsubscribed => 25 | pipeUnsubscribed ? NotificationText.Unsubscribed : NotificationText.Subscribed 26 | ) 27 | ); 28 | 29 | toggle(): void { 30 | this.shown = !this.shown; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/integration/src/app/pipe/i18n.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Host, Pipe, PipeTransform } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy'; 5 | 6 | import { PipeComponent } from './pipe.component'; 7 | import { LoggerFactory } from '../logger/logger.factory'; 8 | 9 | @UntilDestroy() 10 | @Pipe({ name: 'i18n', pure: false }) 11 | export class I18nPipe implements PipeTransform { 12 | constructor(@Host() host: PipeComponent, loggerFactory: LoggerFactory) { 13 | host.pipeUnsubscribed$.next(false); 14 | 15 | const logger = loggerFactory.createLogger('I18nPipe', 'green'); 16 | 17 | new Subject() 18 | .pipe( 19 | untilDestroyed(this), 20 | finalize(() => { 21 | host.pipeUnsubscribed$.next(true); 22 | logger.log('subject has been unsubscribed'); 23 | }) 24 | ) 25 | .subscribe(); 26 | } 27 | 28 | transform(): string { 29 | return 'I have been piped'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/integration/src/app/custom-method/interval.service.ts: -------------------------------------------------------------------------------- 1 | import { untilDestroyed } from '@ngneat/until-destroy'; 2 | import { BehaviorSubject, interval, Subject } from 'rxjs'; 3 | import { finalize } from 'rxjs/operators'; 4 | 5 | import { LoggerFactory } from '../logger/logger.factory'; 6 | 7 | export class IntervalService { 8 | interval$ = new Subject(); 9 | 10 | constructor( 11 | loggerFactory: LoggerFactory, 12 | intervalServiceUnsubscribed$: BehaviorSubject 13 | ) { 14 | const logger = loggerFactory.createLogger('IntervalService', '#7d00a5'); 15 | 16 | intervalServiceUnsubscribed$.next(false); 17 | 18 | interval(1000) 19 | .pipe( 20 | untilDestroyed(this, 'destroy'), 21 | finalize(() => { 22 | intervalServiceUnsubscribed$.next(true); 23 | logger.log('interval has been unsubscribed'); 24 | }) 25 | ) 26 | .subscribe(value => { 27 | logger.log(`interval emits value ${value}`); 28 | this.interval$.next(value); 29 | }); 30 | } 31 | 32 | destroy(): void {} 33 | } 34 | -------------------------------------------------------------------------------- /apps/integration/src/app/directive/directive.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | import { NotificationClass, NotificationText } from '../enums/notification.enum'; 6 | 7 | @Component({ 8 | selector: 'app-directive', 9 | templateUrl: './directive.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class DirectiveComponent { 13 | shown = true; 14 | 15 | directiveUnsubscribed$ = new BehaviorSubject(false); 16 | 17 | directiveUnsubscribedClass$ = this.directiveUnsubscribed$.pipe( 18 | map(directiveUnsubscribed => 19 | directiveUnsubscribed ? NotificationClass.Success : NotificationClass.Danger 20 | ) 21 | ); 22 | 23 | directiveUnsubscribedText$ = this.directiveUnsubscribed$.pipe( 24 | map(directiveUnsubscribed => 25 | directiveUnsubscribed ? NotificationText.Unsubscribed : NotificationText.Subscribed 26 | ) 27 | ); 28 | 29 | toggle(): void { 30 | this.shown = !this.shown; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Netanel Basal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/integration/src/app/array-of-subscriptions/array-of-subscriptions.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { NotificationClass, NotificationText } from '../enums/notification.enum'; 5 | 6 | @Component({ 7 | selector: 'app-array-of-subscriptions', 8 | templateUrl: './array-of-subscriptions.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ArrayOfSubscriptionsComponent { 12 | shown = true; 13 | 14 | documentClickUnsubscribed$ = new BehaviorSubject(false); 15 | 16 | documentClickUnsubscribedClass$ = this.documentClickUnsubscribed$.pipe( 17 | map(documentClickUnsubscribed => 18 | documentClickUnsubscribed ? NotificationClass.Success : NotificationClass.Danger 19 | ) 20 | ); 21 | 22 | documentClickUnsubscribedText$ = this.documentClickUnsubscribed$.pipe( 23 | map(documentClickUnsubscribed => 24 | documentClickUnsubscribed ? NotificationText.Unsubscribed : NotificationText.Subscribed 25 | ) 26 | ); 27 | 28 | toggle(): void { 29 | this.shown = !this.shown; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/integration/src/app/inheritance/issue-97/issue-97.component.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Component, Host } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy'; 5 | 6 | import { LoggerFactory } from '../../logger/logger.factory'; 7 | import { InheritanceComponent } from '../inheritance.component'; 8 | 9 | @UntilDestroy() 10 | @Directive() 11 | export abstract class Issue97Directive {} 12 | 13 | @Component({ 14 | selector: 'app-issue-97', 15 | template: '' 16 | }) 17 | export class Issue97Component extends Issue97Directive { 18 | constructor(loggerFactory: LoggerFactory, @Host() host: InheritanceComponent) { 19 | super(); 20 | 21 | host.issue97Status$.next({ 22 | componentUnsubscribed: false 23 | }); 24 | 25 | const logger = loggerFactory.createLogger('Issue97Component', '#bfb200'); 26 | 27 | new Subject() 28 | .pipe( 29 | untilDestroyed(this), 30 | finalize(() => { 31 | logger.log('subject has been unsubscribed'); 32 | host.issue97Status$.next({ 33 | componentUnsubscribed: true 34 | }); 35 | }) 36 | ) 37 | .subscribe(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our guidelines: https://github.com/ngneat/until-destroy/blob/master/CONTRIBUTING.md#commit 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | ``` 16 | [ ] Bugfix 17 | [ ] Feature 18 | [ ] Code style update (formatting, local variables) 19 | [ ] Refactoring (no functional changes, no api changes) 20 | [ ] Build related changes 21 | [ ] CI related changes 22 | [ ] Documentation content changes 23 | [ ] Other... Please describe: 24 | ``` 25 | 26 | ## What is the current behavior? 27 | 28 | 29 | 30 | Issue Number: N/A 31 | 32 | ## What is the new behavior? 33 | 34 | ## Does this PR introduce a breaking change? 35 | 36 | ``` 37 | [ ] Yes 38 | [ ] No 39 | ``` 40 | 41 | 42 | 43 | ## Other information 44 | -------------------------------------------------------------------------------- /apps/integration-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/integration-e2e/src", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nx/cypress:cypress", 9 | "options": { 10 | "cypressConfig": "apps/integration-e2e/cypress.json", 11 | "tsConfig": "apps/integration-e2e/tsconfig.e2e.json", 12 | "devServerTarget": "integration:serve:production" 13 | } 14 | }, 15 | "e2e-development": { 16 | "executor": "@nx/cypress:cypress", 17 | "options": { 18 | "cypressConfig": "apps/integration-e2e/cypress.json", 19 | "tsConfig": "apps/integration-e2e/tsconfig.e2e.json", 20 | "devServerTarget": "integration:serve:development" 21 | } 22 | }, 23 | "e2e-development-jit": { 24 | "executor": "@nx/cypress:cypress", 25 | "options": { 26 | "cypressConfig": "apps/integration-e2e/cypress.json", 27 | "tsConfig": "apps/integration-e2e/tsconfig.e2e.json", 28 | "devServerTarget": "integration:serve:development-jit" 29 | } 30 | }, 31 | "lint": { 32 | "executor": "@nx/eslint:lint", 33 | "outputs": ["{options.outputFile}"] 34 | } 35 | }, 36 | "tags": ["app"], 37 | "implicitDependencies": ["integration"] 38 | } 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx", "ban"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "ban/ban": [ 10 | "error", 11 | { 12 | "name": "fit", 13 | "message": "The fit is forbidden" 14 | }, 15 | { 16 | "name": "debugger", 17 | "message": "The debugger is forbidden" 18 | }, 19 | { 20 | "name": "fdescribe", 21 | "message": "The fdescribe is forbidden" 22 | } 23 | ], 24 | "@nx/enforce-module-boundaries": [ 25 | "error", 26 | { 27 | "enforceBuildableLibDependency": true, 28 | "allow": [], 29 | "depConstraints": [ 30 | { 31 | "sourceTag": "*", 32 | "onlyDependOnLibsWithTags": ["*"] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | }, 39 | { 40 | "files": ["*.ts", "*.tsx"], 41 | "extends": ["plugin:@nx/typescript"], 42 | "parserOptions": { 43 | "project": "./tsconfig.*?.json" 44 | }, 45 | "rules": {} 46 | }, 47 | { 48 | "files": ["*.js", "*.jsx"], 49 | "extends": ["plugin:@nx/javascript"], 50 | "rules": {} 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /apps/integration/src/app/multiple-custom-methods/multiple-custom-methods.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that it's possible to use multiple custom methods 6 | that can be passed into `untilDestroyed` operator. 7 |

8 | 9 |
10 | 13 | 16 | 19 | 22 |
23 | 24 |

Issue#66 status:

25 | 26 |
27 | {{ firstStreamStatusText$ | async }} 28 |
29 |
30 | {{ secondStreamStatusText$ | async }} 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /apps/integration/src/app/custom-method/custom-method.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that it's possible to pass custom method to 6 | `untilDestroyed` operator, that will be called upon destroy. Given such objectives: 7 |

8 |
    9 |
  1. `IntervalService` subscribes to `interval` stream and has a `destroy()` method
  2. 10 |
  3. 11 | `IntervalService` has an `interval$` stream which emits values forward from 12 | `interval` stream 13 |
  4. 14 |
  5. `CustomMethodComponent` subscribes to `IntervalService.interval$`
  6. 15 |
  7. 16 | `CustomMethodComponent` calls `destroy()` on the `IntervalService` when the below 17 | button is clicked 18 |
  8. 19 |
20 | 21 |
25 | {{ intervalServiceUnsubscribedText$ | async }} 26 |
27 | 28 |
29 | Value from interval service {{ valueFromIntervalService$ | async }} 30 |
31 |
32 |
33 | 34 | 37 |
38 | -------------------------------------------------------------------------------- /apps/integration/src/app/destroyable-provider/destroyable-provider.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | import { NotificationClass, NotificationText } from '../enums/notification.enum'; 5 | 6 | @Component({ 7 | selector: 'app-destroyable-provider', 8 | templateUrl: './destroyable-provider.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class DestroyableProviderComponent { 12 | shown = true; 13 | 14 | providerStatus$ = new BehaviorSubject({ 15 | subjectIsUnsubscribed: false, 16 | subscriptionIsUnsubscribed: false 17 | }); 18 | 19 | providerStatusClass$ = this.providerStatus$.pipe( 20 | map(({ subjectIsUnsubscribed, subscriptionIsUnsubscribed }) => { 21 | if (subjectIsUnsubscribed && subscriptionIsUnsubscribed) { 22 | return NotificationClass.Success; 23 | } else { 24 | return NotificationClass.Danger; 25 | } 26 | }) 27 | ); 28 | 29 | providerStatusText$ = this.providerStatus$.pipe( 30 | map(({ subjectIsUnsubscribed, subscriptionIsUnsubscribed }) => { 31 | if (subjectIsUnsubscribed && subscriptionIsUnsubscribed) { 32 | return NotificationText.Unsubscribed; 33 | } else { 34 | return NotificationText.Subscribed; 35 | } 36 | }) 37 | ); 38 | 39 | toggle(): void { 40 | this.shown = !this.shown; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/integration/src/app/inheritance/inheritance.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | The purpose of this test is to ensure that inheritance from directives and abstract 6 | classes will work as expected. Note that there have been different issues: 7 |

8 |
    9 |
  1. 10 | `Issue61Component` extends from the `Issue61BaseDirective`. Component is decorated 11 | with `@UntilDestroy()` but directive is not 12 |
  2. 13 |
  3. 14 | `Issue97Component` extends from the `Issue97Directive`. Directive is decorated with 15 | `@UntilDestroy()` but component is not 16 |
  4. 17 |
18 | 19 | 20 | 21 | 22 |
23 | Issue#61: {{ issue61StatusText$ | async }} 24 |
25 | 26 |
27 | Issue#97: {{ issue97StatusText$ | async }} 28 |
29 |
30 |
31 | 32 | 35 | 38 |
39 | -------------------------------------------------------------------------------- /apps/integration/src/app/destroyable-provider/connection/connection.service.ts: -------------------------------------------------------------------------------- 1 | import { Host, Injectable } from '@angular/core'; 2 | import { Subject, Subscription } from 'rxjs'; 3 | import { finalize } from 'rxjs/operators'; 4 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 5 | 6 | import { LoggerFactory } from '../../logger/logger.factory'; 7 | import { DestroyableProviderComponent } from '../destroyable-provider.component'; 8 | 9 | @UntilDestroy({ checkProperties: true }) 10 | @Injectable() 11 | export class ConnectionService { 12 | subscription: Subscription; 13 | 14 | private logger = this.loggerFactory.createLogger('ConnectionService', '#009688'); 15 | 16 | constructor( 17 | private loggerFactory: LoggerFactory, 18 | @Host() host: DestroyableProviderComponent 19 | ) { 20 | host.providerStatus$.next({ 21 | subjectIsUnsubscribed: false, 22 | subscriptionIsUnsubscribed: false 23 | }); 24 | 25 | this.subscription = new Subject() 26 | .pipe( 27 | finalize(() => { 28 | this.logger.log('The first ConnectionService subject has been unsubscribed'); 29 | host.providerStatus$.next({ 30 | ...host.providerStatus$.getValue(), 31 | subscriptionIsUnsubscribed: true 32 | }); 33 | }) 34 | ) 35 | .subscribe(); 36 | 37 | new Subject() 38 | .pipe( 39 | untilDestroyed(this), 40 | finalize(() => { 41 | this.logger.log('The second ConnectionService subject has been unsubscribed'); 42 | host.providerStatus$.next({ 43 | ...host.providerStatus$.getValue(), 44 | subjectIsUnsubscribed: true 45 | }); 46 | }) 47 | ) 48 | .subscribe(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/until-destroy.yml: -------------------------------------------------------------------------------- 1 | name: '@ngneat/until-destroy' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Get pnpm store directory 19 | id: pnpm-cache 20 | shell: bash 21 | run: | 22 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 23 | 24 | - uses: actions/cache@v3 25 | id: pnpm-cache 26 | with: 27 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 28 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pnpm-store- 31 | 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: '16' 35 | 36 | - uses: pnpm/action-setup@v2.2.4 37 | with: 38 | version: 7.13.4 39 | 40 | - name: Install dependencies 41 | run: pnpm install --frozen-lockfile 42 | 43 | - name: Run ESLint 44 | run: pnpm lint 45 | 46 | - name: Run unit tests 47 | run: pnpm test 48 | 49 | - name: Run integration tests 50 | run: pnpm test:integration 51 | 52 | - name: Build library 53 | run: pnpm build 54 | 55 | - name: Build integration app in production mode 56 | run: pnpm build:integration 57 | 58 | - name: Build integration app in production mode with RxJS@6 59 | run: pnpm nx run integration:build-rxjs-6 60 | 61 | - run: pnpm nx e2e integration-e2e 62 | - run: pnpm nx e2e-development integration-e2e 63 | - run: pnpm nx e2e-development-jit integration-e2e 64 | -------------------------------------------------------------------------------- /apps/integration/src/app/directive/http.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Host, TemplateRef, ViewContainerRef } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { UntilDestroy } from '@ngneat/until-destroy'; 4 | import { interval, of, Subscription } from 'rxjs'; 5 | import { switchMap, catchError, finalize } from 'rxjs/operators'; 6 | 7 | import { LoggerFactory } from '../logger/logger.factory'; 8 | import { DirectiveComponent } from './directive.component'; 9 | 10 | class Context { 11 | constructor(public $implicit: string | null) {} 12 | } 13 | 14 | @UntilDestroy({ checkProperties: true }) 15 | @Directive({ selector: '[http]' }) 16 | export class HttpDirective { 17 | subscription: Subscription; 18 | 19 | constructor( 20 | http: HttpClient, 21 | loggerFactory: LoggerFactory, 22 | @Host() host: DirectiveComponent, 23 | viewContainerRef: ViewContainerRef, 24 | templateRef: TemplateRef 25 | ) { 26 | host.directiveUnsubscribed$.next(false); 27 | 28 | const logger = loggerFactory.createLogger('HttpDirective', 'red'); 29 | 30 | const viewRef = viewContainerRef.createEmbeddedView(templateRef, new Context(null)); 31 | 32 | this.subscription = interval(1000) 33 | .pipe( 34 | switchMap(() => 35 | http 36 | .get('https://jsonplaceholder.typicode.com/users') 37 | .pipe(catchError(() => of([]))) 38 | ), 39 | finalize(() => { 40 | logger.log('interval has been unsubscribed'); 41 | host.directiveUnsubscribed$.next(true); 42 | }) 43 | ) 44 | .subscribe(response => { 45 | viewRef.context.$implicit = `Received such number of users: ${response.length}`; 46 | viewRef.detectChanges(); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/integration/src/app/inheritance/inheritance.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | import { NotificationClass, NotificationText } from '../enums/notification.enum'; 6 | 7 | @Component({ 8 | selector: 'app-inheritance', 9 | templateUrl: './inheritance.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | }) 12 | export class InheritanceComponent { 13 | issue61Shown = true; 14 | issue97Shown = true; 15 | 16 | issue61Status$ = new BehaviorSubject({ 17 | directiveUnsubscribed: false, 18 | componentUnsubscribed: false, 19 | }); 20 | 21 | issue61StatusClass$ = this.issue61Status$.pipe( 22 | map(({ directiveUnsubscribed, componentUnsubscribed }) => { 23 | if (directiveUnsubscribed && componentUnsubscribed) { 24 | return NotificationClass.Success; 25 | } else { 26 | return NotificationClass.Danger; 27 | } 28 | }) 29 | ); 30 | 31 | issue61StatusText$ = this.issue61Status$.pipe( 32 | map(({ directiveUnsubscribed, componentUnsubscribed }) => { 33 | if (directiveUnsubscribed && componentUnsubscribed) { 34 | return NotificationText.Unsubscribed; 35 | } else { 36 | return NotificationText.Subscribed; 37 | } 38 | }) 39 | ); 40 | 41 | issue97Status$ = new BehaviorSubject({ 42 | componentUnsubscribed: false, 43 | }); 44 | 45 | issue97StatusClass$ = this.issue97Status$.pipe( 46 | map(({ componentUnsubscribed }) => 47 | componentUnsubscribed ? NotificationClass.Success : NotificationClass.Danger 48 | ) 49 | ); 50 | 51 | issue97StatusText$ = this.issue97Status$.pipe( 52 | map(({ componentUnsubscribed }) => 53 | componentUnsubscribed ? NotificationText.Unsubscribed : NotificationText.Subscribed 54 | ) 55 | ); 56 | 57 | toggleIssue61(): void { 58 | this.issue61Shown = !this.issue61Shown; 59 | } 60 | 61 | toggleIssue97(): void { 62 | this.issue97Shown = !this.issue97Shown; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/integration/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | import { AppComponent } from './app.component'; 7 | import { NavbarModule } from './navbar/navbar.module'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | HttpClientModule, 13 | RouterModule.forRoot( 14 | [ 15 | { 16 | path: 'pipe', 17 | loadChildren: () => import('./pipe/pipe.module').then(m => m.PipeModule), 18 | }, 19 | { 20 | path: 'custom-method', 21 | loadChildren: () => 22 | import('./custom-method/custom-method.module').then(m => m.CustomMethodModule), 23 | }, 24 | { 25 | path: 'directive', 26 | loadChildren: () => 27 | import('./directive/directive.module').then(m => m.DirectiveModule), 28 | }, 29 | { 30 | path: 'inheritance', 31 | loadChildren: () => 32 | import('./inheritance/inheritance.module').then(m => m.InheritanceModule), 33 | }, 34 | { 35 | path: 'destroyable-provider', 36 | loadChildren: () => 37 | import('./destroyable-provider/destroyable-provider.module').then( 38 | m => m.DestroyableProviderModule 39 | ), 40 | }, 41 | { 42 | path: 'array-of-subscriptions', 43 | loadChildren: () => 44 | import('./array-of-subscriptions/array-of-subscriptions.module').then( 45 | m => m.ArrayOfSubscriptionsModule 46 | ), 47 | }, 48 | { 49 | path: 'multiple-custom-methods', 50 | loadChildren: () => 51 | import('./multiple-custom-methods/multiple-custom-methods.module').then( 52 | m => m.MultipleCustoMethodsModule 53 | ), 54 | }, 55 | ], 56 | {} 57 | ), 58 | NavbarModule, 59 | ], 60 | declarations: [AppComponent], 61 | bootstrap: [AppComponent], 62 | }) 63 | export class AppModule {} 64 | -------------------------------------------------------------------------------- /apps/integration/src/app/inheritance/issue-61/issue-61.component.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Component, Host } from '@angular/core'; 2 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 3 | import { interval } from 'rxjs'; 4 | import { finalize } from 'rxjs/operators'; 5 | 6 | import { LoggerFactory } from '../../logger/logger.factory'; 7 | import { InheritanceComponent } from '../inheritance.component'; 8 | 9 | @Directive() 10 | export abstract class Issue61BaseDirective { 11 | constructor(loggerFactory: LoggerFactory, host: InheritanceComponent) { 12 | host.issue61Status$.next({ 13 | ...host.issue61Status$.getValue(), 14 | directiveUnsubscribed: false 15 | }); 16 | 17 | const logger = loggerFactory.createLogger('Issue61BaseDirective', '#f58900'); 18 | 19 | interval(1000) 20 | .pipe( 21 | untilDestroyed(this), 22 | finalize(() => { 23 | logger.log('interval has been unsubscribed'); 24 | host.issue61Status$.next({ 25 | ...host.issue61Status$.getValue(), 26 | directiveUnsubscribed: true 27 | }); 28 | }) 29 | ) 30 | .subscribe(value => logger.log(`has emitted value ${value}`)); 31 | } 32 | } 33 | 34 | @UntilDestroy() 35 | @Component({ 36 | selector: 'app-issue-61', 37 | template: '' 38 | }) 39 | export class Issue61Component extends Issue61BaseDirective { 40 | constructor(loggerFactory: LoggerFactory, @Host() host: InheritanceComponent) { 41 | super(loggerFactory, host); 42 | 43 | host.issue61Status$.next({ 44 | ...host.issue61Status$.getValue(), 45 | componentUnsubscribed: false 46 | }); 47 | 48 | const logger = loggerFactory.createLogger('Issue61Component', '#f58900'); 49 | 50 | interval(1000) 51 | .pipe( 52 | untilDestroyed(this), 53 | finalize(() => { 54 | logger.log('subject has been unsubscribed'); 55 | host.issue61Status$.next({ 56 | ...host.issue61Status$.getValue(), 57 | componentUnsubscribed: true 58 | }); 59 | }) 60 | ) 61 | .subscribe(value => { 62 | logger.log(`has emitted value ${value}`); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /libs/until-destroy/src/lib/internals.ts: -------------------------------------------------------------------------------- 1 | import { InjectableType, ɵDirectiveType, ɵComponentType } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | import { PipeType } from './ivy'; 5 | 6 | /** 7 | * Applied to instances and stores `Subject` instance when 8 | * no custom destroy method is provided. 9 | */ 10 | const DESTROY: unique symbol = Symbol('__destroy'); 11 | 12 | /** 13 | * Applied to definitions and informs that class is decorated 14 | */ 15 | export const DECORATOR_APPLIED: unique symbol = Symbol('__decoratorApplied'); 16 | 17 | /** 18 | * If we use the `untilDestroyed` operator multiple times inside the single 19 | * instance providing different `destroyMethodName`, then all streams will 20 | * subscribe to the single subject. If any method is invoked, the subject will 21 | * emit and all streams will be unsubscribed. We wan't to prevent this behavior, 22 | * thus we store subjects under different symbols. 23 | */ 24 | export function getSymbol(destroyMethodName?: keyof T): symbol { 25 | if (typeof destroyMethodName === 'string') { 26 | return Symbol(`__destroy__${destroyMethodName}`); 27 | } else { 28 | return DESTROY; 29 | } 30 | } 31 | 32 | export function markAsDecorated( 33 | type: InjectableType | PipeType | ɵDirectiveType | ɵComponentType 34 | ): void { 35 | // Store this property on the prototype if it's an injectable class, component or directive. 36 | // We will be able to handle class extension this way. 37 | type.prototype[DECORATOR_APPLIED] = true; 38 | } 39 | 40 | export interface UntilDestroyOptions { 41 | blackList?: string[]; 42 | arrayName?: string; 43 | checkProperties?: boolean; 44 | } 45 | 46 | export function createSubjectOnTheInstance(instance: any, symbol: symbol): void { 47 | if (!instance[symbol]) { 48 | instance[symbol] = new Subject(); 49 | } 50 | } 51 | 52 | export function completeSubjectOnTheInstance(instance: any, symbol: symbol): void { 53 | if (instance[symbol]) { 54 | instance[symbol].next(); 55 | instance[symbol].complete(); 56 | // We also have to re-assign this property thus in the future 57 | // we will be able to create new subject on the same instance. 58 | instance[symbol] = null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "@nx/workspace/tasks-runners/default", 5 | "options": {} 6 | } 7 | }, 8 | "defaultProject": "until-destroy", 9 | "generators": { 10 | "@nx/angular:application": { 11 | "style": "scss", 12 | "linter": "eslint", 13 | "unitTestRunner": "jest", 14 | "e2eTestRunner": "cypress" 15 | }, 16 | "@nx/angular:library": { 17 | "style": "scss", 18 | "linter": "eslint", 19 | "unitTestRunner": "jest" 20 | }, 21 | "@nx/angular:component": { 22 | "style": "scss" 23 | } 24 | }, 25 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 26 | "namedInputs": { 27 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 28 | "sharedGlobals": [ 29 | "{workspaceRoot}/angular.json", 30 | "{workspaceRoot}/tsconfig.json", 31 | "{workspaceRoot}/tslint.json", 32 | "{workspaceRoot}/nx.json" 33 | ], 34 | "production": [ 35 | "default", 36 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 37 | "!{projectRoot}/tsconfig.spec.json", 38 | "!{projectRoot}/jest.config.[jt]s", 39 | "!{projectRoot}/.eslintrc.json", 40 | "!{projectRoot}/src/test-setup.[jt]s" 41 | ] 42 | }, 43 | "targetDefaults": { 44 | "build": { 45 | "inputs": ["production", "^production"], 46 | "cache": true 47 | }, 48 | "e2e": { 49 | "inputs": ["default", "^production"], 50 | "cache": true 51 | }, 52 | "e2e-development": { 53 | "inputs": ["default", "^production"] 54 | }, 55 | "e2e-development-jit": { 56 | "inputs": ["default", "^production"] 57 | }, 58 | "@nx/jest:jest": { 59 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 60 | "cache": true, 61 | "options": { 62 | "passWithNoTests": true 63 | }, 64 | "configurations": { 65 | "ci": { 66 | "ci": true, 67 | "codeCoverage": true 68 | } 69 | } 70 | }, 71 | "@nx/eslint:lint": { 72 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], 73 | "cache": true 74 | } 75 | }, 76 | "parallel": 1, 77 | "useInferencePlugins": false, 78 | "defaultBase": "master" 79 | } 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `until-destroy` 2 | 3 | 🙏 We would ❤️ for you to contribute to `until-destroy` and help make it even better than it is today! 4 | 5 | # Developing 6 | 7 | Start by installing all dependencies: 8 | 9 | ```bash 10 | pnpm install --frozen-lockfile 11 | ``` 12 | 13 | Run the tests: 14 | 15 | ```bash 16 | pnpm test 17 | ``` 18 | 19 | Run the tests in watch mode: 20 | 21 | ```bash 22 | pnpm test:watch 23 | ``` 24 | 25 | Run the playground app: 26 | 27 | ```bash 28 | pnpm serve:integration 29 | ``` 30 | 31 | Run the playground integration tests: 32 | 33 | ```bash 34 | pnpm test:integration 35 | ``` 36 | 37 | ## Building 38 | 39 | ```bash 40 | pnpm build 41 | ``` 42 | 43 | ## Coding Rules 44 | 45 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 46 | 47 | - All features or bug fixes **must be tested** by one or more specs (unit-tests). 48 | - All public API methods **must be documented**. 49 | 50 | ## Commit Message Guidelines 51 | 52 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 53 | readable messages** that are easy to follow when looking through the **project history**. But also, 54 | we use the git commit messages to **generate the Akita changelog**. 55 | 56 | ### Commit Message Format 57 | 58 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 59 | format that includes a **type**, a **scope** and a **subject**: 60 | 61 | ``` 62 | (): 63 | 64 | 65 | 66 |