├── .eslintignore ├── apps ├── demos │ ├── src │ │ ├── polyfills.ts │ │ ├── test-setup.ts │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── store │ │ │ │ ├── index.ts │ │ │ │ └── progress │ │ │ │ │ ├── progress.actions.ts │ │ │ │ │ └── progress.state.ts │ │ │ ├── progress │ │ │ │ ├── progress.component.html │ │ │ │ ├── progress.component.scss │ │ │ │ └── progress.component.ts │ │ │ ├── app.component.html │ │ │ ├── app.server.module.ts │ │ │ ├── app.module.ts │ │ │ └── app.component.ts │ │ ├── environments │ │ │ ├── environment.ts │ │ │ └── environment.prod.ts │ │ ├── index.html │ │ ├── main.server.ts │ │ ├── main.ts │ │ └── server.ts │ ├── tsconfig.editor.json │ ├── tsconfig.spec.json │ ├── tsconfig.server.json │ ├── tsconfig.app.json │ ├── .browserslistrc │ ├── jest.config.ts │ ├── tsconfig.json │ ├── .eslintrc.json │ └── project.json └── demos-e2e │ ├── tsconfig.json │ ├── src │ ├── plugins │ │ └── index.js │ └── e2e │ │ └── ssr.cy.ts │ ├── tsconfig.e2e.json │ ├── cypress.config.ts │ ├── .eslintrc.json │ └── project.json ├── .commitlintrc.json ├── libs └── select-snapshot │ ├── src │ ├── test-setup.ts │ ├── index.ts │ └── lib │ │ ├── core │ │ ├── decorators │ │ │ ├── select-snapshot.ts │ │ │ └── view-select-snapshot.ts │ │ └── internals │ │ │ ├── internals.ts │ │ │ ├── static-injector.ts │ │ │ └── select-snapshot.ts │ │ ├── select-snapshot.module.ts │ │ └── select-snapshot.spec.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── .eslintrc.json │ └── project.json ├── .husky ├── pre-commit └── commit-msg ├── jest.config.ts ├── .gitignore ├── .yarnrc.yml ├── tsconfig.base.json ├── jest.preset.js ├── LICENSE ├── .eslintrc.json ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── select-snapshot.yml └── ISSUE_TEMPLATE.md ├── nx.json ├── .yarn └── plugins │ └── @yarnpkg │ └── plugin-after-install.cjs ├── package.json ├── decorate-angular-cli.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /apps/demos/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | -------------------------------------------------------------------------------- /apps/demos/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /apps/demos/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | button { 2 | margin: 0 5px; 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /libs/select-snapshot/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 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /apps/demos/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/demos/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/demos/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './progress/progress.state'; 2 | export * from './progress/progress.actions'; 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | export default { 4 | projects: getJestProjects() 5 | }; 6 | -------------------------------------------------------------------------------- /apps/demos/src/app/store/progress/progress.actions.ts: -------------------------------------------------------------------------------- 1 | export class IncrementProgress { 2 | static readonly type = '[Progress] Increment'; 3 | } 4 | -------------------------------------------------------------------------------- /apps/demos/src/app/progress/progress.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /apps/demos/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/demos-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/demos-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 2 | 3 | module.exports = (on, config) => { 4 | on('file:preprocessor', preprocessTypescript(config)); 5 | }; 6 | -------------------------------------------------------------------------------- /libs/select-snapshot/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/select-snapshot", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/select-snapshot/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/index.ts: -------------------------------------------------------------------------------- 1 | export { SelectSnapshot } from './lib/core/decorators/select-snapshot'; 2 | export { ViewSelectSnapshot } from './lib/core/decorators/view-select-snapshot'; 3 | export { NgxsSelectSnapshotModule } from './lib/select-snapshot.module'; 4 | -------------------------------------------------------------------------------- /apps/demos-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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | dist 3 | node_modules 4 | .idea 5 | .vscode 6 | .cache 7 | yarn-error.log 8 | coverage 9 | 10 | migrations.json 11 | 12 | .pnp.* 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/plugins 16 | !.yarn/releases 17 | !.yarn/sdks 18 | !.yarn/versions 19 | 20 | .angular 21 | -------------------------------------------------------------------------------- /apps/demos/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/demos/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "types": ["node"] 6 | }, 7 | "files": ["src/main.server.ts", "src/server.ts"], 8 | "angularCompilerOptions": { 9 | "entryModule": "./src/app/app.server.module#AppServerModule" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/core/decorators/select-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { defineSelectSnapshotProperties } from '../internals/select-snapshot'; 2 | 3 | export function SelectSnapshot(selectorOrFeature?: any, ...paths: string[]) { 4 | return (type: any, name: string) => { 5 | defineSelectSnapshotProperties(selectorOrFeature, paths, type, name); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /libs/select-snapshot/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demos-e2e/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | video: false, 5 | chromeWebSecurity: false, 6 | fileServerFolder: '.', 7 | screenshotOnRunFailure: false, 8 | e2e: { 9 | supportFile: false, 10 | fixturesFolder: false, 11 | specPattern: './src/e2e/**/*.cy.ts' 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /apps/demos/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Progress is being updated using setInverval which shouldn't cause OnPush views gets updated, but 5 | our progress does 6 |

7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/demos-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/demos/src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [AppModule, ServerModule], 9 | bootstrap: [AppComponent] 10 | }) 11 | export class AppServerModule {} 12 | -------------------------------------------------------------------------------- /apps/demos/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node"], 5 | "module": "esnext", 6 | "target": "es2022", 7 | "useDefineForClassFields": false 8 | }, 9 | "files": ["src/main.ts", "src/polyfills.ts"], 10 | "include": ["src/**/*.d.ts"], 11 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/demos/.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 1 Chrome version 9 | -------------------------------------------------------------------------------- /libs/select-snapshot/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "inlineSources": true, 8 | "types": [] 9 | }, 10 | "exclude": ["src/test-setup.ts", "src/**/*.spec.ts", "jest.config.ts", "src/**/*.test.ts"], 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/demos/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Angular Universal @ngxs-labs/select-snapshot integration 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/demos/src/main.server.ts: -------------------------------------------------------------------------------- 1 | import '@angular/platform-server/init'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | export { AppServerModule } from './app/app.server.module'; 12 | export { renderModule, renderModuleFactory } from '@angular/platform-server'; 13 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | afterInstall: yarn ngcc && node ./decorate-angular-cli.js && yarn husky install 2 | 3 | nodeLinker: node-modules 4 | 5 | npmRegistryServer: "https://registry.npmjs.org" 6 | 7 | plugins: 8 | - path: .yarn/plugins/@yarnpkg/plugin-after-install.cjs 9 | spec: "https://raw.githubusercontent.com/mhassan1/yarn-plugin-after-install/v0.3.1/bundles/@yarnpkg/plugin-after-install.js" 10 | 11 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 12 | -------------------------------------------------------------------------------- /apps/demos/src/app/progress/progress.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 500px; 3 | display: flex; 4 | 5 | .progress-wrapper { 6 | width: 100%; 7 | background-color: #e0e0e0; 8 | padding: 3px; 9 | border-radius: 3px; 10 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); 11 | 12 | .progress { 13 | display: block; 14 | height: 22px; 15 | background-color: #659cef; 16 | border-radius: 3px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/demos/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 | document.addEventListener('DOMContentLoaded', async () => { 12 | try { 13 | await platformBrowserDynamic().bootstrapModule(AppModule); 14 | } catch (e) { 15 | console.error(e); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/select-snapshot.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders, NgModuleRef } from '@angular/core'; 2 | 3 | import { setInjector, clearInjector } from './core/internals/static-injector'; 4 | 5 | @NgModule() 6 | export class NgxsSelectSnapshotModule { 7 | constructor(ngModuleRef: NgModuleRef) { 8 | setInjector(ngModuleRef.injector); 9 | ngModuleRef.onDestroy(clearInjector); 10 | } 11 | 12 | static forRoot(): ModuleWithProviders { 13 | return { 14 | ngModule: NgxsSelectSnapshotModule, 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/select-snapshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngxs-labs/select-snapshot", 3 | "version": "5.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/ngxs-labs/select-snapshot.git" 7 | }, 8 | "license": "MIT", 9 | "homepage": "https://github.com/ngxs-labs/select-snapshot#readme", 10 | "bugs": { 11 | "url": "https://github.com/ngxs-labs/select-snapshot/issues" 12 | }, 13 | "keywords": [ 14 | "ngxs", 15 | "redux", 16 | "store" 17 | ], 18 | "sideEffects": false, 19 | "peerDependencies": { 20 | "@angular/core": ">=15.0.0", 21 | "@ngxs/store": ">=3.7.3", 22 | "rxjs": ">=6.5.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/demos/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxsSelectSnapshotModule } from '@ngxs-labs/select-snapshot'; 5 | 6 | import { ProgressState } from './store'; 7 | 8 | import { AppComponent } from './app.component'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | BrowserModule.withServerTransition({ appId: 'universal-select-snapshot' }), 13 | NgxsModule.forRoot([ProgressState]), 14 | NgxsSelectSnapshotModule.forRoot(), 15 | ], 16 | declarations: [AppComponent], 17 | bootstrap: [AppComponent], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /apps/demos-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demos-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/demos-e2e/src", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nrwl/cypress:cypress", 9 | "options": { 10 | "cypressConfig": "apps/demos-e2e/cypress.config.ts", 11 | "tsConfig": "apps/demos-e2e/tsconfig.e2e.json", 12 | "devServerTarget": "demos:serve-ssr:production" 13 | } 14 | }, 15 | "lint": { 16 | "executor": "@nrwl/linter:eslint", 17 | "options": { 18 | "lintFilePatterns": ["apps/demos-e2e/**/*.{js,ts}"] 19 | } 20 | } 21 | }, 22 | "implicitDependencies": ["demos"] 23 | } 24 | -------------------------------------------------------------------------------- /libs/select-snapshot/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'select-snapshot', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | transform: { 7 | '^.+\\.(ts|mjs|js|html)$': [ 8 | 'jest-preset-angular', 9 | { 10 | tsconfig: '/tsconfig.spec.json', 11 | stringifyContentPathRegex: '\\.(html|svg)$', 12 | }, 13 | ], 14 | }, 15 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 16 | snapshotSerializers: [ 17 | 'jest-preset-angular/build/serializers/no-ng-attributes', 18 | 'jest-preset-angular/build/serializers/ng-snapshot', 19 | 'jest-preset-angular/build/serializers/html-comment', 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /apps/demos/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'demos', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | transform: { 7 | '^.+\\.(ts|mjs|js|html)$': [ 8 | 'jest-preset-angular', 9 | { 10 | isolatedModules: true, 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$' 13 | } 14 | ] 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment' 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /apps/demos/src/app/progress/progress.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, HostBinding, OnDestroy } from '@angular/core'; 2 | import { ViewSelectSnapshot } from '@ngxs-labs/select-snapshot'; 3 | 4 | import { ProgressState } from '../store'; 5 | 6 | @Component({ 7 | selector: 'app-progress', 8 | templateUrl: './progress.component.html', 9 | styleUrls: ['./progress.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | standalone: true, 12 | }) 13 | export class ProgressComponent implements OnDestroy { 14 | @HostBinding('class.ivy-enabled') ivyEnabled!: boolean; 15 | 16 | @ViewSelectSnapshot(ProgressState.getProgress) progress!: number; 17 | 18 | ngOnDestroy(): void { 19 | console.log('Just ensuring that this hook is still called.'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "noImplicitOverride": true, 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "module": "esnext", 14 | "typeRoots": ["node_modules/@types", "typings"], 15 | "lib": ["es2020", "dom", "dom.iterable"], 16 | "skipLibCheck": true, 17 | "skipDefaultLibCheck": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@ngxs-labs/select-snapshot": ["libs/select-snapshot/src/index.ts"] 21 | } 22 | }, 23 | "angularCompilerOptions": { 24 | "strictTemplates": true 25 | }, 26 | "exclude": ["node_modules", "tmp"] 27 | } 28 | -------------------------------------------------------------------------------- /libs/select-snapshot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ], 22 | "extends": "../../tsconfig.base.json", 23 | "angularCompilerOptions": { 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/demos-e2e/src/e2e/ssr.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Server side rendering', () => { 4 | const indexUrl = '/'; 5 | 6 | it('should render `app-progress` component', () => { 7 | cy.visit(indexUrl).get('.progress-wrapper').should('be.visible'); 8 | }); 9 | 10 | it('should click the button and the progress should be updated', () => { 11 | let resolve: VoidFunction; 12 | 13 | const progressCompleted = new Promise(r => (resolve = r)); 14 | 15 | cy.visit(indexUrl) 16 | .then(window => { 17 | window.addEventListener('progressCompleted', () => { 18 | resolve(); 19 | }); 20 | }) 21 | .get('[data-cy=start-progress]') 22 | .click() 23 | .then(() => progressCompleted); 24 | 25 | cy.get('.progress').should('have.attr', 'style', 'width: 100%;'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/demos/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 | "path": "./tsconfig.server.json" 17 | } 18 | ], 19 | "compilerOptions": { 20 | "forceConsistentCasingInFileNames": true, 21 | "strict": true, 22 | "noImplicitOverride": true, 23 | "noPropertyAccessFromIndexSignature": false, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "target": "es2020" 27 | }, 28 | "angularCompilerOptions": { 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/demos/src/app/store/progress/progress.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State, Action, StateContext, Selector } from '@ngxs/store'; 3 | 4 | import { IncrementProgress } from './progress.actions'; 5 | 6 | export interface ProgressStateModel { 7 | progress: number; 8 | } 9 | 10 | @Injectable() 11 | @State({ 12 | name: 'progress', 13 | defaults: { 14 | progress: 0, 15 | }, 16 | }) 17 | export class ProgressState { 18 | @Selector() 19 | static getProgress(state: ProgressStateModel): number { 20 | return state.progress; 21 | } 22 | 23 | @Action(IncrementProgress) 24 | incrementProgress(ctx: StateContext) { 25 | const state = ctx.getState(); 26 | 27 | if (state.progress !== 100) { 28 | state.progress += 1; 29 | ctx.setState({ ...state }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/core/internals/internals.ts: -------------------------------------------------------------------------------- 1 | const DOLLAR_CHAR_CODE = 36; 2 | 3 | export function removeDollarAtTheEnd(name: string | symbol): string { 4 | if (typeof name !== 'string') { 5 | name = name.toString(); 6 | } 7 | const lastCharIndex = name.length - 1; 8 | const dollarAtTheEnd = name.charCodeAt(lastCharIndex) === DOLLAR_CHAR_CODE; 9 | return dollarAtTheEnd ? name.slice(0, lastCharIndex) : name; 10 | } 11 | 12 | export function getPropsArray(selectorOrFeature: string, paths: string[]): string[] { 13 | if (paths.length) { 14 | return [selectorOrFeature, ...paths]; 15 | } 16 | return selectorOrFeature.split('.'); 17 | } 18 | 19 | export function compliantPropGetter(paths: string[]): (x: any) => any { 20 | const copyOfPaths = [...paths]; 21 | return obj => copyOfPaths.reduce((acc: any, part: string) => acc && acc[part], obj); 22 | } 23 | 24 | export const META_KEY = 'NGXS_META'; 25 | -------------------------------------------------------------------------------- /apps/demos/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/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:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/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: '@nrwl/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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | }, 34 | { 35 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 36 | "env": { 37 | "jest": true 38 | }, 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /libs/select-snapshot/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "rules": { 8 | "@angular-eslint/directive-selector": [ 9 | "error", 10 | { 11 | "type": "attribute", 12 | "prefix": "selectSnapshot", 13 | "style": "camelCase" 14 | } 15 | ], 16 | "@angular-eslint/component-selector": [ 17 | "error", 18 | { 19 | "type": "element", 20 | "prefix": "select-snapshot", 21 | "style": "kebab-case" 22 | } 23 | ], 24 | "@typescript-eslint/ban-types": "off", 25 | "@typescript-eslint/no-non-null-assertion": "off" 26 | }, 27 | "extends": [ 28 | "plugin:@nrwl/nx/angular", 29 | "plugin:@angular-eslint/template/process-inline-templates" 30 | ] 31 | }, 32 | { 33 | "files": ["*.html"], 34 | "extends": ["plugin:@nrwl/nx/angular-template"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/core/internals/static-injector.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | 4 | class NgxsSelectSnapshotModuleIsNotImported extends Error { 5 | constructor() { 6 | super(`You've forgotten to import "NgxsSelectSnapshotModule"!`); 7 | } 8 | } 9 | 10 | let injector: Injector | null = null; 11 | let store: Store | null = null; 12 | 13 | function assertDefined(actual: T | null | undefined): asserts actual is T { 14 | if (actual == null) { 15 | throw new NgxsSelectSnapshotModuleIsNotImported(); 16 | } 17 | } 18 | 19 | export function setInjector(parentInjector: Injector): void { 20 | injector = parentInjector; 21 | } 22 | 23 | /** 24 | * Ensure that we don't keep any references in case of the bootstrapped 25 | * module is destroyed via `NgModuleRef.destroy()`. 26 | */ 27 | export function clearInjector(): void { 28 | injector = null; 29 | store = null; 30 | } 31 | 32 | export function getStore(): never | Store { 33 | assertDefined(injector); 34 | store = store || injector!.get(Store); 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our guidelines: https://github.com/ngxs/store/blob/master/CONTRIBUTING.md#commit 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | ## PR Type 9 | What kind of change does this PR introduce? 10 | 11 | 12 | ``` 13 | [ ] Bugfix 14 | [ ] Feature 15 | [ ] Code style update (formatting, local variables) 16 | [ ] Refactoring (no functional changes, no api changes) 17 | [ ] Build related changes 18 | [ ] CI related changes 19 | [ ] Documentation content changes 20 | [ ] Other... Please describe: 21 | ``` 22 | 23 | ## What is the current behavior? 24 | 25 | 26 | Issue Number: N/A 27 | 28 | 29 | ## What is the new behavior? 30 | 31 | 32 | ## Does this PR introduce a breaking change? 33 | ``` 34 | [ ] Yes 35 | [ ] No 36 | ``` 37 | 38 | 39 | 40 | 41 | ## Other information 42 | -------------------------------------------------------------------------------- /.github/workflows/select-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: '@ngxs-labs/select-snapshot' 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 | with: 18 | fetch-depth: 0 19 | 20 | - run: git fetch --no-tags --prune --depth 2 origin master 21 | 22 | - uses: actions/cache@v3 23 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 24 | with: 25 | path: ~/.cache # Default cache directory for both Yarn and Cypress 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 16.13.0 33 | registry-url: 'https://registry.npmjs.org' 34 | 35 | - name: Install dependencies 36 | run: yarn --immutable 37 | 38 | - run: yarn nx affected:lint --parallel --base=origin/master 39 | - run: yarn nx affected:test --parallel --base=origin/master --configuration ci 40 | - run: yarn nx affected:build --base=origin/master 41 | - run: yarn nx affected:e2e --base=origin/master 42 | env: 43 | ELECTRON_EXTRA_LAUNCH_ARGS: '--disable-gpu' 44 | -------------------------------------------------------------------------------- /libs/select-snapshot/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select-snapshot", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/select-snapshot/src", 6 | "prefix": "select-snapshot", 7 | "targets": { 8 | "build": { 9 | "executor": "@nrwl/angular:package", 10 | "outputs": ["{workspaceRoot}/dist/{projectRoot}"], 11 | "options": { 12 | "project": "libs/select-snapshot/ng-package.json" 13 | }, 14 | "configurations": { 15 | "production": { 16 | "tsConfig": "libs/select-snapshot/tsconfig.lib.prod.json" 17 | }, 18 | "development": { 19 | "tsConfig": "libs/select-snapshot/tsconfig.lib.json" 20 | } 21 | }, 22 | "defaultConfiguration": "production" 23 | }, 24 | "test": { 25 | "executor": "@nrwl/jest:jest", 26 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 27 | "options": { 28 | "jestConfig": "libs/select-snapshot/jest.config.ts", 29 | "passWithNoTests": true 30 | }, 31 | "configurations": { 32 | "ci": { 33 | "ci": true, 34 | "codeCoverage": true 35 | } 36 | } 37 | }, 38 | "lint": { 39 | "executor": "@nrwl/linter:eslint", 40 | "outputs": ["{options.outputFile}"], 41 | "options": { 42 | "lintFilePatterns": ["libs/select-snapshot/**/*.ts", "libs/select-snapshot/**/*.html"] 43 | } 44 | } 45 | }, 46 | "tags": ["lib"] 47 | } 48 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "affected": { 3 | "defaultBase": "master" 4 | }, 5 | "npmScope": "select-snapshot", 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "nx/tasks-runners/default", 9 | "options": { 10 | "cacheableOperations": ["build", "lint", "test", "e2e"], 11 | "parallel": 1 12 | } 13 | } 14 | }, 15 | "defaultProject": "select-snapshot", 16 | "generators": { 17 | "@nrwl/angular:application": { 18 | "style": "css", 19 | "linter": "eslint", 20 | "unitTestRunner": "jest", 21 | "e2eTestRunner": "none" 22 | }, 23 | "@nrwl/angular:library": { 24 | "linter": "eslint", 25 | "unitTestRunner": "jest" 26 | }, 27 | "@nrwl/angular:component": { 28 | "style": "css" 29 | } 30 | }, 31 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 32 | "targetDefaults": { 33 | "build": { 34 | "dependsOn": ["^build"], 35 | "inputs": ["production", "^production"] 36 | }, 37 | "e2e": { 38 | "inputs": ["default", "^production"] 39 | }, 40 | "test": { 41 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 42 | }, 43 | "lint": { 44 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 45 | } 46 | }, 47 | "namedInputs": { 48 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 49 | "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json", "{workspaceRoot}/nx.json"], 50 | "production": [ 51 | "default", 52 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 53 | "!{projectRoot}/tsconfig.spec.json", 54 | "!{projectRoot}/jest.config.[jt]s", 55 | "!{projectRoot}/.eslintrc.json" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/demos/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ɵivyEnabled, 4 | ChangeDetectionStrategy, 5 | NgModuleRef, 6 | OnInit, 7 | ViewChild, 8 | ViewContainerRef, 9 | } from '@angular/core'; 10 | import { Store } from '@ngxs/store'; 11 | import { SelectSnapshot } from '@ngxs-labs/select-snapshot'; 12 | 13 | import { ProgressState, IncrementProgress } from './store'; 14 | 15 | @Component({ 16 | selector: 'app-root', 17 | templateUrl: './app.component.html', 18 | styleUrls: ['./app.component.scss'], 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | }) 21 | export class AppComponent implements OnInit { 22 | @ViewChild('progressContainer', { 23 | static: true, 24 | read: ViewContainerRef, 25 | }) 26 | progressContainer!: ViewContainerRef; 27 | 28 | @SelectSnapshot(ProgressState.getProgress) progress!: number; 29 | 30 | constructor(private ngModuleRef: NgModuleRef, private store: Store) {} 31 | 32 | ngOnInit(): void { 33 | import(/* webpackChunkName: 'progress' */ './progress/progress.component').then(m => { 34 | const ref = this.progressContainer.createComponent(m.ProgressComponent); 35 | ref.instance.ivyEnabled = ɵivyEnabled; 36 | ref.changeDetectorRef.detectChanges(); 37 | }); 38 | } 39 | 40 | startProgress(): void { 41 | const intervalId = setInterval(() => { 42 | if (this.progress === 100) { 43 | clearInterval(intervalId); 44 | // Just for testing purposes. 45 | window.dispatchEvent(new CustomEvent('progressCompleted')); 46 | } else { 47 | this.store.dispatch(new IncrementProgress()); 48 | } 49 | }, 5); 50 | } 51 | 52 | destroy(): void { 53 | this.ngModuleRef.destroy(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-after-install.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-after-install", 5 | factory: function (require) { 6 | var plugin=(()=>{var g=Object.create,r=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var k=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty;var I=t=>r(t,"__esModule",{value:!0});var i=t=>{if(typeof require!="undefined")return require(t);throw new Error('Dynamic require of "'+t+'" is not supported')};var h=(t,o)=>{for(var e in o)r(t,e,{get:o[e],enumerable:!0})},w=(t,o,e)=>{if(o&&typeof o=="object"||typeof o=="function")for(let n of C(o))!y.call(t,n)&&n!=="default"&&r(t,n,{get:()=>o[n],enumerable:!(e=x(o,n))||e.enumerable});return t},a=t=>w(I(r(t!=null?g(k(t)):{},"default",t&&t.__esModule&&"default"in t?{get:()=>t.default,enumerable:!0}:{value:t,enumerable:!0})),t);var j={};h(j,{default:()=>b});var c=a(i("@yarnpkg/core")),m={afterInstall:{description:"Hook that will always run after install",type:c.SettingsType.STRING,default:""}};var u=a(i("clipanion")),d=a(i("@yarnpkg/core"));var p=a(i("@yarnpkg/shell")),l=async(t,o)=>{var f;let e=t.get("afterInstall"),n=!!((f=t.projectCwd)==null?void 0:f.endsWith(`dlx-${process.pid}`));return e&&!n?(o&&console.log("Running `afterInstall` hook..."),(0,p.execute)(e,[],{cwd:t.projectCwd||void 0})):0};var s=class extends u.Command{async execute(){let o=await d.Configuration.find(this.context.cwd,this.context.plugins);return l(o,!1)}};s.paths=[["after-install"]];var P={configuration:m,commands:[s],hooks:{afterAllInstalled:async t=>{if(await l(t.configuration,!0))throw new Error("The `afterInstall` hook failed, see output above.")}}},b=P;return j;})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /apps/demos/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/node'; 2 | 3 | import { join } from 'path'; 4 | import * as express from 'express'; 5 | 6 | import { APP_BASE_HREF } from '@angular/common'; 7 | import { ngExpressEngine } from '@nguniversal/express-engine'; 8 | 9 | import { AppServerModule } from './main.server'; 10 | 11 | export function app(): express.Express { 12 | const server = express(); 13 | const distFolder = join(process.cwd(), 'dist/apps/demos/browser'); 14 | 15 | server.engine( 16 | 'html', 17 | ngExpressEngine({ 18 | bootstrap: AppServerModule 19 | }) 20 | ); 21 | 22 | server.set('view engine', 'html'); 23 | server.set('views', distFolder); 24 | 25 | server.get( 26 | '*.*', 27 | express.static(distFolder, { 28 | maxAge: '1y' 29 | }) 30 | ); 31 | 32 | server.get('*', (req, res) => { 33 | res.render('index', { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); 34 | }); 35 | 36 | return server; 37 | } 38 | 39 | function run(): void { 40 | const port = process.env.PORT || 4200; 41 | 42 | const server = app(); 43 | server.listen(port, () => { 44 | console.log(`Node Express server listening on http://localhost:${port}`); 45 | }); 46 | } 47 | 48 | // Webpack will replace 'require' with '__webpack_require__' 49 | // '__non_webpack_require__' is a proxy to Node 'require' 50 | // The below code is to ensure that the server is run only when not requiring the bundle. 51 | declare const __non_webpack_require__: NodeRequire; 52 | const mainModule = __non_webpack_require__.main; 53 | const moduleFilename = (mainModule && mainModule.filename) || ''; 54 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 55 | run(); 56 | } 57 | 58 | export * from './main.server'; 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## I'm submitting a... 8 | 9 |

