├── .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 | ![GitHub top language](https://img.shields.io/github/languages/top/select-name/sharead-frontend) 4 | [![FeatureSliced](https://img.shields.io/badge/Powered%20by-%F0%9F%8D%B0%20Feature%20Sliced-%235c9cb5)](https://feature-sliced.design/) 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/780a3c97-cded-4063-9f74-6e325d98e97f/deploy-status)](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 | react 19 | typescript 20 | typescript 21 | typescript 22 | eslint 23 | prettier 24 | stylelint 25 | feature-sliced 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 | 8 | 9 | 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 | 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 | --------------------------------------------------------------------------------