├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── extensions.json
├── README.md
├── angular.json
├── apps
├── angular-feature-sliced-e2e
│ ├── .eslintrc.json
│ ├── cypress.json
│ ├── src
│ │ ├── fixtures
│ │ │ └── example.json
│ │ ├── integration
│ │ │ └── app.spec.ts
│ │ └── support
│ │ │ ├── app.po.ts
│ │ │ ├── commands.ts
│ │ │ └── index.ts
│ └── tsconfig.json
└── angular-feature-sliced
│ ├── .browserslistrc
│ ├── .eslintrc.json
│ ├── jest.config.js
│ ├── src
│ ├── app
│ │ ├── app.component.html
│ │ ├── app.component.scss
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ └── core
│ │ │ ├── index.ts
│ │ │ └── store
│ │ │ ├── index.ts
│ │ │ ├── store.module.ts
│ │ │ └── store.ts
│ ├── assets
│ │ └── .gitkeep
│ ├── entities
│ │ └── task
│ │ │ ├── components
│ │ │ ├── index.ts
│ │ │ ├── task-card
│ │ │ │ ├── index.ts
│ │ │ │ ├── task-card.component.html
│ │ │ │ ├── task-card.component.scss
│ │ │ │ └── task-card.component.ts
│ │ │ └── task-row
│ │ │ │ ├── index.ts
│ │ │ │ ├── task-row.component.html
│ │ │ │ ├── task-row.component.scss
│ │ │ │ └── task-row.component.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ ├── index.ts
│ │ │ └── task-status.pipe.ts
│ │ │ ├── model
│ │ │ ├── index.ts
│ │ │ ├── task.actions.ts
│ │ │ ├── task.effects.ts
│ │ │ ├── task.facade.ts
│ │ │ ├── task.models.ts
│ │ │ ├── task.reducer.ts
│ │ │ └── task.selectors.ts
│ │ │ └── task.module.ts
│ ├── environments
│ │ ├── environment.prod.ts
│ │ ├── environment.ts
│ │ └── index.ts
│ ├── favicon.ico
│ ├── features
│ │ ├── tasks-filter
│ │ │ ├── components
│ │ │ │ ├── index.ts
│ │ │ │ └── tasks-filter
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tasks-filter.component.html
│ │ │ │ │ ├── tasks-filter.component.scss
│ │ │ │ │ └── tasks-filter.component.ts
│ │ │ ├── index.ts
│ │ │ ├── model
│ │ │ │ ├── config.service.ts
│ │ │ │ └── index.ts
│ │ │ └── tasks-filter.module.ts
│ │ └── toggle-task
│ │ │ ├── components
│ │ │ ├── index.ts
│ │ │ └── toggle-task
│ │ │ │ ├── index.ts
│ │ │ │ ├── toggle-task.component.html
│ │ │ │ ├── toggle-task.component.scss
│ │ │ │ └── toggle-task.component.ts
│ │ │ ├── index.ts
│ │ │ └── toggle-task.module.ts
│ ├── index.html
│ ├── main.ts
│ ├── pages
│ │ ├── app-routing.module.ts
│ │ ├── index.ts
│ │ ├── not-found
│ │ │ ├── index.ts
│ │ │ ├── not-found.module.ts
│ │ │ ├── not-found.page.html
│ │ │ ├── not-found.page.scss
│ │ │ └── not-found.page.ts
│ │ ├── task-details
│ │ │ ├── index.ts
│ │ │ ├── task-details-routing.module.ts
│ │ │ ├── task-details.module.ts
│ │ │ ├── task-details.page.html
│ │ │ ├── task-details.page.scss
│ │ │ └── task-details.page.ts
│ │ └── tasks-list
│ │ │ ├── index.ts
│ │ │ ├── task-list.module.ts
│ │ │ ├── tasks-list.page.html
│ │ │ ├── tasks-list.page.scss
│ │ │ └── tasks-list.page.ts
│ ├── polyfills.ts
│ ├── shared
│ │ ├── api
│ │ │ ├── index.ts
│ │ │ └── typicode
│ │ │ │ ├── index.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── typicode.service.ts
│ │ ├── lib
│ │ │ ├── index.ts
│ │ │ ├── router
│ │ │ │ ├── index.ts
│ │ │ │ ├── router.facade.ts
│ │ │ │ └── router.selectors.ts
│ │ │ └── shared.module.ts
│ │ └── ui
│ │ │ ├── index.ts
│ │ │ └── ui-kit.module.ts
│ ├── styles.scss
│ └── test-setup.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.editor.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── decorate-angular-cli.js
├── jest.config.js
├── jest.preset.js
├── libs
└── .gitkeep
├── nx.json
├── package-lock.json
├── package.json
├── tools
├── generators
│ └── .gitkeep
└── tsconfig.tools.json
└── tsconfig.base.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.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 | "@angular-eslint/component-class-suffix": [
23 | "warn",
24 | {
25 | "suffixes": ["Component", "Page", "Container", "Widget"]
26 | }
27 | ]
28 | }
29 | },
30 | {
31 | "files": ["*.ts", "*.tsx"],
32 | "extends": ["plugin:@nrwl/nx/typescript"],
33 | "rules": {}
34 | },
35 | {
36 | "files": ["*.js", "*.jsx"],
37 | "extends": ["plugin:@nrwl/nx/javascript"],
38 | "rules": {}
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
41 | .angular
42 |
43 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
44 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
45 |
46 | # User-specific stuff
47 | .idea/**/workspace.xml
48 | .idea/**/tasks.xml
49 | .idea/**/usage.statistics.xml
50 | .idea/**/dictionaries
51 | .idea/**/shelf
52 |
53 | # AWS User-specific
54 | .idea/**/aws.xml
55 |
56 | # Generated files
57 | .idea/**/contentModel.xml
58 |
59 | # Sensitive or high-churn files
60 | .idea/**/dataSources/
61 | .idea/**/dataSources.ids
62 | .idea/**/dataSources.local.xml
63 | .idea/**/sqlDataSources.xml
64 | .idea/**/dynamic.xml
65 | .idea/**/uiDesigner.xml
66 | .idea/**/dbnavigator.xml
67 |
68 | # Gradle
69 | .idea/**/gradle.xml
70 | .idea/**/libraries
71 |
72 | # Gradle and Maven with auto-import
73 | # When using Gradle or Maven with auto-import, you should exclude module files,
74 | # since they will be recreated, and may cause churn. Uncomment if using
75 | # auto-import.
76 | # .idea/artifacts
77 | # .idea/compiler.xml
78 | # .idea/jarRepositories.xml
79 | # .idea/modules.xml
80 | # .idea/*.iml
81 | # .idea/modules
82 | # *.iml
83 | # *.ipr
84 |
85 | # CMake
86 | cmake-build-*/
87 |
88 | # Mongo Explorer plugin
89 | .idea/**/mongoSettings.xml
90 |
91 | # File-based project format
92 | *.iws
93 |
94 | # IntelliJ
95 | out/
96 |
97 | # mpeltonen/sbt-idea plugin
98 | .idea_modules/
99 |
100 | # JIRA plugin
101 | atlassian-ide-plugin.xml
102 |
103 | # Cursive Clojure plugin
104 | .idea/replstate.xml
105 |
106 | # SonarLint plugin
107 | .idea/sonarlint/
108 |
109 | # Crashlytics plugin (for Android Studio and IntelliJ)
110 | com_crashlytics_export_strings.xml
111 | crashlytics.properties
112 | crashlytics-build.properties
113 | fabric.properties
114 |
115 | # Editor-based Rest Client
116 | .idea/httpRequests
117 |
118 | # Android studio 3.1+ serialized cache file
119 | .idea/caches/build_file_checksums.ser
120 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "angular.ng-template",
4 | "nrwl.angular-console",
5 | "esbenp.prettier-vscode",
6 | "firsttris.vscode-jest-runner",
7 | "dbaeumer.vscode-eslint"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Feature Sliced
2 |
3 | 
4 | [](https://feature-sliced.design/)
5 | [](https://app.netlify.com/sites/laughing-boyd-e3122c/deploys)
6 |
7 | - [Architecture design guidelines](https://feature-sliced.design/)
8 |
9 | ## Technology stack
10 |
11 | - **UI**: `angular`, `angular-material`, `scss`
12 | - **Data model**: `ngrx`
13 | - **Lang**: `typescript`
14 | - **Lint**: `eslint`, `prettier`, `stylelint`
15 | - **Architecture**: `feature-sliced`
16 |
17 |
18 |

19 |

20 |

21 |

22 |

23 |

24 |

25 |

26 |
27 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "projects": {
4 | "angular-feature-sliced": {
5 | "projectType": "application",
6 | "root": "apps/angular-feature-sliced",
7 | "sourceRoot": "apps/angular-feature-sliced/src",
8 | "prefix": "fs",
9 | "architect": {
10 | "build": {
11 | "builder": "@angular-devkit/build-angular:browser",
12 | "outputs": [
13 | "{options.outputPath}"
14 | ],
15 | "options": {
16 | "outputPath": "dist/apps/angular-feature-sliced",
17 | "index": "apps/angular-feature-sliced/src/index.html",
18 | "main": "apps/angular-feature-sliced/src/main.ts",
19 | "polyfills": "apps/angular-feature-sliced/src/polyfills.ts",
20 | "tsConfig": "apps/angular-feature-sliced/tsconfig.app.json",
21 | "inlineStyleLanguage": "scss",
22 | "assets": [
23 | "apps/angular-feature-sliced/src/favicon.ico",
24 | "apps/angular-feature-sliced/src/assets"
25 | ],
26 | "styles": [
27 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
28 | "apps/angular-feature-sliced/src/styles.scss"
29 | ],
30 | "scripts": []
31 | },
32 | "configurations": {
33 | "production": {
34 | "budgets": [
35 | {
36 | "type": "initial",
37 | "maximumWarning": "500kb",
38 | "maximumError": "1mb"
39 | },
40 | {
41 | "type": "anyComponentStyle",
42 | "maximumWarning": "2kb",
43 | "maximumError": "4kb"
44 | }
45 | ],
46 | "fileReplacements": [
47 | {
48 | "replace": "apps/angular-feature-sliced/src/environments/environment.ts",
49 | "with": "apps/angular-feature-sliced/src/environments/environment.prod.ts"
50 | }
51 | ],
52 | "outputHashing": "all"
53 | },
54 | "development": {
55 | "buildOptimizer": false,
56 | "optimization": false,
57 | "vendorChunk": true,
58 | "extractLicenses": false,
59 | "sourceMap": true,
60 | "namedChunks": true
61 | }
62 | },
63 | "defaultConfiguration": "production"
64 | },
65 | "serve": {
66 | "builder": "@angular-devkit/build-angular:dev-server",
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "angular-feature-sliced:build:production"
70 | },
71 | "development": {
72 | "browserTarget": "angular-feature-sliced:build:development"
73 | }
74 | },
75 | "defaultConfiguration": "development"
76 | },
77 | "extract-i18n": {
78 | "builder": "@angular-devkit/build-angular:extract-i18n",
79 | "options": {
80 | "browserTarget": "angular-feature-sliced:build"
81 | }
82 | },
83 | "lint": {
84 | "builder": "@nrwl/linter:eslint",
85 | "options": {
86 | "lintFilePatterns": [
87 | "apps/angular-feature-sliced/src/**/*.ts",
88 | "apps/angular-feature-sliced/src/**/*.html"
89 | ]
90 | }
91 | },
92 | "test": {
93 | "builder": "@nrwl/jest:jest",
94 | "outputs": [
95 | "coverage/apps/angular-feature-sliced"
96 | ],
97 | "options": {
98 | "jestConfig": "apps/angular-feature-sliced/jest.config.js",
99 | "passWithNoTests": true
100 | }
101 | }
102 | },
103 | "tags": []
104 | },
105 | "angular-feature-sliced-e2e": {
106 | "root": "apps/angular-feature-sliced-e2e",
107 | "sourceRoot": "apps/angular-feature-sliced-e2e/src",
108 | "projectType": "application",
109 | "architect": {
110 | "e2e": {
111 | "builder": "@nrwl/cypress:cypress",
112 | "options": {
113 | "cypressConfig": "apps/angular-feature-sliced-e2e/cypress.json",
114 | "devServerTarget": "angular-feature-sliced:serve:development"
115 | },
116 | "configurations": {
117 | "production": {
118 | "devServerTarget": "angular-feature-sliced:serve:production"
119 | }
120 | }
121 | },
122 | "lint": {
123 | "builder": "@nrwl/linter:eslint",
124 | "outputs": [
125 | "{options.outputFile}"
126 | ],
127 | "options": {
128 | "lintFilePatterns": [
129 | "apps/angular-feature-sliced-e2e/**/*.{js,ts}"
130 | ]
131 | }
132 | }
133 | },
134 | "tags": [],
135 | "implicitDependencies": [
136 | "angular-feature-sliced"
137 | ]
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
3 | "ignorePatterns": ["!**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7 | "rules": {}
8 | },
9 | {
10 | "files": ["src/plugins/index.js"],
11 | "rules": {
12 | "@typescript-eslint/no-var-requires": "off",
13 | "no-undef": "off"
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileServerFolder": ".",
3 | "fixturesFolder": "./src/fixtures",
4 | "integrationFolder": "./src/integration",
5 | "modifyObstructiveCode": false,
6 | "supportFile": "./src/support/index.ts",
7 | "pluginsFile": false,
8 | "video": true,
9 | "videosFolder": "../../dist/cypress/apps/angular-feature-sliced-e2e/videos",
10 | "screenshotsFolder": "../../dist/cypress/apps/angular-feature-sliced-e2e/screenshots",
11 | "chromeWebSecurity": false
12 | }
13 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/src/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io"
4 | }
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/src/integration/app.spec.ts:
--------------------------------------------------------------------------------
1 | import { getGreeting } from '../support/app.po';
2 |
3 | describe('angular-feature-sliced', () => {
4 | beforeEach(() => cy.visit('/'));
5 |
6 | it('should display welcome message', () => {
7 | // Custom command example, see `../support/commands.ts` file
8 | cy.login('my-email@something.com', 'myPassword');
9 |
10 | // Function helper example, see `../support/app.po.ts` file
11 | getGreeting().contains('Welcome angular-feature-sliced');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/src/support/app.po.ts:
--------------------------------------------------------------------------------
1 | export const getGreeting = () => cy.get('h1');
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/src/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 |
11 | // eslint-disable-next-line @typescript-eslint/no-namespace
12 | declare namespace Cypress {
13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
14 | interface Chainable {
15 | login(email: string, password: string): void;
16 | }
17 | }
18 | //
19 | // -- This is a parent command --
20 | Cypress.Commands.add('login', (email, password) => {
21 | console.log('Custom command example: Login', email, password);
22 | });
23 | //
24 | // -- This is a child command --
25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
26 | //
27 | //
28 | // -- This is a dual command --
29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
30 | //
31 | //
32 | // -- This will overwrite an existing command --
33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
34 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/src/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced-e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "sourceMap": false,
5 | "outDir": "../../dist/out-tsc",
6 | "allowJs": true,
7 | "types": ["cypress", "node"],
8 | "forceConsistentCasingInFileNames": true,
9 | "strict": true,
10 | "noImplicitOverride": true,
11 | "noPropertyAccessFromIndexSignature": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true
14 | },
15 | "include": ["src/**/*.ts", "src/**/*.js"],
16 | "angularCompilerOptions": {
17 | "strictInjectionParameters": true,
18 | "strictInputAccessModifiers": true,
19 | "strictTemplates": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/.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 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "../../.eslintrc.json",
4 | "@feature-sliced/eslint-config/rules/import-order",
5 | "@feature-sliced/eslint-config/rules/public-api",
6 | "@feature-sliced/eslint-config/rules/layers-slices"
7 | ],
8 | "ignorePatterns": ["!**/*"],
9 | "overrides": [
10 | {
11 | "files": ["*.ts"],
12 | "extends": [
13 | "plugin:@nrwl/nx/angular",
14 | "plugin:@angular-eslint/template/process-inline-templates"
15 | ],
16 | "rules": {
17 | "@angular-eslint/directive-selector": [
18 | "error",
19 | {
20 | "type": "attribute",
21 | "prefix": "fs",
22 | "style": "camelCase"
23 | }
24 | ],
25 | "@angular-eslint/component-selector": [
26 | "error",
27 | {
28 | "type": "element",
29 | "prefix": "fs",
30 | "style": "kebab-case"
31 | }
32 | ]
33 | }
34 | },
35 | {
36 | "files": ["*.html"],
37 | "extends": ["plugin:@nrwl/nx/angular-template"],
38 | "rules": {}
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'angular-feature-sliced',
3 | preset: '../../jest.preset.js',
4 | setupFilesAfterEnv: ['/src/test-setup.ts'],
5 | globals: {
6 | 'ts-jest': {
7 | tsconfig: '/tsconfig.spec.json',
8 | stringifyContentPathRegex: '\\.(html|svg)$',
9 | },
10 | },
11 | coverageDirectory: '../../coverage/apps/angular-feature-sliced',
12 | transform: {
13 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
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/angular-feature-sliced/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Affiction/angular-feature-sliced/7c10b81129584e75da69d83fea0499f00ed7fb8e/apps/angular-feature-sliced/src/app/app.component.scss
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'fs-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss'],
7 | })
8 | export class AppComponent {}
9 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 | import { AppRoutingModule } from 'pages';
5 | import { AppComponent } from 'app/app.component';
6 | import { AppStoreModule } from 'app/core';
7 |
8 | @NgModule({
9 | declarations: [AppComponent],
10 | imports: [
11 | BrowserModule,
12 | BrowserAnimationsModule,
13 | AppRoutingModule,
14 | AppStoreModule,
15 | ],
16 | bootstrap: [AppComponent],
17 | })
18 | export class AppModule {}
19 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/core/index.ts:
--------------------------------------------------------------------------------
1 | export { AppStoreModule } from './store';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/core/store/index.ts:
--------------------------------------------------------------------------------
1 | export { AppStoreModule } from './store.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/core/store/store.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { EffectsModule } from '@ngrx/effects';
3 | import { StoreRouterConnectingModule } from '@ngrx/router-store';
4 | import { StoreModule } from '@ngrx/store';
5 | import { StoreDevtoolsModule } from '@ngrx/store-devtools';
6 | import { NxModule } from '@nrwl/angular';
7 | import { environment } from 'environments';
8 |
9 | import { metaReducers, ROOT_REDUCERS, rootEffects } from './store';
10 |
11 | @NgModule({
12 | imports: [
13 | NxModule.forRoot(), // Provide DataPersistence service: https://nx.dev/l/a/guides/misc-data-persistence#optimistic-updates
14 | StoreModule.forRoot(ROOT_REDUCERS, {
15 | metaReducers,
16 | runtimeChecks: {
17 | strictActionImmutability: true,
18 | strictStateImmutability: true,
19 | strictStateSerializability: true,
20 | strictActionSerializability: true,
21 | strictActionWithinNgZone: true,
22 | strictActionTypeUniqueness: true,
23 | },
24 | }),
25 | StoreRouterConnectingModule.forRoot(),
26 | EffectsModule.forRoot(rootEffects),
27 | !environment.production
28 | ? StoreDevtoolsModule.instrument({
29 | name: 'Angular Feature Sliced',
30 | })
31 | : [],
32 | ],
33 | })
34 | export class AppStoreModule {}
35 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/app/core/store/store.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken, Type } from '@angular/core';
2 | import * as FromRouter from '@ngrx/router-store';
3 | import { Action, ActionReducerMap, MetaReducer } from '@ngrx/store';
4 | import { environment } from 'environments';
5 | import { TaskModel } from 'entities/task';
6 |
7 | /**
8 | * Every reducer module's default export is the reducer function it¬self. In
9 | * addition, each module should export a type or interface that describes
10 | * the state of the reducer plus any selector functions. The `* as`
11 | * notation packages up all of the exports into a single object.
12 | */
13 | /**
14 | * As mentioned, we treat each reducer like a table in a database. This means
15 | * our top level state interface is just a map of keys to inner state types.
16 | */
17 | export interface State {
18 | router: FromRouter.RouterState;
19 | [TaskModel.TASK_FEATURE_KEY]: TaskModel.State;
20 | }
21 |
22 | /**
23 | * Our state is composed of a map of action reducer functions.
24 | * These reducer functions are called with each dispatched action
25 | * and the current or initial state and return a new immutable state.
26 | */
27 | export const ROOT_REDUCERS = new InjectionToken<
28 | ActionReducerMap
29 | >('Root reducers token', {
30 | factory: () => ({
31 | router: FromRouter.routerReducer,
32 | [TaskModel.TASK_FEATURE_KEY]: TaskModel.reducer,
33 | }),
34 | });
35 |
36 | /**
37 | * By default, @ngrx/store uses combineReducers with the reducer map to compose
38 | * the root meta-reducer. To add more meta-reducers, provide an array of meta-reducers
39 | * that will be composed to form the root meta-reducer.
40 | */
41 | export const metaReducers: MetaReducer[] = !environment.production
42 | ? []
43 | : [];
44 |
45 | export const rootEffects: Type[] = [TaskModel.TaskEffects];
46 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Affiction/angular-feature-sliced/7c10b81129584e75da69d83fea0499f00ed7fb8e/apps/angular-feature-sliced/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './task-card';
2 | export * from './task-row';
3 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-card/index.ts:
--------------------------------------------------------------------------------
1 | export * from './task-card.component';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-card/task-card.component.html:
--------------------------------------------------------------------------------
1 |
2 | Task#{{ task.id }}
3 | {{ task.title }}
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-card/task-card.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
5 | .card {
6 | &__title {
7 | margin-bottom: 20px;
8 | }
9 |
10 | &__footer {
11 | display: flex;
12 | justify-content: center;
13 | padding: 20px 0;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-card/task-card.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | EventEmitter,
5 | Input,
6 | Output,
7 | } from '@angular/core';
8 | import type { Task } from 'shared/api';
9 |
10 | @Component({
11 | selector: 'fs-task-card',
12 | templateUrl: './task-card.component.html',
13 | styleUrls: ['./task-card.component.scss'],
14 | changeDetection: ChangeDetectionStrategy.OnPush,
15 | })
16 | export class TaskCardComponent {
17 | @Input()
18 | task!: Task;
19 |
20 | @Output()
21 | backToTask: EventEmitter = new EventEmitter();
22 | }
23 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-row/index.ts:
--------------------------------------------------------------------------------
1 | export * from './task-row.component';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-row/task-row.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ task?.title }}
4 |
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-row/task-row.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
5 | .card {
6 | &:hover {
7 | cursor: pointer;
8 | }
9 | }
10 |
11 | .completed {
12 | opacity: .5;
13 | text-decoration: line-through;
14 | }
15 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/components/task-row/task-row.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2 | import type { Task } from 'shared/api';
3 |
4 | @Component({
5 | selector: 'fs-task-row',
6 | templateUrl: './task-row.component.html',
7 | styleUrls: ['./task-row.component.scss'],
8 | changeDetection: ChangeDetectionStrategy.OnPush,
9 | })
10 | export class TaskRowComponent {
11 | @Input()
12 | task!: Task;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/index.ts:
--------------------------------------------------------------------------------
1 | export * as TaskModel from './model';
2 | export * as TaskLib from './lib';
3 | export { TaskFacade } from './model';
4 | export { TaskModule } from './task.module';
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './task-status.pipe';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/lib/task-status.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import type { Task } from 'shared/api';
3 |
4 | type Status = 'CLOSED' | 'OPENED';
5 |
6 | @Pipe({ name: 'taskStatus' })
7 | export class TaskStatusPipe implements PipeTransform {
8 | transform(task: Task): Status {
9 | return task.completed ? 'CLOSED' : 'OPENED';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/index.ts:
--------------------------------------------------------------------------------
1 | export { TaskFacade } from './task.facade';
2 | export * from './task.models';
3 | export * from './task.effects';
4 | export * from './task.reducer';
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction, props } from '@ngrx/store';
2 | import type { Task } from 'shared/api';
3 | import { QueryConfig } from './task.models';
4 |
5 | export const loadAllTasks = createAction('[Task/API] Load All Tasks');
6 | export const loadTasksSuccess = createAction(
7 | '[Task/API] Load All Tasks Success',
8 | props<{ tasks: Task[] }>()
9 | );
10 | export const loadTasksFailure = createAction(
11 | '[Task/API] Load All Tasks Failure',
12 | props<{ error: unknown }>()
13 | );
14 |
15 | export const filterTasks = createAction(
16 | '[Task] Filter Tasks',
17 | props<{ queryConfig: QueryConfig }>()
18 | );
19 |
20 | export const toggleTask = createAction(
21 | '[Task] Toggle Task',
22 | props<{ task: Task }>()
23 | );
24 |
25 | export const loadTask = createAction(
26 | '[Task/API] Load Current Task',
27 | props<{ id: number | string }>()
28 | );
29 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.effects.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Actions, createEffect } from '@ngrx/effects';
3 | import { DataPersistence } from '@nrwl/angular';
4 | import { map } from 'rxjs/operators';
5 | import { TypicodeService } from 'shared/api';
6 |
7 | import * as TaskActions from './task.actions';
8 | import * as TaskFeature from './task.reducer';
9 |
10 | @Injectable()
11 | export class TaskEffects {
12 | loadAllTasks$ = createEffect(() =>
13 | this.dataPersistence.fetch(TaskActions.loadAllTasks, {
14 | run: () => {
15 | return this.typicodeApi
16 | .getTasksList()
17 | .pipe(map((tasks) => TaskActions.loadTasksSuccess({ tasks })));
18 | },
19 | onError: (_, error) => {
20 | console.error('Error', error);
21 | return TaskActions.loadTasksFailure({ error });
22 | },
23 | })
24 | );
25 |
26 | constructor(
27 | private readonly actions$: Actions,
28 | private readonly dataPersistence: DataPersistence,
29 | private readonly typicodeApi: TypicodeService
30 | ) {}
31 | }
32 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import type { Task } from 'shared/api';
4 |
5 | import * as TaskActions from './task.actions';
6 | import { QueryConfig } from './task.models';
7 | import * as TaskSelectors from './task.selectors';
8 |
9 | @Injectable({ providedIn: 'root' })
10 | export class TaskFacade {
11 | loaded$ = this.store.select(TaskSelectors.getTaskLoaded);
12 | loading$ = this.store.select(TaskSelectors.getTaskLoading);
13 | allTask$ = this.store.select(TaskSelectors.getAllTask);
14 | selectedTask$ = this.store.select(TaskSelectors.getSelected);
15 | filteredTasks$ = this.store.select(TaskSelectors.getFilteredTasks);
16 |
17 | constructor(private readonly store: Store) {}
18 |
19 | loadTasks(): void {
20 | this.store.dispatch(TaskActions.loadAllTasks());
21 | }
22 |
23 | filterTasks(queryConfig: QueryConfig): void {
24 | this.store.dispatch(TaskActions.filterTasks({ queryConfig }));
25 | }
26 |
27 | toggleTask(task: Task) {
28 | this.store.dispatch(TaskActions.toggleTask({ task }));
29 | }
30 |
31 | loadTask(id: number | string) {
32 | this.store.dispatch(TaskActions.loadTask({ id }));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.models.ts:
--------------------------------------------------------------------------------
1 | export type QueryConfig = {
2 | completed?: boolean;
3 | userId?: number;
4 | };
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.reducer.ts:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
2 | import { Action, createReducer, on } from '@ngrx/store';
3 | import type { Task } from 'shared/api';
4 |
5 | import * as TaskActions from './task.actions';
6 | import { QueryConfig } from './task.models';
7 |
8 | export const TASK_FEATURE_KEY = 'task';
9 |
10 | export interface State extends EntityState {
11 | selectedId?: string | number;
12 | loading?: boolean;
13 | loaded?: boolean;
14 | error?: unknown;
15 | queryConfig?: QueryConfig;
16 | }
17 |
18 | export interface TaskPartialState {
19 | readonly [TASK_FEATURE_KEY]: State;
20 | }
21 |
22 | export const taskAdapter: EntityAdapter = createEntityAdapter();
23 |
24 | export const initialState: State = taskAdapter.getInitialState({
25 | loading: false,
26 | loaded: false,
27 | });
28 |
29 | const taskReducer = createReducer(
30 | initialState,
31 | on(TaskActions.loadAllTasks, (state) => ({
32 | ...state,
33 | loaded: false,
34 | loading: true,
35 | error: null,
36 | })),
37 | on(TaskActions.loadTasksSuccess, (state, { tasks }) =>
38 | taskAdapter.setAll(tasks, { ...state, loading: false, loaded: true })
39 | ),
40 | on(TaskActions.loadTasksFailure, (state, { error }) => ({ ...state, error })),
41 |
42 | on(TaskActions.filterTasks, (state, { queryConfig }) => ({
43 | ...state,
44 | queryConfig,
45 | })),
46 |
47 | on(TaskActions.toggleTask, (state, { task }) => {
48 | return taskAdapter.updateOne(
49 | {
50 | id: task.id,
51 | changes: {
52 | ...task,
53 | completed: !task.completed,
54 | },
55 | },
56 | state
57 | );
58 | })
59 | );
60 |
61 | export function reducer(state: State | undefined, action: Action) {
62 | return taskReducer(state, action);
63 | }
64 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/model/task.selectors.ts:
--------------------------------------------------------------------------------
1 | import { createFeatureSelector, createSelector } from '@ngrx/store';
2 | import { RouterModel } from 'shared/lib';
3 | import { TASK_FEATURE_KEY, State, taskAdapter } from './task.reducer';
4 |
5 | export const getTaskState = createFeatureSelector(TASK_FEATURE_KEY);
6 |
7 | const { selectAll, selectEntities } = taskAdapter.getSelectors();
8 |
9 | export const getTaskLoading = createSelector(
10 | getTaskState,
11 | (state: State) => state.loading
12 | );
13 |
14 | export const getTaskLoaded = createSelector(
15 | getTaskState,
16 | (state: State) => state.loaded
17 | );
18 |
19 | export const getTaskError = createSelector(
20 | getTaskState,
21 | (state: State) => state.error
22 | );
23 |
24 | export const getAllTask = createSelector(getTaskState, (state: State) =>
25 | selectAll(state)
26 | );
27 |
28 | export const getTaskEntities = createSelector(getTaskState, (state: State) =>
29 | selectEntities(state)
30 | );
31 |
32 | export const getSelected = createSelector(
33 | getTaskEntities,
34 | RouterModel.selectRouteParam('id'),
35 | (entities, selectedId) => (selectedId ? entities[selectedId] : undefined)
36 | );
37 |
38 | export const getQueryConfig = createSelector(
39 | getTaskState,
40 | (state: State) => state.queryConfig
41 | );
42 |
43 | export const getFilteredTasks = createSelector(
44 | getAllTask,
45 | getQueryConfig,
46 | (tasks, config) =>
47 | tasks.filter(
48 | (task) =>
49 | config?.completed === undefined || task.completed === config.completed
50 | )
51 | );
52 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/entities/task/task.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { SharedModule } from 'shared/lib';
3 | import { UiKitModule } from 'shared/ui';
4 |
5 | import { TaskCardComponent, TaskRowComponent } from './components';
6 | import { TaskStatusPipe } from './lib';
7 |
8 | @NgModule({
9 | declarations: [TaskCardComponent, TaskRowComponent, TaskStatusPipe],
10 | imports: [SharedModule, UiKitModule],
11 | exports: [TaskCardComponent, TaskRowComponent, TaskStatusPipe],
12 | })
13 | export class TaskModule {}
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | apiUrl: 'https://jsonplaceholder.typicode.com',
4 | };
5 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | apiUrl: 'https://jsonplaceholder.typicode.com',
8 | };
9 |
10 | /*
11 | * For easier debugging in development mode, you can import the following file
12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
13 | *
14 | * This import should be commented out in production mode because it will have a negative impact
15 | * on performance if an error is thrown.
16 | */
17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/environments/index.ts:
--------------------------------------------------------------------------------
1 | export { environment } from './environment';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Affiction/angular-feature-sliced/7c10b81129584e75da69d83fea0499f00ed7fb8e/apps/angular-feature-sliced/src/favicon.ico
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tasks-filter';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/components/tasks-filter/index.ts:
--------------------------------------------------------------------------------
1 | export * from './tasks-filter.component';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/components/tasks-filter/tasks-filter.component.html:
--------------------------------------------------------------------------------
1 |
7 |
15 | {{ filter.title }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/components/tasks-filter/tasks-filter.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/components/tasks-filter/tasks-filter.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { TaskFacade } from 'entities/task';
3 |
4 | import { ConfigService } from '../../model';
5 |
6 | @Component({
7 | selector: 'fs-tasks-filter',
8 | templateUrl: './tasks-filter.component.html',
9 | styleUrls: ['./tasks-filter.component.scss'],
10 | changeDetection: ChangeDetectionStrategy.OnPush,
11 | })
12 | export class TasksFilterComponent {
13 | constructor(
14 | public readonly configService: ConfigService,
15 | public readonly taskFacade: TaskFacade
16 | ) {}
17 | }
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/index.ts:
--------------------------------------------------------------------------------
1 | export { TasksFilterModule } from './tasks-filter.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/model/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { TaskModel } from 'entities/task';
3 |
4 | export type Filter = {
5 | id: number;
6 | title: string;
7 | config: TaskModel.QueryConfig;
8 | };
9 |
10 | export const DEFAULT_FILTER = 1;
11 |
12 | @Injectable({
13 | providedIn: 'root',
14 | })
15 | export class ConfigService {
16 | public currentFilter: number = DEFAULT_FILTER;
17 | readonly filters: Record = {
18 | 1: {
19 | id: 1,
20 | title: 'All',
21 | config: {},
22 | },
23 | 2: {
24 | id: 2,
25 | title: 'Opened',
26 | config: { completed: false },
27 | },
28 | 3: {
29 | id: 3,
30 | title: 'Closed',
31 | config: { completed: true },
32 | },
33 | };
34 | readonly filtersList = Object.values(this.filters);
35 |
36 | getFilterById(id: number): Filter {
37 | return this.filters[id];
38 | }
39 |
40 | selectFilter(id: number): void {
41 | this.currentFilter = id;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config.service';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/tasks-filter/tasks-filter.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { SharedModule } from 'shared/lib';
3 | import { UiKitModule } from 'shared/ui';
4 |
5 | import { TasksFilterComponent } from './components';
6 | import { ConfigService } from './model';
7 |
8 | const EXPORT_COMPONENTS = [TasksFilterComponent];
9 |
10 | @NgModule({
11 | declarations: EXPORT_COMPONENTS,
12 | imports: [SharedModule, UiKitModule],
13 | exports: EXPORT_COMPONENTS,
14 | providers: [ConfigService],
15 | })
16 | export class TasksFilterModule {}
17 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './toggle-task';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/components/toggle-task/index.ts:
--------------------------------------------------------------------------------
1 | export * from './toggle-task.component';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/components/toggle-task/toggle-task.component.html:
--------------------------------------------------------------------------------
1 |
6 | {{ withStatus ? (task | taskStatus) : null }}
7 |
8 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/components/toggle-task/toggle-task.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: inline-block;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/components/toggle-task/toggle-task.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2 | import { TaskFacade } from 'entities/task';
3 | import type { Task } from 'shared/api';
4 |
5 | @Component({
6 | selector: 'fs-toggle-task',
7 | templateUrl: './toggle-task.component.html',
8 | styleUrls: ['./toggle-task.component.scss'],
9 | changeDetection: ChangeDetectionStrategy.OnPush,
10 | })
11 | export class ToggleTaskComponent {
12 | @Input()
13 | task!: Task;
14 |
15 | @Input()
16 | withStatus = false;
17 |
18 | constructor(public readonly taskFacade: TaskFacade) {}
19 | }
20 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/index.ts:
--------------------------------------------------------------------------------
1 | export { ToggleTaskModule } from './toggle-task.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/features/toggle-task/toggle-task.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { TaskModule } from 'entities/task';
3 | import { SharedModule } from 'shared/lib';
4 | import { UiKitModule } from 'shared/ui';
5 |
6 | import { ToggleTaskComponent } from './components';
7 |
8 | @NgModule({
9 | declarations: [ToggleTaskComponent],
10 | imports: [SharedModule, UiKitModule, TaskModule],
11 | exports: [ToggleTaskComponent],
12 | })
13 | export class ToggleTaskModule {}
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Angular Feature Sliced
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 | import { environment } from 'environments';
4 |
5 | import { AppModule } from './app/app.module';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch((err) => console.error(err));
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { NotFoundPage, NotFoundPageModule } from 'pages/not-found';
4 | import { TaskListModule, TasksListPage } from 'pages/tasks-list';
5 |
6 | const routes: Routes = [
7 | { path: '', redirectTo: '/tasks-list', pathMatch: 'full' },
8 | { path: 'tasks-list', component: TasksListPage },
9 | {
10 | path: 'task-details/:id',
11 | loadChildren: () =>
12 | import('./task-details/task-details.module').then(
13 | (m) => m.TaskDetailsModule
14 | ),
15 | },
16 | { path: 'not-found', component: NotFoundPage },
17 | { path: '**', redirectTo: '/not-found' },
18 | ];
19 |
20 | @NgModule({
21 | imports: [NotFoundPageModule, TaskListModule, RouterModule.forRoot(routes)],
22 | exports: [RouterModule],
23 | })
24 | export class AppRoutingModule {}
25 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export { AppRoutingModule } from './app-routing.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/not-found/index.ts:
--------------------------------------------------------------------------------
1 | export { NotFoundPage } from './not-found.page';
2 | export { NotFoundPageModule } from './not-found.module';
3 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/not-found/not-found.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { SharedModule } from 'shared/lib';
3 |
4 | import { NotFoundPage } from './not-found.page';
5 |
6 | @NgModule({
7 | declarations: [NotFoundPage],
8 | imports: [SharedModule],
9 | exports: [NotFoundPage],
10 | })
11 | export class NotFoundPageModule {}
12 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/not-found/not-found.page.html:
--------------------------------------------------------------------------------
1 | not-found works!
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/not-found/not-found.page.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/not-found/not-found.page.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'fs-not-found',
5 | templateUrl: './not-found.page.html',
6 | styleUrls: ['./not-found.page.scss'],
7 | changeDetection: ChangeDetectionStrategy.OnPush,
8 | })
9 | export class NotFoundPage {}
10 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/index.ts:
--------------------------------------------------------------------------------
1 | export * from './task-details.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/task-details-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { TaskDetailsPage } from './task-details.page';
5 |
6 | const routes: Routes = [
7 | {
8 | path: '',
9 | component: TaskDetailsPage,
10 | },
11 | ];
12 |
13 | @NgModule({
14 | imports: [RouterModule.forChild(routes)],
15 | exports: [RouterModule],
16 | })
17 | export class TaskDetailsRoutingModule {}
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/task-details.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ToggleTaskModule } from 'features/toggle-task';
3 | import { TaskModule } from 'entities/task';
4 | import { SharedModule } from 'shared/lib';
5 | import { UiKitModule } from 'shared/ui';
6 |
7 | import { TaskDetailsRoutingModule } from './task-details-routing.module';
8 | import { TaskDetailsPage } from './task-details.page';
9 |
10 | @NgModule({
11 | declarations: [TaskDetailsPage],
12 | imports: [
13 | TaskDetailsRoutingModule,
14 | SharedModule,
15 | UiKitModule,
16 | TaskModule,
17 | ToggleTaskModule,
18 | ],
19 | })
20 | export class TaskDetailsModule {}
21 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/task-details.page.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/task-details.page.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | padding: 20px;
4 | }
5 |
6 | .task-details {
7 | margin: 0 auto;
8 | max-width: 500px;
9 | }
10 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/task-details/task-details.page.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { Location } from '@angular/common';
3 | import { TaskFacade } from 'entities/task';
4 |
5 | @Component({
6 | selector: 'fs-task-details',
7 | templateUrl: './task-details.page.html',
8 | styleUrls: ['./task-details.page.scss'],
9 | changeDetection: ChangeDetectionStrategy.OnPush,
10 | })
11 | export class TaskDetailsPage {
12 | constructor(
13 | public readonly taskFacade: TaskFacade,
14 | public readonly location: Location
15 | ) {}
16 | }
17 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/tasks-list/index.ts:
--------------------------------------------------------------------------------
1 | export { TasksListPage } from './tasks-list.page';
2 | export { TaskListModule } from './task-list.module';
3 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/tasks-list/task-list.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { TasksFilterModule } from 'features/tasks-filter';
3 | import { ToggleTaskModule } from 'features/toggle-task';
4 | import { TaskModule } from 'entities/task';
5 | import { SharedModule } from 'shared/lib';
6 | import { UiKitModule } from 'shared/ui';
7 |
8 | import { TasksListPage } from './tasks-list.page';
9 |
10 | @NgModule({
11 | declarations: [TasksListPage],
12 | imports: [
13 | SharedModule,
14 | UiKitModule,
15 | TaskModule,
16 | TasksFilterModule,
17 | ToggleTaskModule,
18 | ],
19 | exports: [TasksListPage],
20 | })
21 | export class TaskListModule {}
22 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/tasks-list/tasks-list.page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 | 0; else empty;">
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Nothing found!
26 |
27 |
28 |
33 |
34 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/tasks-list/tasks-list.page.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | padding: 20px;
4 | }
5 |
6 | .task-list {
7 | margin: 0 auto;
8 | max-width: 500px;
9 |
10 | &__filter {
11 | display: flex;
12 | justify-content: center;
13 | margin: 0 auto;
14 | margin-bottom: 40px;
15 | }
16 | }
17 |
18 | .task-list {
19 | &__row {
20 | margin-bottom: 15px;
21 | }
22 | }
23 |
24 | .checkbox {
25 | margin-right: 10px;
26 | }
27 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/pages/tasks-list/tasks-list.page.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { filter, tap } from 'rxjs/operators';
4 | import { TaskFacade } from 'entities/task';
5 | import { Task } from 'shared/api';
6 |
7 | @Component({
8 | selector: 'fs-tasks-list',
9 | templateUrl: './tasks-list.page.html',
10 | styleUrls: ['./tasks-list.page.scss'],
11 | changeDetection: ChangeDetectionStrategy.OnPush,
12 | })
13 | export class TasksListPage {
14 | loadTasks$ = this.taskFacade.filteredTasks$.pipe(
15 | filter((tasks) => tasks.length == 0),
16 | tap(() => {
17 | this.taskFacade.loadTasks();
18 | })
19 | );
20 |
21 | constructor(
22 | public readonly taskFacade: TaskFacade,
23 | public readonly router: Router
24 | ) {}
25 |
26 | trackByTaskId(id: number, task: Task) {
27 | return task.id;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including
12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 | /***************************************************************************************************
51 | * APPLICATION IMPORTS
52 | */
53 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './typicode';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/api/typicode/index.ts:
--------------------------------------------------------------------------------
1 | export * from './typicode.service';
2 | export * from './models';
3 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/api/typicode/models.ts:
--------------------------------------------------------------------------------
1 | export type Task = {
2 | id: number;
3 | title: string;
4 | userId: number;
5 | completed: boolean;
6 | };
7 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/api/typicode/typicode.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { environment } from 'environments';
4 | import { Observable } from 'rxjs';
5 |
6 | import type { Task } from './models';
7 | const BASE_URL = '/todos';
8 |
9 | export type GetTasksListParams = {
10 | userId?: number;
11 | completed?: boolean;
12 | };
13 |
14 | export type GetTaskByIdParams = {
15 | taskId: number;
16 | [key: string]: string | number;
17 | };
18 |
19 | @Injectable({
20 | providedIn: 'root',
21 | })
22 | export class TypicodeService {
23 | constructor(private readonly http: HttpClient) {}
24 |
25 | getTasksList(params?: GetTasksListParams): Observable {
26 | return this.http.get(`${environment.apiUrl}${BASE_URL}`, {
27 | params,
28 | });
29 | }
30 |
31 | getTaskById({ taskId, ...params }: GetTaskByIdParams): Observable {
32 | return this.http.get(`${environment.apiUrl}${BASE_URL}/${taskId}`, {
33 | params,
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * as RouterModel from './router';
2 | export { RouterFacade } from './router';
3 | export { SharedModule } from './shared.module';
4 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/lib/router/index.ts:
--------------------------------------------------------------------------------
1 | export * from './router.selectors';
2 | export { RouterFacade } from './router.facade';
3 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/lib/router/router.facade.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 |
4 | import * as RouterSelectors from './router.selectors';
5 |
6 | @Injectable({ providedIn: 'root' })
7 | export class RouterFacade {
8 | queryParams$ = this.store.select(RouterSelectors.selectQueryParams);
9 | routeParams$ = this.store.select(RouterSelectors.selectRouteParams);
10 | selectRouteUrl$ = this.store.select(RouterSelectors.selectUrl);
11 | currentRoute$ = this.store.select(RouterSelectors.selectCurrentRoute);
12 |
13 | constructor(private store: Store) {}
14 | }
15 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/lib/router/router.selectors.ts:
--------------------------------------------------------------------------------
1 | import { getSelectors } from '@ngrx/router-store';
2 |
3 | export const {
4 | selectCurrentRoute, // select the current route
5 | selectFragment, // select the current route fragment
6 | selectQueryParams, // select the current route query params
7 | selectQueryParam, // factory function to select a query param
8 | selectRouteParams, // select the current route params
9 | selectRouteParam, // factory function to select a route param
10 | selectRouteData, // select the current route data
11 | selectUrl, // select the current url
12 | } = getSelectors();
13 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/lib/shared.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { HttpClientModule } from '@angular/common/http';
3 | import { NgModule } from '@angular/core';
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms';
5 |
6 | @NgModule({
7 | exports: [CommonModule, HttpClientModule, FormsModule, ReactiveFormsModule],
8 | })
9 | export class SharedModule {}
10 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ui-kit.module';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/shared/ui/ui-kit.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { MatCheckboxModule } from '@angular/material/checkbox';
3 | import { MatButtonToggleModule } from '@angular/material/button-toggle';
4 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
5 | import { MatCardModule } from '@angular/material/card';
6 | import { MatButtonModule } from '@angular/material/button';
7 |
8 | @NgModule({
9 | exports: [
10 | MatCheckboxModule,
11 | MatButtonToggleModule,
12 | MatProgressSpinnerModule,
13 | MatCardModule,
14 | MatButtonModule,
15 | ],
16 | })
17 | export class UiKitModule {}
18 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | html,
4 | body {
5 | height: 100%;
6 | }
7 | body {
8 | margin: 0;
9 | font-family: Roboto, 'Helvetica Neue', sans-serif;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import 'jest-preset-angular/setup-jest';
2 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": [],
6 | "target": "ES2017"
7 | },
8 | "files": ["src/main.ts", "src/polyfills.ts"],
9 | "include": ["src/**/*.d.ts"],
10 | "exclude": ["**/*.test.ts", "**/*.spec.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/tsconfig.editor.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["**/*.ts"],
4 | "compilerOptions": {
5 | "types": ["jest", "node"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/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 | "baseUrl": "./src",
18 | "forceConsistentCasingInFileNames": true,
19 | "strict": true,
20 | "noImplicitOverride": true,
21 | "noPropertyAccessFromIndexSignature": true,
22 | "noImplicitReturns": true,
23 | "noFallthroughCasesInSwitch": true
24 | },
25 | "angularCompilerOptions": {
26 | "strictInjectionParameters": true,
27 | "strictInputAccessModifiers": true,
28 | "strictTemplates": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/angular-feature-sliced/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": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/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('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.');
33 | process.exit(0);
34 | }
35 |
36 | /**
37 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still
38 | * invoke the Nx CLI and get the benefits of computation caching.
39 | */
40 | function symlinkNgCLItoNxCLI() {
41 | try {
42 | const ngPath = './node_modules/.bin/ng';
43 | const nxPath = './node_modules/.bin/nx';
44 | if (isWindows) {
45 | /**
46 | * This is the most reliable way to create symlink-like behavior on Windows.
47 | * Such that it works in all shells and works with npx.
48 | */
49 | ['', '.cmd', '.ps1'].forEach(ext => {
50 | if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext));
51 | });
52 | } else {
53 | // If unix-based, symlink
54 | cp.execSync(`ln -sf ./nx ${ngPath}`);
55 | }
56 | }
57 | catch(e) {
58 | output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message });
59 | throw e;
60 | }
61 | }
62 |
63 | try {
64 | symlinkNgCLItoNxCLI();
65 | require('@nrwl/cli/lib/decorate-cli').decorateCli();
66 | output.log({ title: 'Angular CLI has been decorated to enable computation caching.' });
67 | } catch(e) {
68 | output.error({ title: 'Decoration of the Angular CLI did not complete successfully' });
69 | }
70 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { getJestProjects } = require('@nrwl/jest');
2 |
3 | module.exports = {
4 | projects: getJestProjects(),
5 | };
6 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nrwl/jest/preset');
2 |
3 | module.exports = { ...nxPreset };
4 |
--------------------------------------------------------------------------------
/libs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Affiction/angular-feature-sliced/7c10b81129584e75da69d83fea0499f00ed7fb8e/libs/.gitkeep
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmScope": "angular-feature-sliced",
3 | "affected": {
4 | "defaultBase": "main"
5 | },
6 | "cli": {
7 | "defaultCollection": "@nrwl/angular",
8 | "packageManager": "npm"
9 | },
10 | "implicitDependencies": {
11 | "package.json": {
12 | "dependencies": "*",
13 | "devDependencies": "*"
14 | },
15 | ".eslintrc.json": "*"
16 | },
17 | "tasksRunnerOptions": {
18 | "default": {
19 | "runner": "@nrwl/workspace/tasks-runners/default",
20 | "options": {
21 | "cacheableOperations": [
22 | "build",
23 | "lint",
24 | "test",
25 | "e2e"
26 | ]
27 | }
28 | }
29 | },
30 | "targetDependencies": {
31 | "build": [
32 | {
33 | "target": "build",
34 | "projects": "dependencies"
35 | }
36 | ]
37 | },
38 | "generators": {
39 | "@nrwl/angular:application": {
40 | "style": "scss",
41 | "linter": "eslint",
42 | "unitTestRunner": "jest",
43 | "e2eTestRunner": "cypress"
44 | },
45 | "@nrwl/angular:library": {
46 | "linter": "eslint",
47 | "unitTestRunner": "jest"
48 | },
49 | "@nrwl/angular:component": {
50 | "style": "scss"
51 | }
52 | },
53 | "defaultProject": "angular-feature-sliced"
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-feature-sliced",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "sideEffects": false,
6 | "scripts": {
7 | "ng": "nx",
8 | "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main",
9 | "start": "nx serve",
10 | "build": "nx build",
11 | "test": "nx test"
12 | },
13 | "private": true,
14 | "dependencies": {
15 | "@angular/animations": "~13.1.0",
16 | "@angular/cdk": "^13.1.1",
17 | "@angular/common": "~13.1.0",
18 | "@angular/compiler": "~13.1.0",
19 | "@angular/core": "~13.1.0",
20 | "@angular/forms": "~13.1.0",
21 | "@angular/material": "^13.1.1",
22 | "@angular/platform-browser": "~13.1.0",
23 | "@angular/platform-browser-dynamic": "~13.1.0",
24 | "@angular/router": "~13.1.0",
25 | "@ngrx/component-store": "~13.0.0",
26 | "@ngrx/effects": "~13.0.0",
27 | "@ngrx/entity": "~13.0.0",
28 | "@ngrx/router-store": "~13.0.0",
29 | "@ngrx/store": "~13.0.0",
30 | "@nrwl/angular": "13.4.1",
31 | "rxjs": "~7.4.0",
32 | "tslib": "^2.0.0",
33 | "zone.js": "~0.11.4"
34 | },
35 | "devDependencies": {
36 | "@angular-devkit/build-angular": "~13.1.0",
37 | "@angular-eslint/eslint-plugin": "~13.0.1",
38 | "@angular-eslint/eslint-plugin-template": "~13.0.1",
39 | "@angular-eslint/template-parser": "~13.0.1",
40 | "@angular/cli": "~13.1.0",
41 | "@angular/compiler-cli": "~13.1.0",
42 | "@angular/language-service": "~13.1.0",
43 | "@feature-sliced/eslint-config": "^0.1.0-beta",
44 | "@ngrx/schematics": "~13.0.0",
45 | "@ngrx/store-devtools": "~13.0.0",
46 | "@nrwl/cli": "13.4.1",
47 | "@nrwl/cypress": "13.4.1",
48 | "@nrwl/eslint-plugin-nx": "13.4.1",
49 | "@nrwl/jest": "13.4.1",
50 | "@nrwl/linter": "13.4.1",
51 | "@nrwl/tao": "13.4.1",
52 | "@nrwl/workspace": "13.4.1",
53 | "@types/jest": "27.0.2",
54 | "@types/node": "14.14.33",
55 | "@typescript-eslint/eslint-plugin": "~5.3.0",
56 | "@typescript-eslint/parser": "~5.3.0",
57 | "cypress": "^9.1.0",
58 | "eslint": "8.2.0",
59 | "eslint-config-prettier": "8.1.0",
60 | "eslint-plugin-cypress": "^2.10.3",
61 | "jasmine-marbles": "~0.9.1",
62 | "jest": "27.2.3",
63 | "jest-preset-angular": "11.0.0",
64 | "prettier": "^2.3.1",
65 | "ts-jest": "27.0.5",
66 | "typescript": "~4.4.3"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tools/generators/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Affiction/angular-feature-sliced/7c10b81129584e75da69d83fea0499f00ed7fb8e/tools/generators/.gitkeep
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"],
9 | "importHelpers": false
10 | },
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/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 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "lib": ["es2017", "dom"],
14 | "skipLibCheck": true,
15 | "skipDefaultLibCheck": true,
16 | "baseUrl": ".",
17 | "paths": {}
18 | },
19 | "exclude": ["node_modules", "tmp"]
20 | }
21 |
--------------------------------------------------------------------------------