10 | [ ] Regression (a behavior that used to work and stopped working in a new release)
11 | [ ] Bug report  
12 | [ ] Performance issue
13 | [ ] Feature request
14 | [ ] Documentation issue or request
15 | [ ] Support request => https://github.com/ngxs/store/blob/master/CONTRIBUTING.md
16 | [ ] Other... Please describe:
17 | 
18 | 19 | ## Current behavior 20 | 21 | 22 | 23 | ## Expected behavior 24 | 25 | 26 | 27 | ## Minimal reproduction of the problem with instructions 28 | 29 | 38 | 39 | ## What is the motivation / use case for changing the behavior? 40 | 41 | 42 | 43 | ## Environment 44 | 45 |

46 | Libs:
47 | - @angular/core version: X.Y.Z
48 | - @ngxs/store version: X.Y.Z
49 | 
50 | 
51 | Browser:
52 | - [ ] Chrome (desktop) version XX
53 | - [ ] Chrome (Android) version XX
54 | - [ ] Chrome (iOS) version XX
55 | - [ ] Firefox version XX
56 | - [ ] Safari (desktop) version XX
57 | - [ ] Safari (iOS) version XX
58 | - [ ] IE version XX
59 | - [ ] Edge version XX
60 |  
61 | For Tooling issues:
62 | - Node version: XX  
63 | - Platform:  
64 | 
65 | Others:
66 | 
67 | 
68 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/core/internals/select-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngxs/store'; 2 | 3 | import { getStore } from './static-injector'; 4 | import { removeDollarAtTheEnd, getPropsArray, compliantPropGetter, META_KEY } from './internals'; 5 | 6 | type CreateSelectorFactory = (selectorOrFeature: any) => any; 7 | 8 | export function createSelectorFactory(paths: string[]): CreateSelectorFactory { 9 | return (selectorOrFeature: any) => { 10 | if (typeof selectorOrFeature === 'string') { 11 | const propsArray = getPropsArray(selectorOrFeature, paths); 12 | return compliantPropGetter(propsArray); 13 | } else if (selectorOrFeature[META_KEY] && selectorOrFeature[META_KEY].path) { 14 | return compliantPropGetter(selectorOrFeature[META_KEY].path.split('.')); 15 | } 16 | 17 | return selectorOrFeature; 18 | }; 19 | } 20 | 21 | export function getSelectorFromInstance( 22 | instance: any, 23 | selectorFnName: string, 24 | createSelector: CreateSelectorFactory, 25 | selectorOrFeature: any, 26 | ) { 27 | return instance[selectorFnName] || (instance[selectorFnName] = createSelector(selectorOrFeature)); 28 | } 29 | 30 | export function defineSelectSnapshotProperties( 31 | selectorOrFeature: any, 32 | paths: string[], 33 | target: Object, 34 | name: string | symbol, 35 | store?: Store, 36 | ) { 37 | const selectorFnName = `__${name.toString()}__selector`; 38 | const createSelector = createSelectorFactory(paths); 39 | 40 | Object.defineProperties(target, { 41 | [selectorFnName]: { 42 | writable: true, 43 | enumerable: false, 44 | configurable: true, 45 | }, 46 | [name]: { 47 | get() { 48 | const selector = getSelectorFromInstance( 49 | this, 50 | selectorFnName, 51 | createSelector, 52 | selectorOrFeature, 53 | ); 54 | // Don't use the `directiveInject` here as it works ONLY 55 | // during view creation. 56 | store = getStore(); 57 | return store.selectSnapshot(selector); 58 | }, 59 | enumerable: true, 60 | configurable: true, 61 | }, 62 | }); 63 | 64 | return { 65 | selectorFnName, 66 | createSelector, 67 | selectorOrFeature: selectorOrFeature || removeDollarAtTheEnd(name), 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select-snapshot", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/ngxs-labs/select-snapshot.git" 7 | }, 8 | "license": "MIT", 9 | "homepage": "https://github.com/ngxs-labs/select-snapshot#readme", 10 | "bugs": { 11 | "url": "https://github.com/ngxs-labs/select-snapshot/issues" 12 | }, 13 | "keywords": [ 14 | "ngxs", 15 | "redux", 16 | "store" 17 | ], 18 | "scripts": { 19 | "prebuild": "nx build select-snapshot --skip-nx-cache", 20 | "build": "yarn prebuild && yarn postbuild", 21 | "postbuild": "cpx README.md dist/libs/select-snapshot", 22 | "test": "nx test select-snapshot", 23 | "e2e": "nx e2e demos-e2e" 24 | }, 25 | "private": true, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "15.1.1", 28 | "@angular-devkit/core": "15.1.1", 29 | "@angular-devkit/schematics": "15.1.1", 30 | "@angular-eslint/eslint-plugin": "15.0.0", 31 | "@angular-eslint/eslint-plugin-template": "15.0.0", 32 | "@angular-eslint/template-parser": "15.0.0", 33 | "@angular/animations": "15.1.0", 34 | "@angular/cli": "15.1.1", 35 | "@angular/common": "15.1.0", 36 | "@angular/compiler": "15.1.0", 37 | "@angular/compiler-cli": "15.1.0", 38 | "@angular/core": "15.1.0", 39 | "@angular/forms": "15.1.0", 40 | "@angular/language-service": "15.1.0", 41 | "@angular/platform-browser": "15.1.0", 42 | "@angular/platform-browser-dynamic": "15.1.0", 43 | "@angular/platform-server": "15.1.0", 44 | "@angular/router": "15.1.0", 45 | "@commitlint/cli": "^17.5.0", 46 | "@commitlint/config-conventional": "^17.4.4", 47 | "@nguniversal/builders": "15.1.0", 48 | "@nguniversal/express-engine": "15.1.0", 49 | "@ngxs/store": "3.7.6", 50 | "@nrwl/angular": "15.8.9", 51 | "@nrwl/cli": "15.8.9", 52 | "@nrwl/cypress": "15.8.9", 53 | "@nrwl/eslint-plugin-nx": "15.8.9", 54 | "@nrwl/jest": "15.8.9", 55 | "@nrwl/js": "15.8.9", 56 | "@nrwl/linter": "15.8.9", 57 | "@nrwl/workspace": "15.8.9", 58 | "@schematics/angular": "15.1.1", 59 | "@types/express": "^4.17.2", 60 | "@types/jest": "29.4.4", 61 | "@types/node": "^16.11.7", 62 | "@typescript-eslint/eslint-plugin": "5.43.0", 63 | "@typescript-eslint/parser": "5.43.0", 64 | "cpx": "^1.5.0", 65 | "cypress": "^10.0.0", 66 | "eslint": "8.15.0", 67 | "eslint-config-prettier": "^8.3.0", 68 | "eslint-plugin-cypress": "^2.12.1", 69 | "express": "^4.17.1", 70 | "husky": "^8.0.0", 71 | "jest": "29.4.3", 72 | "jest-environment-jsdom": "29.4.3", 73 | "jest-preset-angular": "13.0.0", 74 | "lint-staged": "^13.0.0", 75 | "ng-packagr": "~15.2.2", 76 | "nx": "15.8.9", 77 | "postcss": "8.4.16", 78 | "postcss-import": "14.1.0", 79 | "postcss-preset-env": "7.5.0", 80 | "postcss-url": "10.1.3", 81 | "prettier": "2.7.1", 82 | "rxjs": "^7.5.4", 83 | "ts-jest": "29.0.5", 84 | "ts-node": "10.9.1", 85 | "tslib": "^2.3.0", 86 | "typescript": "~4.9.5", 87 | "zone.js": "0.12.0" 88 | }, 89 | "lint-staged": { 90 | "*.{js,html,scss,ts,md}": [ 91 | "prettier --write" 92 | ] 93 | }, 94 | "prettier": { 95 | "semi": true, 96 | "endOfLine": "lf", 97 | "tabWidth": 2, 98 | "printWidth": 100, 99 | "trailingComma": "all", 100 | "bracketSpacing": true, 101 | "arrowParens": "avoid", 102 | "singleQuote": true 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nrwl/workspace').output; 31 | } catch (e) { 32 | console.warn( 33 | 'Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.' 34 | ); 35 | process.exit(0); 36 | } 37 | 38 | /** 39 | * Paths to files being patched 40 | */ 41 | const angularCLIInitPath = 'node_modules/@angular/cli/lib/cli/index.js'; 42 | 43 | /** 44 | * Patch index.js to warn you if you invoke the undecorated Angular CLI. 45 | */ 46 | function patchAngularCLI(initPath) { 47 | const angularCLIInit = fs.readFileSync(initPath, 'utf-8').toString(); 48 | 49 | if (!angularCLIInit.includes('NX_CLI_SET')) { 50 | fs.writeFileSync( 51 | initPath, 52 | ` 53 | if (!process.env['NX_CLI_SET']) { 54 | const { output } = require('@nrwl/workspace'); 55 | output.warn({ title: 'The Angular CLI was invoked instead of the Nx CLI. Use "npx ng [command]" or "nx [command]" instead.' }); 56 | } 57 | ${angularCLIInit} 58 | ` 59 | ); 60 | } 61 | } 62 | 63 | /** 64 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 65 | * invoke the Nx CLI and get the benefits of computation caching. 66 | */ 67 | function symlinkNgCLItoNxCLI() { 68 | try { 69 | const ngPath = './node_modules/.bin/ng'; 70 | const nxPath = './node_modules/.bin/nx'; 71 | if (isWindows) { 72 | /** 73 | * This is the most reliable way to create symlink-like behavior on Windows. 74 | * Such that it works in all shells and works with npx. 75 | */ 76 | ['', '.cmd', '.ps1'].forEach(ext => { 77 | if (fs.existsSync(nxPath + ext)) 78 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 79 | }); 80 | } else { 81 | // If unix-based, symlink 82 | cp.execSync(`ln -sf ./nx ${ngPath}`); 83 | } 84 | } catch (e) { 85 | output.error({ 86 | title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message 87 | }); 88 | throw e; 89 | } 90 | } 91 | 92 | try { 93 | symlinkNgCLItoNxCLI(); 94 | patchAngularCLI(angularCLIInitPath); 95 | output.log({ 96 | title: 'Angular CLI has been decorated to enable computation caching.' 97 | }); 98 | } catch (e) { 99 | output.error({ 100 | title: 'Decoration of the Angular CLI did not complete successfully' 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /apps/demos/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demos", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/demos/src", 6 | "prefix": "app", 7 | "targets": { 8 | "build": { 9 | "executor": "@angular-devkit/build-angular:browser", 10 | "options": { 11 | "outputPath": "dist/apps/demos/browser", 12 | "index": "apps/demos/src/index.html", 13 | "main": "apps/demos/src/main.ts", 14 | "polyfills": "apps/demos/src/polyfills.ts", 15 | "tsConfig": "apps/demos/tsconfig.app.json", 16 | "styles": [], 17 | "scripts": [], 18 | "vendorChunk": true, 19 | "extractLicenses": false, 20 | "buildOptimizer": false, 21 | "sourceMap": true, 22 | "optimization": false, 23 | "namedChunks": true, 24 | "assets": ["apps/demos/src/favicon.ico", "apps/demos/src/assets"] 25 | }, 26 | "configurations": { 27 | "development": { 28 | "buildOptimizer": false, 29 | "optimization": false, 30 | "vendorChunk": true, 31 | "extractLicenses": false, 32 | "sourceMap": true, 33 | "namedChunks": true 34 | }, 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "apps/demos/src/environments/environment.ts", 39 | "with": "apps/demos/src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "namedChunks": false, 46 | "extractLicenses": true, 47 | "vendorChunk": false, 48 | "buildOptimizer": true, 49 | "budgets": [ 50 | { 51 | "type": "initial", 52 | "maximumWarning": "2mb", 53 | "maximumError": "5mb" 54 | }, 55 | { 56 | "type": "anyComponentStyle", 57 | "maximumWarning": "6kb", 58 | "maximumError": "10kb" 59 | } 60 | ] 61 | } 62 | }, 63 | "defaultConfiguration": "production" 64 | }, 65 | "serve": { 66 | "executor": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "demos:build" 69 | }, 70 | "configurations": { 71 | "development": { 72 | "browserTarget": "demos:build:development" 73 | }, 74 | "production": { 75 | "browserTarget": "demos:build:production" 76 | } 77 | }, 78 | "defaultConfiguration": "development" 79 | }, 80 | "lint": { 81 | "executor": "@nrwl/linter:eslint", 82 | "options": { 83 | "lintFilePatterns": ["apps/demos/src/**/*.ts", "apps/demos/src/**/*.html"] 84 | }, 85 | "outputs": ["{options.outputFile}"] 86 | }, 87 | "test": { 88 | "executor": "@nrwl/jest:jest", 89 | "outputs": ["{workspaceRoot}/coverage/apps/demos"], 90 | "options": { 91 | "jestConfig": "apps/demos/jest.config.ts", 92 | "passWithNoTests": true 93 | } 94 | }, 95 | "server": { 96 | "executor": "@angular-devkit/build-angular:server", 97 | "options": { 98 | "outputPath": "dist/apps/demos/server", 99 | "main": "apps/demos/src/server.ts", 100 | "tsConfig": "apps/demos/tsconfig.server.json", 101 | "sourceMap": true, 102 | "optimization": false 103 | }, 104 | "configurations": { 105 | "development": { 106 | "optimization": false, 107 | "sourceMap": true, 108 | "extractLicenses": false 109 | }, 110 | "production": { 111 | "sourceMap": false, 112 | "optimization": true, 113 | "outputHashing": "media", 114 | "fileReplacements": [ 115 | { 116 | "replace": "apps/demos/src/environments/environment.ts", 117 | "with": "apps/demos/src/environments/environment.prod.ts" 118 | } 119 | ] 120 | } 121 | }, 122 | "defaultConfiguration": "production" 123 | }, 124 | "serve-ssr": { 125 | "executor": "@nguniversal/builders:ssr-dev-server", 126 | "options": { 127 | "browserTarget": "demos:build", 128 | "serverTarget": "demos:server" 129 | }, 130 | "configurations": { 131 | "development": { 132 | "browserTarget": "demos:build:development", 133 | "serverTarget": "demos:server:development" 134 | }, 135 | "production": { 136 | "browserTarget": "demos:build:production", 137 | "serverTarget": "demos:server:production" 138 | } 139 | }, 140 | "defaultConfiguration": "development" 141 | } 142 | }, 143 | "type": ["app"] 144 | } 145 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/core/decorators/view-select-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ɵComponentType, 3 | ɵDirectiveType, 4 | ɵComponentDef, 5 | ɵDirectiveDef, 6 | ɵɵdirectiveInject, 7 | ChangeDetectorRef, 8 | } from '@angular/core'; 9 | import { Store } from '@ngxs/store'; 10 | import { Subscription } from 'rxjs'; 11 | 12 | import { 13 | defineSelectSnapshotProperties, 14 | getSelectorFromInstance, 15 | } from '../internals/select-snapshot'; 16 | 17 | interface CreateSelectorParams { 18 | name: string | symbol; 19 | paths: string[]; 20 | selectorOrFeature: any; 21 | } 22 | 23 | const targetToScheduledMicrotaskMap = new Map>(); 24 | const targetToCreateSelectorParamsMap = new Map(); 25 | 26 | const SUBSCRIPTIONS: unique symbol = Symbol('SUBSCRIPTIONS'); 27 | 28 | export function ViewSelectSnapshot(selectorOrFeature?: any, ...paths: string[]): PropertyDecorator { 29 | return (target: Object, name: string | symbol) => { 30 | const createSelectorParams = targetToCreateSelectorParamsMap.get(target) || []; 31 | 32 | createSelectorParams.push({ 33 | name, 34 | paths, 35 | selectorOrFeature, 36 | }); 37 | 38 | if (!targetToCreateSelectorParamsMap.has(target)) { 39 | targetToCreateSelectorParamsMap.set(target, createSelectorParams); 40 | // Since this is a property decorator we're not able to get the component definition and factory 41 | // synchronously in the current message loop tick. Note that this `Promise` will be resolved 42 | // before the `APP_INITIALIZER` is resolved. 43 | targetToScheduledMicrotaskMap.set( 44 | target, 45 | Promise.resolve().then(() => decorateTarget(target)), 46 | ); 47 | } 48 | }; 49 | } 50 | 51 | function decorateTarget(target: Object): void { 52 | const createSelectorParams = targetToCreateSelectorParamsMap.get(target)!; 53 | const def = getDef(target.constructor); 54 | const factory = getFactory(target.constructor); 55 | 56 | // `factory` is a readonly property, but still overridable. 57 | (def as { factory: () => InstanceWithSubscriptions }).factory = () => { 58 | const instance = factory(); 59 | const store = ɵɵdirectiveInject(Store); 60 | const ref = ɵɵdirectiveInject(ChangeDetectorRef); 61 | for (const { name, paths, selectorOrFeature } of createSelectorParams) { 62 | const properties = defineSelectSnapshotProperties( 63 | selectorOrFeature, 64 | paths, 65 | target, 66 | name, 67 | store, 68 | ); 69 | const selector = getSelectorFromInstance( 70 | instance, 71 | properties.selectorFnName, 72 | properties.createSelector, 73 | properties.selectorOrFeature, 74 | ); 75 | const subscription = store.select(selector).subscribe(() => ref.markForCheck()); 76 | const subscriptions = getSubscriptionsOnTheInstance(instance); 77 | subscriptions.push(subscription); 78 | } 79 | return instance; 80 | }; 81 | 82 | overrideNgOnDestroy(target); 83 | 84 | targetToScheduledMicrotaskMap.delete(target); 85 | targetToCreateSelectorParamsMap.delete(target); 86 | } 87 | 88 | function overrideNgOnDestroy(target: Object): void { 89 | // Angular 10.0.5+ doesn't store `ngOnDestroy` hook on the component definition anymore and 90 | // allows to override lifecycle hooks through prototypes. 91 | // Previously, it was stored on the `type[NG_CMP_DEF].onDestroy` property, unfortunately, 92 | // it's a breaking change that'll require to use Angular 10.0.5+. 93 | const ngOnDestroy = target.constructor.prototype.ngOnDestroy; 94 | 95 | // Since Angular 10.0.5+ it's possible to override the `ngOnDestroy` directly on the prototype. 96 | target.constructor.prototype.ngOnDestroy = function (this: InstanceWithSubscriptions) { 97 | // Invoke the original `ngOnDestroy`. 98 | ngOnDestroy?.call(this); 99 | 100 | const subscriptions = this[SUBSCRIPTIONS]; 101 | 102 | if (Array.isArray(subscriptions)) { 103 | while (subscriptions.length) { 104 | const subscription = subscriptions.pop()!; 105 | subscription.unsubscribe(); 106 | } 107 | } 108 | }; 109 | } 110 | 111 | function getFactory(type: Function): () => InstanceWithSubscriptions { 112 | return (type as ɵDirectiveType).ɵfac as () => InstanceWithSubscriptions; 113 | } 114 | 115 | function getDef(type: Function): ɵComponentDef | ɵDirectiveDef { 116 | // Gotta cast since it's `unknown`. 117 | const ɵcmp = (type as ɵComponentType).ɵcmp as ɵComponentDef; 118 | const ɵdir = (type as ɵDirectiveType).ɵdir as ɵDirectiveDef; 119 | return ɵcmp || ɵdir; 120 | } 121 | 122 | function getSubscriptionsOnTheInstance(instance: InstanceWithSubscriptions): Subscription[] { 123 | return instance[SUBSCRIPTIONS] || (instance[SUBSCRIPTIONS] = []); 124 | } 125 | 126 | interface InstanceWithSubscriptions { 127 | [SUBSCRIPTIONS]?: Subscription[]; 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | --- 6 | 7 | > Flexibile decorator, an alternative for the `@Select` but selects a snapshot of the state 8 | 9 | ![@ngxs-labs/select-snapshot](https://github.com/ngxs-labs/select-snapshot/workflows/@ngxs-labs/select-snapshot/badge.svg) 10 | [![NPM](https://badge.fury.io/js/%40ngxs-labs%2Fselect-snapshot.svg)](https://badge.fury.io/js/%40ngxs-labs%2Fselect-snapshot) 11 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/ngxs-labs/select-snapshot/blob/master/LICENSE) 12 | 13 | ## Table of Contents 14 | 15 | - [Compatibility with Angular Versions](#compatibility-with-angular-versions) 16 | - [Install](#📦-install) 17 | - [Usage](#🔨-usage) 18 | - [API](#api) 19 | - [SelectSnapshot](#selectsnapshot) 20 | - [ViewSelectSnapshot](#viewselectsnapshot) 21 | - [Summary](#summary) 22 | 23 | ## Compatibility with Angular Versions 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 40 | 41 | 42 | 45 | 48 | 49 | 50 | 53 | 56 | 57 | 58 |
@ngxs-labs/select-snapshotAngular
35 | 3.x 36 | 38 | >= 10.0.5 < 13 39 |
43 | 4.x 44 | 46 | >= 13 < 15 47 |
51 | 5.x 52 | 54 | >= 15 55 |
59 | 60 | ## 📦 Install 61 | 62 | To install `@ngxs-labs/select-snapshot`, run the following command: 63 | 64 | ```sh 65 | $ npm install @ngxs-labs/select-snapshot 66 | # Or if you're using yarn 67 | $ yarn add @ngxs-labs/select-snapshot 68 | # Or if you're using pnpm 69 | $ pnpm install @ngxs-labs/select-snapshot 70 | ``` 71 | 72 | ## 🔨 Usage 73 | 74 | Import the `NgxsSelectSnapshotModule` into your root application module: 75 | 76 | ```typescript 77 | import { NgModule } from '@angular/core'; 78 | import { NgxsModule } from '@ngxs/store'; 79 | import { NgxsSelectSnapshotModule } from '@ngxs-labs/select-snapshot'; 80 | 81 | @NgModule({ 82 | imports: [NgxsModule.forRoot(states), NgxsSelectSnapshotModule.forRoot()], 83 | }) 84 | export class AppModule {} 85 | ``` 86 | 87 | ## API 88 | 89 | `@ngxs-labs/select-snapshot` exposes `@SelectSnapshot` and `@ViewSelectSnapshot` decorators, they might be used to decorate class properties. 90 | 91 | ### SelectSnapshot 92 | 93 | `@SelectSnapshot` decorator should be used similarly to the `@Select` decorator. It will decorate the property to always return the latest selected value, whereas `@Select` decorates properties to return observable. Given the following example: 94 | 95 | ```ts 96 | import { SelectSnapshot } from '@ngxs-labs/select-snapshot'; 97 | 98 | @Injectable() 99 | export class TokenInterceptor { 100 | @SelectSnapshot(AuthState.token) token: string | null; 101 | 102 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 103 | if (this.token) { 104 | req = req.clone({ 105 | setHeaders: { 106 | Authorization: `Bearer ${this.token}`, 107 | }, 108 | }); 109 | } 110 | 111 | return next.handle(req); 112 | } 113 | } 114 | ``` 115 | 116 | We don't have to inject the `Store` and call the `selectSnapshot`. 117 | 118 | Behind the scenes, `@SelectSnapshot` sets up a getter that calls `store.selectSnapshot` with the provided selector on each access. 119 | In the above example, it roughly equates to setting up this property getter: 120 | 121 | ```ts 122 | get token(): string | null { 123 | // ... inject `Store` in variable `store` 124 | return store.selectSnapshot(AuthState.token); 125 | } 126 | ``` 127 | 128 | ### ViewSelectSnapshot 129 | 130 | `@ViewSelectSnapshot` is a decorator that should decorate class properties that are used in templates (e.g. _renderable_ or passed as _bindings_). Given the following example: 131 | 132 | ```ts 133 | @Component({ 134 | selector: 'app-progress', 135 | template: ` 136 |
137 |
138 |
139 | `, 140 | changeDetection: ChangeDetectionStrategy.OnPush, 141 | }) 142 | export class ProgressComponent { 143 | // 🚫 Do not use `SelectSnapshot` since `progress` is used in the template. 144 | @SelectSnapshot(ProgressState.getProgress) progress: number; 145 | } 146 | ``` 147 | 148 | The `@ViewSelectSnapshot` decorator will force the template to be updated whenever the `progress` property is changed on the state: 149 | 150 | ```ts 151 | @Component({ 152 | selector: 'app-progress', 153 | template: ` 154 |
155 |
156 |
157 | `, 158 | changeDetection: ChangeDetectionStrategy.OnPush, 159 | }) 160 | export class ProgressComponent { 161 | // ✔️ Our view will be checked and updated. 162 | @ViewSelectSnapshot(ProgressState.getProgress) progress: number; 163 | } 164 | ``` 165 | 166 | The decorator internally subscribes to `store.select` with the provided selector and calls `markForCheck()` whenever the state is updated (and the selector emits). 167 | 168 | ## Summary 169 | 170 | We have looked at several examples of using both decorators. Consider to use the `@SelectSnapshot` if decorated properties are not used in templates! Consider to use the `@ViewSelectSnapshot` if decorated properties are used in templates (e.g. _renderable_ or passed as _bindings_). 171 | -------------------------------------------------------------------------------- /libs/select-snapshot/src/lib/select-snapshot.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Component, Type } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { State, Action, StateContext, NgxsModule, Store, Selector } from '@ngxs/store'; 5 | 6 | import { SelectSnapshot, NgxsSelectSnapshotModule } from '..'; 7 | 8 | describe('SelectSnapshot', () => { 9 | interface AnimalsStateModel { 10 | pandas: string[]; 11 | } 12 | 13 | type BearsStateModel = string[]; 14 | 15 | interface BearsChildrenStateModel { 16 | children: string[]; 17 | } 18 | 19 | class AddPanda { 20 | static type = '[Animals] Add panda'; 21 | constructor(public name: string) {} 22 | } 23 | 24 | // Used for convenience and avoiding `any` in the selector callbacks 25 | interface RootStateModel { 26 | animals: { 27 | pandas: string[]; 28 | bears: string[] & { 29 | bearsChildren: { 30 | children: string[]; 31 | }; 32 | }; 33 | }; 34 | } 35 | 36 | @State({ 37 | name: 'bearsChildren', 38 | defaults: { 39 | children: [], 40 | }, 41 | }) 42 | @Injectable() 43 | class BearsChildrenState {} 44 | 45 | @State({ 46 | name: 'bears', 47 | defaults: [], 48 | children: [BearsChildrenState], 49 | }) 50 | @Injectable() 51 | class BearsState {} 52 | 53 | @State({ 54 | name: 'animals', 55 | defaults: { 56 | pandas: [], 57 | }, 58 | children: [BearsState], 59 | }) 60 | @Injectable() 61 | class AnimalsState { 62 | @Action(AddPanda) 63 | addPanda({ getState, patchState }: StateContext, { name }: AddPanda): void { 64 | const { pandas } = getState(); 65 | 66 | patchState({ 67 | pandas: [...pandas, name], 68 | }); 69 | } 70 | } 71 | 72 | const states = [BearsChildrenState, BearsState, AnimalsState]; 73 | 74 | function configureTestingModule(component: Type): void { 75 | TestBed.configureTestingModule({ 76 | imports: [ 77 | NgxsModule.forRoot(states, { developmentMode: true }), 78 | NgxsSelectSnapshotModule.forRoot(), 79 | ], 80 | declarations: [component], 81 | }); 82 | } 83 | 84 | function expectIsArrayToBeTruthy(array: T): void { 85 | expect(Array.isArray(array)).toBeTruthy(); 86 | } 87 | 88 | it('should select the correct state using string', () => { 89 | // Arrange 90 | @Component({ template: '' }) 91 | class TestComponent { 92 | @SelectSnapshot('animals') animals!: AnimalsStateModel; 93 | 94 | @SelectSnapshot('animals.bears') bears!: BearsStateModel; 95 | 96 | @SelectSnapshot('animals.bears.bearsChildren') bearsChildren!: BearsChildrenStateModel; 97 | } 98 | 99 | // Act 100 | configureTestingModule(TestComponent); 101 | 102 | // Assert 103 | const { animals, bears, bearsChildren } = 104 | TestBed.createComponent(TestComponent).componentInstance; 105 | 106 | expect(animals.pandas).toBeDefined(); 107 | expectIsArrayToBeTruthy(animals.pandas); 108 | 109 | expect(bears).toBeDefined(); 110 | expectIsArrayToBeTruthy(bears); 111 | 112 | expect(bearsChildren.children).toBeDefined(); 113 | expectIsArrayToBeTruthy(bearsChildren.children); 114 | }); 115 | 116 | it('should select the correct state using a state class', () => { 117 | // Arrange 118 | @Component({ template: '' }) 119 | class TestComponent { 120 | @SelectSnapshot(AnimalsState) animals!: AnimalsStateModel; 121 | 122 | @SelectSnapshot(BearsState) bears!: BearsStateModel; 123 | 124 | @SelectSnapshot(BearsChildrenState) bearsChildren!: BearsChildrenStateModel; 125 | } 126 | 127 | // Act 128 | TestBed.configureTestingModule({ 129 | imports: [ 130 | NgxsModule.forRoot(states, { developmentMode: true }), 131 | NgxsSelectSnapshotModule.forRoot(), 132 | ], 133 | declarations: [TestComponent], 134 | }); 135 | 136 | // Assert 137 | const { animals, bears, bearsChildren } = 138 | TestBed.createComponent(TestComponent).componentInstance; 139 | 140 | expect(animals.pandas).toBeDefined(); 141 | expectIsArrayToBeTruthy(animals.pandas); 142 | 143 | expect(bears).toBeDefined(); 144 | expectIsArrayToBeTruthy(bears); 145 | 146 | expect(bearsChildren.children).toBeDefined(); 147 | expectIsArrayToBeTruthy(bearsChildren.children); 148 | }); 149 | 150 | it('should select the correct state using a function', () => { 151 | // Arrange 152 | @Component({ template: '' }) 153 | class TestComponent { 154 | @SelectSnapshot((state: RootStateModel) => state.animals.bears.bearsChildren) 155 | bearsChildren!: BearsChildrenStateModel; 156 | } 157 | 158 | // Act 159 | configureTestingModule(TestComponent); 160 | 161 | // Assert 162 | const { bearsChildren } = TestBed.createComponent(TestComponent).componentInstance; 163 | 164 | expect(bearsChildren.children).toBeDefined(); 165 | expectIsArrayToBeTruthy(bearsChildren.children); 166 | }); 167 | 168 | it('should select the correct state after timeout', done => { 169 | // Arrange 170 | @Component({ template: '' }) 171 | class TestComponent { 172 | @SelectSnapshot((state: RootStateModel) => state.animals) animals!: AnimalsStateModel; 173 | 174 | constructor(store: Store) { 175 | setTimeout(() => { 176 | store.dispatch([new AddPanda('Mark'), new AddPanda('Max'), new AddPanda('Artur')]); 177 | }, 100); 178 | } 179 | } 180 | 181 | // Act 182 | configureTestingModule(TestComponent); 183 | 184 | // Assert 185 | const { componentInstance } = TestBed.createComponent(TestComponent); 186 | 187 | expectIsArrayToBeTruthy(componentInstance.animals.pandas); 188 | 189 | setTimeout(() => { 190 | expect(componentInstance.animals.pandas.length).toBe(3); 191 | expect(componentInstance.animals.pandas).toEqual(['Mark', 'Max', 'Artur']); 192 | done(); 193 | }, 200); 194 | }); 195 | 196 | it('should fail when TypeError is thrown in select lambda', () => { 197 | // Arrange 198 | @Component({ template: '' }) 199 | class TestComponent { 200 | @SelectSnapshot((state: any) => state.animals.not.here) 201 | public something: any; 202 | } 203 | 204 | let message!: string; 205 | 206 | // Act 207 | configureTestingModule(TestComponent); 208 | 209 | // P.S. `store.selectSnapshot` throws exception 210 | try { 211 | const { something } = TestBed.createComponent(TestComponent).componentInstance; 212 | } catch (error) { 213 | message = (error as Error).message; 214 | } 215 | 216 | // Assert 217 | expect(message).toEqual("Cannot read properties of undefined (reading 'here')"); 218 | }); 219 | 220 | // Arrange 221 | class Increment { 222 | public static type = '[Counter] Increment'; 223 | } 224 | 225 | @State({ 226 | name: 'counter', 227 | defaults: 0, 228 | }) 229 | @Injectable() 230 | class CounterState { 231 | @Action(Increment) 232 | increment({ setState, getState }: StateContext): void { 233 | setState(getState() + 1); 234 | } 235 | } 236 | 237 | @Component({ template: '' }) 238 | class TestComponent { 239 | @SelectSnapshot(CounterState) counter!: number; 240 | } 241 | 242 | it('should get the correct snapshot after dispatching multiple actions', () => { 243 | // Act 244 | TestBed.configureTestingModule({ 245 | imports: [ 246 | NgxsModule.forRoot([CounterState], { developmentMode: true }), 247 | NgxsSelectSnapshotModule.forRoot(), 248 | ], 249 | declarations: [TestComponent], 250 | }); 251 | 252 | // Assert 253 | const { componentInstance } = TestBed.createComponent(TestComponent); 254 | const store: Store = TestBed.inject(Store); 255 | 256 | store.dispatch([new Increment(), new Increment(), new Increment()]); 257 | 258 | expect(componentInstance.counter).toBe(3); 259 | 260 | store.dispatch(new Increment()); 261 | 262 | expect(componentInstance.counter).toBe(4); 263 | }); 264 | 265 | it('should get the correct state after destroying the module', () => { 266 | // Act 267 | TestBed.configureTestingModule({ 268 | imports: [ 269 | NgxsModule.forRoot([CounterState], { developmentMode: true }), 270 | NgxsSelectSnapshotModule.forRoot(), 271 | ], 272 | declarations: [TestComponent], 273 | }); 274 | 275 | // Assert 276 | const { componentInstance } = TestBed.createComponent(TestComponent); 277 | const store: Store = TestBed.inject(Store); 278 | 279 | expect(componentInstance.counter).toBe(0); 280 | 281 | store.dispatch(new Increment()); 282 | 283 | expect(componentInstance.counter).toBe(1); 284 | }); 285 | 286 | @State({ 287 | name: 'nullselector', 288 | defaults: { 289 | foo: 'Hello', 290 | }, 291 | }) 292 | @Injectable() 293 | class NullSelectorState { 294 | @Selector() 295 | static notHere(state: any) { 296 | return state.does.not.exist; 297 | } 298 | } 299 | 300 | it('should not fail when TypeError is thrown in select lambda', () => { 301 | // Arrange 302 | @Component({ template: '' }) 303 | class TestComponent { 304 | @SelectSnapshot(NullSelectorState.notHere) state: any; 305 | } 306 | 307 | // Act 308 | TestBed.configureTestingModule({ 309 | imports: [ 310 | NgxsModule.forRoot([NullSelectorState], { developmentMode: true }), 311 | NgxsSelectSnapshotModule.forRoot(), 312 | ], 313 | declarations: [TestComponent], 314 | }); 315 | 316 | // Assert 317 | const { state } = TestBed.createComponent(TestComponent).componentInstance; 318 | expect(state).toBeUndefined(); 319 | }); 320 | }); 321 | --------------------------------------------------------------------------------