├── .browserslistrc ├── .circleci └── config.yml ├── .coveralls.yml ├── .editorconfig ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── ngx-feature-toggle │ ├── .eslintrc.json │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── ngx-feature-toggle-provider.component.spec.ts │ │ ├── ngx-feature-toggle-provider.component.ts │ │ ├── ngx-feature-toggle-route-guard.router.spec.ts │ │ ├── ngx-feature-toggle-route-guard.router.ts │ │ ├── ngx-feature-toggle.directive.spec.ts │ │ ├── ngx-feature-toggle.directive.ts │ │ └── ngx-feature-toggle.module.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── scripts └── build.js ├── server.ts ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.server.module.ts │ ├── customer │ │ ├── customer-detail.component.ts │ │ ├── customer.component.css │ │ └── customer.component.ts │ ├── error │ │ ├── error.component.html │ │ ├── error.component.scss │ │ ├── error.component.ts │ │ └── error.module.ts │ ├── hello.component.html │ ├── hello.component.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.ts │ │ └── home.module.ts │ └── restrict-page-due-feature-toggle │ │ ├── restrict-page-due-feature-toggle.component.html │ │ ├── restrict-page-due-feature-toggle.component.scss │ │ ├── restrict-page-due-feature-toggle.component.ts │ │ └── restrict-page-due-feature-toggle.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.server.ts ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.server.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | docker_defaults: &docker_defaults 4 | docker: 5 | - image: cimg/node:14.20.0-browsers 6 | environment: 7 | NG_CLI_ANALYTICS: false 8 | 9 | working_directory: ~/project/repo 10 | 11 | attach_workspace: &attach_workspace 12 | attach_workspace: 13 | at: ~/project 14 | 15 | install_steps: &install_steps 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | name: Restore node_modules cache 20 | keys: 21 | - dependency-cache-{{ .Branch }}-{{ checksum "package.json" }} 22 | - dependency-cache-{{ .Branch }}- 23 | - dependency-cache- 24 | - run: 25 | name: Installing Dependencies 26 | command: | 27 | npm install --silent 28 | - save_cache: 29 | name: Save node_modules cache 30 | key: dependency-cache-{{ .Branch }}-{{ checksum "package.json" }} 31 | paths: 32 | - node_modules/ 33 | - persist_to_workspace: 34 | root: ~/project 35 | paths: 36 | - repo 37 | 38 | workflows: 39 | version: 2 40 | build_pipeline: 41 | jobs: 42 | - build 43 | - unit_test: 44 | requires: 45 | - build 46 | - bundle_size: 47 | requires: 48 | - build 49 | - build_ssr: 50 | requires: 51 | - build 52 | 53 | orbs: 54 | browser-tools: circleci/browser-tools@1.4.0 55 | jobs: 56 | build: 57 | <<: *docker_defaults 58 | <<: *install_steps 59 | unit_test: 60 | <<: *docker_defaults 61 | steps: 62 | - *attach_workspace 63 | - browser-tools/install-chrome 64 | - run: 65 | name: Running unit tests 66 | command: | 67 | npm run lint:types 68 | npm run lint 69 | npm test 70 | environment: 71 | CHROME_BIN: /usr/bin/google-chrome 72 | # To be added back in later 73 | # sudo npm run coveralls 74 | build_ssr: 75 | <<: *docker_defaults 76 | steps: 77 | - *attach_workspace 78 | - run: 79 | name: Building for Angular Universal 80 | command: | 81 | npm rebuild 82 | npm run build:ssr 83 | bundle_size: 84 | <<: *docker_defaults 85 | steps: 86 | - *attach_workspace 87 | - run: 88 | name: Checking bundle size 89 | command: | 90 | npm rebuild 91 | npm run build:pkg 92 | npm run bundlesize 93 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: GN9LPZUNQ2nT1CLPPHivvG1KisnoeSP5m 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.{js,ts}] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "prefix": "ngx", 27 | "style": "kebab-case", 28 | "type": "element" 29 | } 30 | ], 31 | "@angular-eslint/directive-selector": [ 32 | "error", 33 | { 34 | "prefix": "ngx", 35 | "style": "camelCase", 36 | "type": "attribute" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | 3 | # Each line is a file pattern followed by one or more owners. 4 | 5 | # These owners will be the default owners for everything in 6 | 7 | # the repo. Unless a later match takes precedence, 8 | 9 | # @global-owner1 and @global-owner2 will be requested for 10 | 11 | # review when someone opens a pull request. 12 | 13 | * @willmendesneto 14 | 15 | # Order is important; the last matching pattern takes the most 16 | 17 | # precedence. When someone opens a pull request that only 18 | 19 | # modifies JS files, only @js-owner and not the global 20 | 21 | # owner(s) will be requested for a review. 22 | 23 | # *.js @js-owner #This is an inline comment. 24 | 25 | # You can also use email addresses if you prefer. They'll be 26 | 27 | # used to look up users just like we do for commit author 28 | 29 | # emails. 30 | 31 | # *.go docs@example.com 32 | 33 | # Teams can be specified as code owners as well. Teams should 34 | 35 | # be identified in the format @org/team-name. Teams must have 36 | 37 | # explicit write access to the repository. In this example, 38 | 39 | # the octocats team in the octo-org organization owns all .txt files. 40 | 41 | # *.txt @octo-org/octocats 42 | 43 | # In this example, @doctocat owns any files in the build/logs 44 | 45 | # directory at the root of the repository and any of its 46 | 47 | # subdirectories. 48 | 49 | # /build/logs/ @doctocat 50 | 51 | # The `docs/*` pattern will match files like 52 | 53 | # `docs/getting-started.md` but not further nested files like 54 | 55 | # `docs/build-app/troubleshooting.md`. 56 | 57 | # docs/\* docs@example.com 58 | 59 | # In this example, @octocat owns any file in an apps directory 60 | 61 | # anywhere in your repository. 62 | 63 | # apps/ @octocat 64 | 65 | # In this example, @doctocat owns any file in the `/docs` 66 | 67 | # directory in the root of your repository and any of its 68 | 69 | # subdirectories. 70 | 71 | # /docs/ @doctocat 72 | 73 | # In this example, any change inside the `/scripts` directory 74 | 75 | # will require approval from @doctocat or @octocat. 76 | 77 | # /scripts/ @doctocat @octocat 78 | 79 | # In this example, @octocat owns any file in the `/apps` 80 | 81 | # directory in the root of your repository except for the `/apps/github` 82 | 83 | # subdirectory, as its owners are left empty. 84 | 85 | # /apps/ @octocat 86 | # /apps/github 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** (check one with "x") 2 | 3 | - [ ] bug report => search github for a similar issue or PR before submitting 4 | - [ ] feature request 5 | 6 | **Current behavior** 7 | 8 | 9 | 10 | **Expected behavior** 11 | 12 | 13 | 14 | **Reproduction of the problem** 15 | 16 | 19 | 20 | **What is the motivation / use case for changing the behavior?** 21 | 22 | 23 | 24 | **Please tell us about your environment:** 25 | 26 | 27 | 28 | - **Browser:** [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | 29 | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] 30 | 31 | 32 | 33 | - **Language:** [all | TypeScript X.X | ES6/7 | ES5] 34 | 35 | - **Node (if applicable):** `node --version` = 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Please check if the PR fulfills these requirements** 2 | 3 | - [ ] The commit messages follow these 4 | [guidelines](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y) 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | **What kind of change does this PR introduce?** _(check one with "x")_ 9 | 10 | - [ ] Bugfix 11 | - [ ] Feature 12 | - [ ] Code style update (formatting, local variables) 13 | - [ ] Refactoring (no functional changes, no api changes) 14 | - [ ] Build related changes 15 | - [ ] CI related changes 16 | - [ ] Other. Please describe: 17 | 18 | If it is a Bugfix, please describe the root cause and what could have been done to prevent it… 19 | 20 | **What is the current behavior?** _(You can link to an open issue here, add screenshots…)_ 21 | 22 | **What is the new behavior?** 23 | 24 | **Does this PR introduce a breaking change?** _(check one with "x")_ 25 | 26 | - [ ] Yes 27 | - [ ] No 28 | 29 | If this PR contains a breaking change, please describe the impact and migration 30 | path for existing applications: … 31 | 32 | **Other information (if applicable)**: 33 | 34 | --- 35 | 36 | _Please @mention @people to review this PR…_ 37 | -------------------------------------------------------------------------------- /.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 | **/coverage 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events.json 16 | speed-measure-plugin.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | .angular 37 | /.angular/cache 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 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 | **/coverage 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | 14 | # profiling files 15 | chrome-profiler-events.json 16 | speed-measure-plugin.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | .angular 37 | /.angular/cache 38 | /.sass-cache 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.20.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/CODEOWNERS 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "printWidth": 120, 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "trailingComma": "all", 10 | "useTabs": false, 11 | "overrides": [ 12 | { 13 | "files": "*.scss", 14 | "options": { 15 | "printWidth": 80 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased][] 9 | 10 | ### Fixed 11 | 12 | - Users now can redirect when Path is an empty string. When you configure home page to have path = '', and use NgxFeatureToggleRouteGuard with redirectTo = '', redirect does not happen. Thanks @brandonsmith86 🎉 13 | - Fixing `redirecTo` types 14 | 15 | ### Added 16 | 17 | - Adding typescript types validation on pipeline 18 | 19 | ## [12.0.0][] - 2023-02-20 20 | 21 | ### Updated 22 | 23 | - Upgrading library to Angular v15 24 | 25 | ## [7.0.0][] - 2023-02-04 26 | 27 | ### Added 28 | 29 | - Adding support for extending global theme added via `NgxSkeletonLoaderModule.forRoot({ theme: /* ...list of CSS atributes */} })` 30 | 31 | By default when using `NgxSkeletonLoaderModule.forRoot({ theme: /* ...list of CSS atributes */} })` the application is using this value as source of truth, overriding any local theming passed to `` component via `[theme]` input. 32 | 33 | By using `NgxSkeletonLoaderModule.forRoot({ theme: { extendsFromRoot: true, /* ...list of CSS atributes */} })` in your application, you should also be aware that: 34 | 35 | - By default, every `` component will use `theme` coming from `NgxSkeletonLoaderModule.forRoot()` as the source of truth 36 | - If there's any CSS attribute on the component locally which overrides the CSS spec, it combines both themes, but overriding global CSS attributes in favor of local ones. 37 | 38 | ```html 39 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | ``` 61 | 62 | - Adding new `custom-content` appearance. From now on, consumers can now add their own content inside `` component. So that, they can add some custom content, such as SVG, as an example 63 | - Adding examples for `custom-content` usage 64 | 65 | ### Updated 66 | 67 | - Updagrading module to Angular v15 68 | 69 | ### Fixed 70 | 71 | - Removing build warnings 72 | 73 | ## [6.0.0][] - 2022-08-18 74 | 75 | ### Updated 76 | 77 | - Adding Publishing setup using NPX 78 | - Replacing CSS class namespace from `.loader` to `.skeleton-loader` 79 | 80 | #### Breaking Change 81 | 82 | The CSS class used as namespace was changed. Previously, it was called `.loader` and now is `.skeleton-loader`. It might cause some issues in cases of `:host` DOM style scoping usage. For the sake of semantic versioning, please bear in mind this scenario in case of `:host` usage. 83 | 84 | ## [5.0.0][] - 2022-02-08 85 | 86 | ### Updated 87 | 88 | > Thanks @yharaskrik 89 | 90 | #### Breaking Change 91 | 92 | Bundle distribution are now `esm2020`, `fesm2015` and `fesm2020`. UMD and CommonJS versions were support were removed from Angular CLI directly. So the next version for the package will be a major version to cover these changes accordingly. 93 | 94 | - Updating package bundle distribution 95 | - Updating `@angular/cli` to v13 96 | - Applying project changes to v13 97 | - Updating bundlesize check to point to `fesm2020` 98 | 99 | ## [4.0.0][] - 2021-07-28 100 | 101 | ### Fixed 102 | 103 | #### Breaking Change 104 | 105 | - Rolling back "Adding mechanism to prevents calling `forRoot()` more than once if module is loaded asynchronously in a submodule.". Unfortunately, this was affecting consumers and it needed to be reverted to avoid friction in other applications. 106 | 107 | If you need to have this feature in place, the suggestion is to create a specific module in your app and apply the changes on your application. 108 | 109 | ## [3.0.0][] - 2021-07-23 110 | 111 | ### Added 112 | 113 | #### Breaking Change 114 | 115 | - Adding mechanism to prevents calling `forRoot()` more than once if module is loaded asynchronously in a submodule. This is required in order to avoid issues in consumers. To avoid that, consumers should load the module once on the main module instead - if loading submodules async. 116 | 117 | ## [2.10.1][] - 2021-07-13 118 | 119 | ### Fixed 120 | 121 | - Ensures every ARIA progressbar node has an accessible name. This is caused by missing aria-label on the `` element. 122 | 123 | Thanks @rkristelijn for raising the issue and the pull request! 124 | 125 | ## [2.10.0][] - 2021-06-15 126 | 127 | ### Added 128 | 129 | - Adding module configuration support via `forRoot()` method. Now you can add configure your module via `forRoot()`. You can now set the default of `appearance`, `animation`, `theme`, `loadingText`, `count` and/or `items`.E.G. 130 | 131 | ```ts 132 | 133 | @NgModule({ 134 | // ... 135 | imports: [NgxSkeletonLoaderModule.forRoot({ appearance: 'circle', count: 3 )], 136 | // ... 137 | }) 138 | ``` 139 | 140 | ## [2.9.2][] - 2021-04-11 141 | 142 | ### Updated 143 | 144 | - Updating link in README.md 145 | 146 | ### Fixed 147 | 148 | - Bumping dev dependencies to avoid security issues 149 | 150 | ## [2.9.1][] - 2021-02-20 151 | 152 | ### Fixed 153 | 154 | - Adding `appearance` attribute to be checked via `ngOnChanges` 155 | 156 | ### Updated 157 | 158 | - Updating examples with new features 159 | 160 | ## [2.9.0][] - 2021-02-19 161 | 162 | ### Added 163 | 164 | - Adding validation for @Input attributes that needs internal manipulation. After these changes: 165 | - if `count` is not a numeric value, it will use the default value as `1` 166 | - if `animation` is not a valid attribute, it will use the default value as `progress` 167 | - PS: The other values alredy have a fallback, so nothing to worry here 168 | - Adding error feedback for `appearance` attribute in case of wrong configuration. Now it will show a error message on the console in case of receiving a wrong value 169 | 170 | ### Updated 171 | 172 | - Adding `ngOnChange` to validate `count` input in case of changes via binding 173 | - Updating `README.md` with information about `appearance` and `theme` usage. 174 | 175 | ## [2.8.0][] - 2021-02-18 176 | 177 | ### Fixed 178 | 179 | - Using `ngAcceptInputType_count` for template checking in count input. That solves issue https://github.com/willmendesneto/ngx-feature-toggle/issues/59. You can find more details about it in https://angular.io/guide/template-typecheck 180 | - Fixing type issues on `yarn build:ssr` command 181 | 182 | ### Updated 183 | 184 | - Updating `perf-marks` to `v1.14.1` 185 | - Adding strict mode support in module 186 | - Updating types for `theme` to match with `ngStyle`. More details in https://angular.io/api/common/NgStyle#properties 187 | 188 | ## [2.7.0][] - 2021-02-06 189 | 190 | ### Added 191 | 192 | - Adding new `loadingText` attribute to be used as WAI-ARIA `aria-valuetext`. In this case, it will render the component using "Please wait ...". Otherwise, it defaults to "Loading..." 193 | 194 | ```html 195 | 196 | 197 | 198 |
199 | 200 |
201 | ``` 202 | 203 | ### Updated 204 | 205 | - Using OnPush as changeDetection mechanism into ngx-feature-toggle component 206 | - Adding ability to pass `false` as string or boolean (coming from variable value via binding) on `animation` attribute in `ngx-feature-toggle` component configuration. animation will receive `false` as string when attribute field it's not using binding. Component now can receive `false` (boolean), "false" (string), or any other animation type via binding. 207 | 208 | ```html 209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 |
221 | ``` 222 | 223 | ## [2.6.2][] - 2020-12-08 224 | 225 | ### Fixed 226 | 227 | - Removing Lighthouse "Avoid non-composited animations" issue. Lighthouse shows warnings from ngx-feature-toggle.scss -file (progress). 228 | 229 | - "Avoid non-composited animations": 230 | - "Animations which are not composited can be janky and contribute to CLS" 231 | 232 | To solve that, instead of using CSS `background-position` the module is now using CSS `translate3d`, which improves the animation by using GPU instead of CPU. Issue fixed and performance boost added 🎉 233 | 234 | ## [2.6.1][] - 2020-11-30 235 | 236 | ### Fixed 237 | 238 | - Solving `forRoot()` types error `Generic type 'ModuleWithProviders' requires 1 type argument(s)`. Closes https://github.com/willmendesneto/ngx-feature-toggle/issues/51 239 | 240 | ## [2.6.0][] - 2020-11-15 241 | 242 | ### Added 243 | 244 | - Adding `NgxSkeletonLoaderModule.forRoot()` method. Usage: 245 | 246 | ```js 247 | import { NgModule } from '@angular/core'; 248 | import { NgxSkeletonLoaderModule } from 'ngx-feature-toggle'; 249 | // ... list of other app dependencies 250 | 251 | import { AppComponent } from './app.component'; 252 | // ... list of other app components/modules 253 | 254 | @NgModule({ 255 | declarations: [AppComponent], 256 | imports: [NgxSkeletonLoaderModule.forRoot()], 257 | providers: [], 258 | bootstrap: [AppComponent], 259 | }) 260 | export class AppModule {} 261 | ``` 262 | 263 | ## [2.5.0][] - 2020-10-10 264 | 265 | ### Fixed 266 | 267 | - Fixing bundle size command on CircleCI pipeline 268 | 269 | ### Updated 270 | 271 | - Upgrading NodeJS to v14.11.0 272 | - Updating `perf-marks` package to v1.14.0 273 | - Improving skeleton animations fps by using `cubic-bezier` instead of `ease-in-out` 274 | 275 | ## [2.4.4][] - 2020-08-21 276 | 277 | ### Fixed 278 | 279 | - Remove check requirements if perf-marks is running in a browser or not in Angular apps 🔥 280 | 281 | ### Added 282 | 283 | - Adding Angular Universal support for examples. Now we can run `npm run dev:ssr` and access `http://localhost:4200/index.html` and the page will run using angular universal 💪 284 | - Adding `.prettierrc` file with some of the code styling rules 285 | 286 | ## [2.4.3][] - 2020-08-13 287 | 288 | ### Fixed 289 | 290 | - Avoiding perf-marks call if running in Angular Universal applications 291 | 292 | ## [2.4.2][] - 2020-08-01 293 | 294 | ### Updated 295 | 296 | - Bumping `perf-marks` to latest version 297 | 298 | ## [2.4.1][] - 2020-08-01 299 | 300 | ### Updated 301 | 302 | - Bumping `perf-marks` to latest version 303 | 304 | ## [2.4.0][] - 2020-08-01 305 | 306 | ### Added 307 | 308 | - Adding User Timing API to track component render and content loader time 309 | 310 | ### Updated 311 | 312 | - Updating examples with new skeleton simulation 313 | - Adding Stackblitz link for user card skeleton loading demo 314 | 315 | ## [2.3.0][] - 2020-08-01 316 | 317 | ### Added 318 | 319 | - Adding User Timing API to track component render and content loader time 320 | 321 | ### Updated 322 | 323 | - Updating examples with new skeleton simulation 324 | - Adding Stackblitz link for user card skeleton loading demo 325 | 326 | ## [2.2.1][] - 2020-06-30 327 | 328 | ### Fixed 329 | 330 | - For compatibility with IE11 by using indexOf instead of `includes` 331 | 332 | ### Updated 333 | 334 | - Updating `npm run postinstall` command to follow the new rules from update.angular.io website 335 | 336 | ## [2.2.0][] - 2020-06-01 337 | 338 | ### Added 339 | 340 | - Using `prefers-reduced-motion` to respect user’s OS option to `Reduce Motion`. More details about `prefers-reduced-motion` in https://web.dev/prefers-reduced-motion/ 341 | 342 | ## [2.1.0][] - 2020-06-01 343 | 344 | ### Updated 345 | 346 | - Upgrading @angular/cli to version 9 347 | - 🎉 Decreasing bundle size to 1.17KB 🎉 348 | 349 | ## [2.0.0][] - 2020-05-15 350 | 351 | ### Updated 352 | 353 | - Upgrading NodeJS to v12.16.2 354 | - Updating documentation with `animation` attribute 355 | 356 | ### Added 357 | 358 | - Supporting for new animation `progress-dark` to enable users when using theme with darker color schema 359 | - Supporting for different animations 🎉 360 | 361 | Now we can define the animation we want to use in `` component via `animation` input. It's a string that can defined the animation used during the loading, having as options: 362 | 363 | - `false`: it will disable the animation; 364 | - `progress` - _default_: it will use it `progress` as animation; 365 | - `pulse`: it will use `pulse` as animation; 366 | 367 | > `progress` is the default animation, used as the single one previously. If you don't pass the animation attribute, it defaults to `progress`. 368 | 369 | ```html 370 |
371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 |
379 | ``` 380 | 381 | - Supporting enabling/disabling animations. 382 | Now the users will be able to enable/disable animations by using `animation` input. It's a string with `false` as value that the component receives to check if it should not load progress animation. 383 | 384 | > It works only to disable it. In case you want to keep enable it 385 | 386 | ```js 387 |
388 | 389 |
390 | ``` 391 | 392 | ## [1.2.7][] - 2020-04-13 393 | 394 | ### Updated 395 | 396 | - Decreasing bundle size after disable Ivy in production build 397 | - Adding description, keywords and github information on `package.json` files 398 | 399 | ## [1.2.6][] - 2020-02-26 400 | 401 | ### Fixed 402 | 403 | - Changing angular library configuration to prod and forcing it at publish time 404 | 405 | ## [1.2.5][] - 2020-02-25 406 | 407 | ### Fixed 408 | 409 | - Changing angular library configuration to prod 410 | 411 | ## [1.2.4][] - 2020-02-25 412 | 413 | ### Updated 414 | 415 | - Updating Github templates 416 | - Updating Angular CLI to v9 417 | 418 | ## [1.2.3][] - 2020-02-25 419 | 420 | ### Fixed 421 | 422 | - Solving peerDependency warning when installing library in an Angular 9 project 423 | 424 | ## [1.2.2][] - 2019-06-22 425 | 426 | ### Fixed 427 | 428 | - Fixing component dimensions via theme 429 | 430 | ## [1.2.1][] - 2019-06-08 431 | 432 | ### Updated 433 | 434 | - Updating Angular CLI to v8 435 | 436 | ## [1.2.0][] - 2019-04-19 437 | 438 | ### Updated 439 | 440 | - Updating Angular CLI to 7.3.8 441 | 442 | ## [1.1.2][] - 2019-01-07 443 | 444 | ### Added 445 | 446 | - Adding badges for stackblitz, bundlephobia and license 447 | 448 | ### Updated 449 | 450 | - Removing unnecessary CSS styles for skeleton 451 | 452 | ## [1.1.1][] - 2018-12-17 453 | 454 | ### Fixed 455 | 456 | - Fixing Stackblitz link demo link 457 | 458 | ## [1.1.0][] - 2018-12-17 459 | 460 | ### Added 461 | 462 | - Added GitHub urls into `package.json` 463 | - Added Circle CI integration 464 | - Added Coveralls integration 465 | - Added GitHub templates 466 | - Added `CODE_OF_CONDUCT.md` with the Code of conduct 467 | - Added unit tests for skeletons and demo components 468 | 469 | ### Updated 470 | 471 | - Decreased bundle size 472 | - New gif showing `ngx-feature-toggle` in action 473 | 474 | ## [1.0.2][] - 2018-12-16 475 | 476 | ### Fixed 477 | 478 | - Added markdown files in dist folder in build time 479 | 480 | ## [1.0.1][] - 2018-12-16 481 | 482 | ### Fixed 483 | 484 | - Added markdown files in dist folder in build time 485 | 486 | ## [1.0.0][] - 2018-12-16 487 | 488 | ### Fixed 489 | 490 | - Fixed build script 491 | 492 | ## [0.0.1][] - 2018-12-16 493 | 494 | ### Added 495 | 496 | - Created `ngx-feature-toggle` 497 | - Created test automation for the module 498 | 499 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v0.0.1...HEAD 500 | [0.0.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v0.0.1 501 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.0.0...HEAD 502 | [1.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.0.0 503 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.0.2...HEAD 504 | [1.0.2]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.0.1...v1.0.2 505 | [1.0.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.0.1 506 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.0.2...HEAD 507 | [1.0.2]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.0.2 508 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.1.0...HEAD 509 | [1.1.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.1.0 510 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.1.1...HEAD 511 | [1.1.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.1.1 512 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.4...HEAD 513 | [1.2.4]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.3...v1.2.4 514 | [1.2.3]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.2...v1.2.3 515 | [1.2.2]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.1...v1.2.2 516 | [1.2.1]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.0...v1.2.1 517 | [1.2.0]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.1.2...v1.2.0 518 | [1.1.2]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.1.2 519 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.5...HEAD 520 | [1.2.5]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.2.5 521 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.6...HEAD 522 | [1.2.6]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.2.6 523 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v1.2.7...HEAD 524 | [1.2.7]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v1.2.7 525 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.0.0...HEAD 526 | [2.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.0.0 527 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.1.0...HEAD 528 | [2.1.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.1.0 529 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.2.0...HEAD 530 | [2.2.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.2.0 531 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.2.1...HEAD 532 | [2.2.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.2.1 533 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.3.0...HEAD 534 | [2.3.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.3.0 535 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.4.0...HEAD 536 | [2.4.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.4.0 537 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.4.1...HEAD 538 | [2.4.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.4.1 539 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.4.2...HEAD 540 | [2.4.2]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.4.2 541 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.4.3...HEAD 542 | [2.4.3]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.4.3 543 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.4.4...HEAD 544 | [2.4.4]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.4.4 545 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.5.0...HEAD 546 | [2.5.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.5.0 547 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.6.0...HEAD 548 | [2.6.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.6.0 549 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.6.1...HEAD 550 | [2.6.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.6.1 551 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.6.2...HEAD 552 | [2.6.2]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.6.2 553 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.7.0...HEAD 554 | [2.7.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.7.0 555 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.8.0...HEAD 556 | [2.8.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.8.0 557 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.9.0...HEAD 558 | [2.9.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.9.0 559 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.9.1...HEAD 560 | [2.9.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.9.1 561 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.9.2...HEAD 562 | [2.9.2]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.9.2 563 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.10.0...HEAD 564 | [2.10.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.10.0 565 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v2.10.1...HEAD 566 | [2.10.1]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v2.10.1 567 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v4.0.0...HEAD 568 | [4.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v3.0.0...v4.0.0 569 | [3.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v3.0.0 570 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v5.0.0...HEAD 571 | [5.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v5.0.0 572 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v6.0.0...HEAD 573 | [6.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v6.0.0 574 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v7.0.0...HEAD 575 | [7.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v7.0.0 576 | [unreleased]: https://github.com/willmendesneto/ngx-feature-toggle/compare/v12.0.0...HEAD 577 | [12.0.0]: https://github.com/willmendesneto/ngx-feature-toggle/tree/v12.0.0 578 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to start 2 | 3 | **Note** requires node v6.x.x or higher and npm 3.x.x. 4 | 5 | ```bash 6 | git clone https://github.com/willmendesneto/ngx-feature-toggle.git 7 | cd ngx-feature-toggle 8 | npm install 9 | npm start 10 | ``` 11 | 12 | # Running test 13 | 14 | ```bash 15 | npm test 16 | ``` 17 | 18 | ## Submitting Pull Requests 19 | 20 | **Please follow these basic steps to simplify pull request reviews - if you don't you'll probably just be asked to anyway.** 21 | 22 | - Please rebase your branch against the current master 23 | - Run `npm install` to make sure your development dependencies are up-to-date 24 | - Please ensure that the test suite passes **and** that code is lint free before submitting a PR by running: 25 | - `npm test` 26 | - If you've added new functionality, **please** include tests which validate its behaviour 27 | - Make reference to possible [issues](https://github.com/willmendesneto/ngx-feature-toggle/issues) on PR comment 28 | - This module follows Angular commit message standard, so please make sure that you are following this standard. 29 | 30 | ## Submitting bug reports 31 | 32 | - Please detail the affected browser(s) and operating system(s) 33 | - Please be sure to state which version of node **and** npm you're using 34 | - Please use try to simulate your bug based on [the demo on Stackblitz](https://ngx-feature-toggle-sample.stackblitz.io)! 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Wilson Mendes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGX Feature Toggle 2 | 3 | [![Dependency Status](https://david-dm.org/willmendesneto/ngx-feature-toggle.svg)](https://david-dm.org/willmendesneto/ngx-feature-toggle) 4 | [![npm](https://img.shields.io/badge/stackblitz-online-orange.svg)](https://stackblitz.com/edit/ngx-feature-toggle-sample) 5 | 6 | [![NPM](https://nodei.co/npm/ngx-feature-toggle.png?downloads=true&downloadRank=true&stars=true)](https://npmjs.org/ngx-feature-toggle) 7 | [![NPM](https://nodei.co/npm-dl/ngx-feature-toggle.png?height=3&months=3)](https://npmjs.org/ngx-feature-toggle) 8 | 9 | [![Build Status](https://circleci.com/gh/willmendesneto/ngx-feature-toggle.svg?style=shield)](https://circleci.com/gh/willmendesneto/ngx-feature-toggle) 10 | [![Coverage Status](https://coveralls.io/repos/willmendesneto/ngx-feature-toggle/badge.svg?branch=master)](https://coveralls.io/r/willmendesneto/ngx-feature-toggle?branch=master) 11 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/ngx-feature-toggle.svg)](https://bundlephobia.com/result?p=ngx-feature-toggle) 12 | [![npm](https://img.shields.io/npm/l/express.svg?maxAge=2592000)](/LICENSE) 13 | 14 | Your module to handle with [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html) in Angular applications easier. 15 | 16 | ## Why Feature toggle? 17 | 18 | > This is a common concept, but why use this directive instead solve it via server-side rendering? 19 | 20 | The idea of this directive is make this process transparent and easier. So the main point is integrate this directive with other tooling process, such as: 21 | 22 | - Server-side rendering; 23 | - Progressive rendering; 24 | - Any other that you like :) 25 | 26 | You can integrate with WebSockets or handling this in a EventSourcing architecture. It's totally transparent for you and you can integrate easier in your application. 27 | 28 | - [Demo](#demo) 29 | - [Install](#install) 30 | - [Setup](#setup) 31 | - [Development](#development) 32 | - [Contribute](#contribute) 33 | 34 | ## Demo 35 | 36 | Try out the demos on Stackblitz: 37 | 38 | - [Components and directives example](https://stackblitz.com/edit/ngx-feature-toggle-sample) 39 | - [Routing Guards example](https://stackblitz.com/edit/ngx-feature-toggle-routing-guard-sample) 40 | 41 | ## Install 42 | 43 | You can get it on NPM installing `ngx-feature-toggle` module as a project dependency. 44 | 45 | ```shell 46 | npm install ngx-feature-toggle --save 47 | ``` 48 | 49 | ## Setup 50 | 51 | You'll need to add `FeatureToggleModule` to your application module. So that, the `featureToggle` components will be accessible in your application. 52 | 53 | ```typescript 54 | ... 55 | import { FeatureToggleModule } from 'ngx-feature-toggle'; 56 | ... 57 | @NgModule({ 58 | declarations: [ 59 | YourAppComponent 60 | ], 61 | imports: [ 62 | ... 63 | FeatureToggleModule, 64 | ... 65 | ], 66 | providers: [], 67 | bootstrap: [YourAppComponent] 68 | }) 69 | 70 | export class YourAppComponent {} 71 | 72 | ``` 73 | 74 | Now you just need to add a configuration in your application root component. Your feature toggle configuration can be added using different approaches, such as: 75 | 76 | - RXJS subscribe information; 77 | - HTTP Request; 78 | - CQRS event data; 79 | - File information; 80 | - etc; 81 | 82 | After that, you can use the `featureToggle` components and directives in your templates, passing the string based on the feature toggle configuration data. 83 | 84 | ## Module 85 | 86 | ### Components and Directives 87 | 88 | - `feature-toggle-provider`: Handle with feature toggle configuration in your application. It adds the default values of your enabled/disabled features; 89 | - `*featureToggle`: Directive that handles with feature toggle check. So that, the component will be rendered/removed based on the feature toggle configuration is enabled; 90 | - `*featureToggleWhenDisabled`: Directive that handles with feature toggle check. So that, the component will be rendered/removed when the feature toggle configuration is disabled; 91 | 92 | ```typescript 93 | import { Component } from '@angular/core'; 94 | 95 | @Component({ 96 | selector: 'component-docs', 97 | template: ` 98 | 99 |
100 |

condition is true and "featureToggle" is enabled.

101 |
102 |
103 |

condition is false and "featureToggle" is disabled. In that case this content should not be rendered.

104 |
105 |
106 |

107 | condition is false and "featureToggle" is disabled 108 | but it has "!" as a prefix of the feature toggle to be checked. 109 | In that case this content should be rendered. 110 |

111 |
112 |
116 |

117 | This is a combined condition. It shows if enableSecondText is true and enableFirstText is falsy, 118 | but it has "!" as a prefix. If both cases are correct, then the "featureToggle" is enabled and rendering this 119 | component. 120 |

121 |
122 |
123 | `, 124 | }) 125 | export class ComponentDocsComponent { 126 | public featureToggleData: any = { 127 | enableFirstText: false, 128 | enableSecondText: true, 129 | }; 130 | } 131 | ``` 132 | 133 | ### Route Guards 134 | 135 | In some scenarios when you need to prevent the route to be loaded, you can use `NgxFeatureToggleCanLoadGuard`, by passing the class and configuration of the feature toggle to be checked in your route data. 136 | 137 | ```js 138 | ... 139 | export const routes: Routes = [ 140 | 141 | { 142 | path: 'home', 143 | component: HomeComponent, 144 | canActivate: [NgxFeatureToggleCanLoadGuard], 145 | data: { 146 | // Using array as configuration 147 | featureToggle: [ 148 | // This configuration will check if feature toggle is enabled 149 | 'enableSecondText', 150 | // This configuration will check if feature toggle is disabled 151 | // since it has `!` prefix in the configuration 152 | '!enableFirstText' 153 | ], 154 | }, 155 | }, 156 | { 157 | path: 'dashboard', 158 | component: DashboardComponent, 159 | canActivate: [NgxFeatureToggleCanLoadGuard], 160 | data: { 161 | // Using string as configuration 162 | featureToggle: 'enableSecondText', 163 | }, 164 | }, 165 | ]; 166 | ... 167 | ``` 168 | 169 | Also, you can use `NgxFeatureToggleRouteGuard` to check if the route should be activated or not by passing the class and configuration of the feature toggle to be checked in your route data. 170 | 171 | ```js 172 | ... 173 | export const routes: Routes = [ 174 | 175 | { 176 | path: 'home', 177 | component: HomeComponent, 178 | canActivate: [NgxFeatureToggleRouteGuard], 179 | data: { 180 | // Using array as configuration 181 | featureToggle: [ 182 | // This configuration will check if feature toggle is enabled 183 | 'enableSecondText', 184 | // This configuration will check if feature toggle is disabled 185 | // since it has `!` prefix in the configuration 186 | '!enableFirstText' 187 | ], 188 | }, 189 | }, 190 | { 191 | path: 'dashboard', 192 | component: DashboardComponent, 193 | canActivate: [NgxFeatureToggleRouteGuard], 194 | data: { 195 | // Using string as configuration 196 | featureToggle: 'enableSecondText', 197 | }, 198 | }, 199 | ]; 200 | ... 201 | ``` 202 | 203 | In both route guards you can pass route data with feature toggle as an array. For scenarios when you need to check for feature toggles enabled and/or disabled you can easily configure it by passing `!` if the application should check if the feature toggle is disabled 204 | 205 | ```js 206 | ... 207 | export const routes: Routes = [ 208 | { 209 | path: 'home', 210 | component: HomeComponent, 211 | canActivate: [NgxFeatureToggleRouteGuard], 212 | data: { 213 | // Using array as configuration 214 | featureToggle: [ 215 | // This configuration will check if feature toggle is enabled 216 | 'enableSecondText', 217 | // This configuration will check if feature toggle is disabled 218 | // since it has `!` prefix in the configuration 219 | '!enableFirstText' 220 | ], 221 | }, 222 | }, 223 | { 224 | path: 'dashboard', 225 | component: DashboardComponent, 226 | canActivate: [NgxFeatureToggleRouteGuard], 227 | data: { 228 | // Using string as configuration 229 | featureToggle: 'enableSecondText', 230 | }, 231 | }, 232 | ]; 233 | ... 234 | ``` 235 | 236 | In this case, we are combining the checks. So the component will be activated if `enableSecondText` is configured as `true` AND `enableFirstText` is configured as `false`. With that configuration you can have all the flexibility to cover different scenarios in your app. 237 | 238 | Use `NgxFeatureToggleRouteGuard` to control when the child component of a specific component can be activate via routing. It can be passed as an array of items. 239 | 240 | ```js 241 | ... 242 | export const routes: Routes = [ 243 | { 244 | path: 'customer', 245 | component: CustomerComponent, 246 | canActivateChild: [NgxFeatureToggleRouteGuard], 247 | children: [ 248 | { 249 | path: ':id', 250 | component: CustomerDetailComponent, 251 | // This is the featureToggle configuration for 252 | // the child component. It can also use 253 | // a combination of feature toggles 254 | data: { 255 | featureToggle: [ 256 | // This configuration will check if feature toggle is enabled 257 | 'enableCustomerPage', 258 | // This configuration will check if feature toggle is disabled 259 | // since it has `!` prefix in the configuration 260 | '!enableChildrenNavigation'], 261 | }, 262 | }, 263 | ], 264 | }, 265 | { 266 | path: 'dashboard', 267 | component: DashboardComponent, 268 | canActivateChild: [NgxFeatureToggleRouteGuard], 269 | children: [ 270 | { 271 | path: ':id', 272 | component: DashboardDetailsComponent, 273 | // This is the featureToggle configuration for 274 | // the child component. It can also use 275 | // a combination of feature toggles 276 | data: { 277 | // using string to configure 278 | featureToggle: 'enableDashboardDetailsPage', 279 | }, 280 | }, 281 | ], 282 | }, 283 | ]; 284 | ... 285 | ``` 286 | 287 | #### Redirects 288 | 289 | You might have some specific requirements that you should redirect a user to a specific route in case of a feature flag is disabled. For that, you can use `redirectTo` as a mechanism to redirect a user in a specific route when it tries to access in a route with a CanActivate/CanActivateChild/CanLoad Feature Toggle Guard and the feature toggle is disabled. 290 | 291 | For advanced scenarios you can use a combination of route guards AND redirects. E.G. 292 | 293 | ```js 294 | ... 295 | export const routes: Routes = [ 296 | { 297 | path: 'customer', 298 | component: CustomerComponent, 299 | canLoad: [NgxFeatureToggleRouteGuard], 300 | canActivate: [NgxFeatureToggleRouteGuard], 301 | canActivateChild: [NgxFeatureToggleRouteGuard], 302 | // This is the featureToggle configuration for 303 | // the parent component 304 | data: { 305 | featureToggle: ['enableCustomerPage'], 306 | // If feature toggle is disabled, the user will be redirected to `/error` URL 307 | redirectTo: '/error' 308 | }, 309 | children: [ 310 | { 311 | path: ':id', 312 | component: CustomerDetailComponent, 313 | // This is the featureToggle configuration for 314 | // the child component. It can also use 315 | // a combination of feature toggles 316 | data: { 317 | featureToggle: ['enableCustomerPage', '!enableChildrenNavigation'], 318 | // If one (or all of them) of the feature toggle is disabled, the user will be redirected to `/customer-error` URL 319 | // Note that you can use redirects for the main url and their children 320 | redirectTo: '/customer-error' 321 | }, 322 | }, 323 | ], 324 | }, 325 | ]; 326 | ... 327 | ``` 328 | 329 | ## Development 330 | 331 | ### Run demo locally 332 | 333 | 1. This project uses [Angular CLI](https://cli.angular.io/) as base. That means you just need to run `npm start` and access the link `http://localhost:4200` in your browser 334 | 335 | ### Run tests 336 | 337 | 1. Run `npm test` for run tests. In case you want to test using watch, please use `npm run tdd` 338 | 339 | ### Publish 340 | 341 | this project is using `np` package to publish, which makes things straightforward. EX: `np --contents=dist/ngx-feature-toggle` 342 | 343 | > For more details, [please check np package on npmjs.com](https://www.npmjs.com/package/np) 344 | 345 | ## Contribute 346 | 347 | For any type of contribution, please follow the instructions in [CONTRIBUTING.md](https://github.com/willmendesneto/ngx-feature-toggle/blob/master/CONTRIBUTING.md) and read [CODE_OF_CONDUCT.md](https://github.com/willmendesneto/ngx-feature-toggle/blob/master/CODE_OF_CONDUCT.md) files. 348 | 349 | ## Author 350 | 351 | **Wilson Mendes (willmendesneto)** 352 | 353 | - 354 | - 355 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-feature-toggle-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ngx-feature-toggle-demo/browser", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": false, 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": ["src/styles.scss"], 28 | "scripts": [], 29 | "vendorChunk": true, 30 | "extractLicenses": false, 31 | "buildOptimizer": false, 32 | "sourceMap": true, 33 | "optimization": false, 34 | "namedChunks": true, 35 | "allowedCommonJsDependencies": ["feature-toggle-service"] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "aot": true, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | } 59 | ] 60 | } 61 | }, 62 | "defaultConfiguration": "" 63 | }, 64 | "serve": { 65 | "builder": "@angular-devkit/build-angular:dev-server", 66 | "options": { 67 | "browserTarget": "ngx-feature-toggle-demo:build" 68 | }, 69 | "configurations": { 70 | "production": { 71 | "browserTarget": "ngx-feature-toggle-demo:build:production" 72 | } 73 | } 74 | }, 75 | "extract-i18n": { 76 | "builder": "@angular-devkit/build-angular:extract-i18n", 77 | "options": { 78 | "browserTarget": "ngx-feature-toggle-demo:build" 79 | } 80 | }, 81 | "test": { 82 | "builder": "@angular-devkit/build-angular:karma", 83 | "options": { 84 | "main": "src/test.ts", 85 | "polyfills": "src/polyfills.ts", 86 | "tsConfig": "tsconfig.spec.json", 87 | "karmaConfig": "karma.conf.js", 88 | "assets": ["src/favicon.ico", "src/assets"], 89 | "styles": ["src/styles.scss"], 90 | "scripts": [] 91 | } 92 | }, 93 | "e2e": { 94 | "builder": "@angular-devkit/build-angular:protractor", 95 | "options": { 96 | "protractorConfig": "e2e/protractor.conf.js", 97 | "devServerTarget": "ngx-feature-toggle-demo:serve" 98 | }, 99 | "configurations": { 100 | "production": { 101 | "devServerTarget": "ngx-feature-toggle-demo:serve:production" 102 | } 103 | } 104 | }, 105 | "server": { 106 | "builder": "@angular-devkit/build-angular:server", 107 | "options": { 108 | "outputPath": "dist/ngx-feature-toggle-demo/server", 109 | "main": "server.ts", 110 | "tsConfig": "tsconfig.server.json", 111 | "sourceMap": true, 112 | "optimization": false 113 | }, 114 | "configurations": { 115 | "production": { 116 | "outputHashing": "media", 117 | "fileReplacements": [ 118 | { 119 | "replace": "src/environments/environment.ts", 120 | "with": "src/environments/environment.prod.ts" 121 | } 122 | ], 123 | "sourceMap": false, 124 | "optimization": true 125 | } 126 | }, 127 | "defaultConfiguration": "" 128 | }, 129 | "serve-ssr": { 130 | "builder": "@nguniversal/builders:ssr-dev-server", 131 | "options": { 132 | "browserTarget": "ngx-feature-toggle-demo:build", 133 | "serverTarget": "ngx-feature-toggle-demo:server" 134 | }, 135 | "configurations": { 136 | "production": { 137 | "browserTarget": "ngx-feature-toggle-demo:build:production", 138 | "serverTarget": "ngx-feature-toggle-demo:server:production" 139 | } 140 | } 141 | }, 142 | "prerender": { 143 | "builder": "@nguniversal/builders:prerender", 144 | "options": { 145 | "browserTarget": "ngx-feature-toggle-demo:build:production", 146 | "serverTarget": "ngx-feature-toggle-demo:server:production", 147 | "routes": ["/"] 148 | }, 149 | "configurations": { 150 | "production": {} 151 | } 152 | } 153 | } 154 | }, 155 | "ngx-feature-toggle": { 156 | "projectType": "library", 157 | "root": "projects/ngx-feature-toggle", 158 | "sourceRoot": "projects/ngx-feature-toggle/src", 159 | "prefix": "lib", 160 | "architect": { 161 | "build": { 162 | "builder": "@angular-devkit/build-angular:ng-packagr", 163 | "options": { 164 | "tsConfig": "projects/ngx-feature-toggle/tsconfig.lib.json", 165 | "project": "projects/ngx-feature-toggle/ng-package.json" 166 | }, 167 | "configurations": { 168 | "production": { 169 | "tsConfig": "projects/ngx-feature-toggle/tsconfig.lib.prod.json" 170 | } 171 | } 172 | }, 173 | "test": { 174 | "builder": "@angular-devkit/build-angular:karma", 175 | "options": { 176 | "main": "projects/ngx-feature-toggle/src/test.ts", 177 | "tsConfig": "projects/ngx-feature-toggle/tsconfig.spec.json", 178 | "karmaConfig": "projects/ngx-feature-toggle/karma.conf.js" 179 | } 180 | }, 181 | "lint": { 182 | "builder": "@angular-eslint/builder:lint", 183 | "options": { 184 | "lintFilePatterns": ["projects/ngx-feature-toggle/**/*.ts", "projects/ngx-feature-toggle/**/*.html"] 185 | } 186 | } 187 | } 188 | } 189 | }, 190 | "schematics": { 191 | "@angular-eslint/schematics:application": { 192 | "setParserOptionsProject": true 193 | }, 194 | "@angular-eslint/schematics:library": { 195 | "setParserOptionsProject": true 196 | } 197 | }, 198 | "cli": { 199 | "analytics": false 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('NGX Skeleton Loader'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | // tslint:disable-next-line: no-any 6 | return browser.get(browser.baseUrl) as Promise; 7 | } 8 | 9 | getTitleText() { 10 | return element(by.css('app-root h1')).getText() as Promise; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | require('karma-coverage'), 15 | ], 16 | client: { 17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | jasmineHtmlReporter: { 20 | suppressAll: true, // removes the duplicated traces 21 | }, 22 | coverageReporter: { 23 | dir: require('path').join(__dirname, './coverage/ngx-feature-toggle-demo'), 24 | subdir: '.', 25 | reporters: [{ type: 'html' }, { type: 'lcovonly' }, { type: 'text-summary' }], 26 | fixWebpackSourcePaths: true, 27 | }, 28 | reporters: ['progress', 'kjhtml'], 29 | port: 9876, 30 | colors: true, 31 | logLevel: config.LOG_INFO, 32 | autoWatch: true, 33 | browsers: [process.env.CI ? 'ChromeHeadlessNoSandbox' : 'Chrome'], 34 | customLaunchers: { 35 | ChromeHeadlessNoSandbox: { 36 | base: 'ChromeHeadless', 37 | flags: ['--no-sandbox'], 38 | }, 39 | }, 40 | singleRun: false, 41 | restartOnFileChange: true, 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-feature-toggle-demo", 3 | "description": "Your module to handle with feature toggles in Angular applications easier.", 4 | "author": "Will Mendes ", 5 | "license": "MIT", 6 | "version": "12.0.0", 7 | "keywords": [ 8 | "angular", 9 | "ngx", 10 | "ng", 11 | "feature-toggle", 12 | "feature", 13 | "flag", 14 | "on", 15 | "off", 16 | "toggle", 17 | "ngx-feature-toggle" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/willmendesneto/ngx-feature-toggle.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/willmendesneto/ngx-feature-toggle/issues" 25 | }, 26 | "homepage": "https://github.com/willmendesneto/ngx-feature-toggle#readme", 27 | "scripts": { 28 | "ng": "ng", 29 | "start": "ng serve", 30 | "build": "ng build", 31 | "test": "ng test --watch=false --no-progress --code-coverage && ng test ngx-feature-toggle --watch=false --no-progress --code-coverage", 32 | "tdd": "ng test", 33 | "lint": "ng lint", 34 | "lint:types": "tsc --noEmit", 35 | "e2e": "ng e2e", 36 | "bundlesize": "bundlesize", 37 | "build:pkg": "ng build ngx-feature-toggle --configuration production", 38 | "update-library-version": "node ./scripts/build.js && cp -f *.md dist/ngx-feature-toggle", 39 | "postversion": "git push && git push --tags", 40 | "prepublish": "npm run build && npm run build:pkg && npm run update-library-version", 41 | "version": "version-changelog CHANGELOG.md && changelog-verify CHANGELOG.md && npm run build:pkg && npm run update-library-version && git add .", 42 | "coveralls": "cat ./coverage/ngx-feature-toggle/lcov.info | ./node_modules/.bin/coveralls && rm -rf coverage", 43 | "postinstall": "ngcc", 44 | "dev:ssr": "ng run ngx-feature-toggle-demo:serve-ssr", 45 | "serve:ssr": "node dist/ngx-feature-toggle-demo/server/main.js", 46 | "build:ssr": "ng build --configuration production && ng run ngx-feature-toggle-demo:server:production", 47 | "prerender": "ng run ngx-feature-toggle-demo:prerender" 48 | }, 49 | "private": false, 50 | "bundlesize": [ 51 | { 52 | "path": "./dist/ngx-feature-toggle/fesm2020/ngx-feature-toggle.mjs", 53 | "maxSize": "1.7KB" 54 | } 55 | ], 56 | "dependencies": { 57 | "@angular/animations": "^15.2.4", 58 | "@angular/common": "^15.2.4", 59 | "@angular/compiler": "^15.2.4", 60 | "@angular/core": "^15.2.4", 61 | "@angular/forms": "^15.2.4", 62 | "@angular/platform-browser": "^15.2.4", 63 | "@angular/platform-browser-dynamic": "^15.2.4", 64 | "@angular/platform-server": "^15.2.4", 65 | "@angular/router": "^15.2.4", 66 | "@nguniversal/express-engine": "^15.0.0", 67 | "express": "^4.15.2", 68 | "feature-toggle-service": "^6.1.0", 69 | "rxjs": "~6.6.7", 70 | "tslib": "^2.5.1", 71 | "zone.js": "~0.13.0" 72 | }, 73 | "devDependencies": { 74 | "@angular-devkit/build-angular": "^15.0.4", 75 | "@angular-eslint/builder": "15.1.0", 76 | "@angular-eslint/eslint-plugin": "15.1.0", 77 | "@angular-eslint/eslint-plugin-template": "15.1.0", 78 | "@angular-eslint/schematics": "15.1.0", 79 | "@angular-eslint/template-parser": "15.1.0", 80 | "@angular/cli": "^15.0.4", 81 | "@angular/compiler-cli": "^15.0.4", 82 | "@angular/language-service": "^15.0.4", 83 | "@nguniversal/builders": "^15.0.0", 84 | "@types/express": "^4.17.0", 85 | "@types/jasmine": "~3.6.0", 86 | "@types/jasminewd2": "~2.0.3", 87 | "@types/node": "^12.11.1", 88 | "@typescript-eslint/eslint-plugin": "^5.43.0", 89 | "@typescript-eslint/parser": "^5.43.0", 90 | "ajv": "^8.11.2", 91 | "bundlesize": "^0.18.1", 92 | "changelog-verify": "^1.1.0", 93 | "core-js": "^3.26.1", 94 | "coveralls": "^3.0.2", 95 | "eslint": "^8.28.0", 96 | "jasmine-core": "~3.6.0", 97 | "jasmine-spec-reporter": "~5.0.0", 98 | "karma": "~6.3.9", 99 | "karma-chrome-launcher": "~3.1.0", 100 | "karma-coverage": "^2.1.0", 101 | "karma-coverage-istanbul-reporter": "~3.0.2", 102 | "karma-jasmine": "~4.0.0", 103 | "karma-jasmine-html-reporter": "^1.5.0", 104 | "ng-packagr": "^15.0.3", 105 | "protractor": "~7.0.0", 106 | "ts-node": "~8.3.0", 107 | "tsickle": "^0.39.1", 108 | "typescript": "~4.8.4", 109 | "version-changelog": "^3.1.0" 110 | }, 111 | "engines": { 112 | "node": ">=14.20.0" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["projects/ngx-feature-toggle/tsconfig.lib.json", "projects/ngx-feature-toggle/tsconfig.spec.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/README.md: -------------------------------------------------------------------------------- 1 | # NgxSkeletonLoader 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.0. 4 | 5 | ## Code scaffolding 6 | 7 | Run `ng generate component component-name --project ngx-feature-toggle` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ngx-feature-toggle`. 8 | 9 | > Note: Don't forget to add `--project ngx-feature-toggle` or else it will be added to the default project in your `angular.json` file. 10 | 11 | ## Build 12 | 13 | Run `ng build ngx-feature-toggle` to build the project. The build artifacts will be stored in the `dist/` directory. 14 | 15 | ## Publishing 16 | 17 | After building your library with `ng build ngx-feature-toggle`, go to the dist folder `cd dist/ngx-feature-toggle` and run `npm publish`. 18 | 19 | ## Running unit tests 20 | 21 | Run `ng test ngx-feature-toggle` to execute the unit tests via [Karma](https://karma-runner.github.io). 22 | 23 | ## Further help 24 | 25 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 26 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | require('karma-coverage'), 15 | ], 16 | client: { 17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | dir: require('path').join(__dirname, '../../coverage/ngx-feature-toggle'), 21 | reports: ['html', 'lcovonly'], 22 | fixWebpackSourcePaths: true, 23 | }, 24 | reporters: ['progress', 'kjhtml'], 25 | port: 9876, 26 | colors: true, 27 | logLevel: config.LOG_INFO, 28 | autoWatch: true, 29 | browsers: [process.env.CI ? 'ChromeHeadlessNoSandbox' : 'Chrome'], 30 | customLaunchers: { 31 | ChromeHeadlessNoSandbox: { 32 | base: 'ChromeHeadless', 33 | flags: ['--no-sandbox'], 34 | }, 35 | }, 36 | singleRun: false, 37 | restartOnFileChange: true, 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-feature-toggle", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["."] 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-feature-toggle", 3 | "description": "Your module to handle with feature toggles in Angular applications easier.", 4 | "author": "Will Mendes ", 5 | "version": "12.0.0", 6 | "keywords": [ 7 | "angular", 8 | "ngx", 9 | "feature-toggle", 10 | "feature-flag", 11 | "feature", 12 | "toggle", 13 | "flag", 14 | "ngx-feature-toggle" 15 | ], 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/willmendesneto/ngx-feature-toggle.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/willmendesneto/ngx-feature-toggle/issues" 23 | }, 24 | "homepage": "https://github.com/willmendesneto/ngx-feature-toggle#readme", 25 | "peerDependencies": { 26 | "@angular/common": ">=8.0.0", 27 | "@angular/core": ">=8.0.0", 28 | "@angular/router": ">=8.0.0" 29 | }, 30 | "dependencies": { 31 | "feature-toggle-service": "^6.0.0" 32 | } 33 | } -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle-provider.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; 2 | import { TestBed, waitForAsync as asyncMethod } from '@angular/core/testing'; 3 | import { FeatureToggleProviderComponent } from './ngx-feature-toggle-provider.component'; 4 | import { FeatureToggleDirective } from './ngx-feature-toggle.directive'; 5 | import { set, FeatureToggleServiceConfig } from 'feature-toggle-service'; 6 | 7 | @Component({ 8 | selector: 'kp-container', 9 | template: ` 10 |
11 | 12 |
13 |

Enabled content

14 |
Disabled content
15 |
16 |
17 |
18 | `, 19 | }) 20 | class ContainerComponent { 21 | featureToggleData: FeatureToggleServiceConfig = { 22 | enableFirstText: true, 23 | enableSecondText: false, 24 | }; 25 | } 26 | 27 | describe('Component: FeatureToggleProviderComponent', () => { 28 | let fixture: any; 29 | let nativeElement: any; 30 | const stub: any = {}; 31 | 32 | beforeEach( 33 | asyncMethod(() => { 34 | set({ enableFirstText: true }); 35 | 36 | fixture = TestBed.configureTestingModule({ 37 | declarations: [ContainerComponent, FeatureToggleProviderComponent, FeatureToggleDirective], 38 | schemas: [NO_ERRORS_SCHEMA], 39 | }).createComponent(ContainerComponent); 40 | nativeElement = fixture.nativeElement; 41 | fixture.detectChanges(); 42 | }), 43 | ); 44 | 45 | afterEach(() => { 46 | set({ enableFirstText: false }); 47 | }); 48 | 49 | it('should render the enabled children content', () => { 50 | const elementText = fixture.nativeElement.querySelectorAll('.feature-toggle-component')[0].innerText; 51 | expect(elementText).toContain('Enabled content'); 52 | }); 53 | 54 | it('should NOT render the disabled content', () => { 55 | const elementText = fixture.nativeElement.querySelectorAll('.feature-toggle-component')[0].innerText; 56 | expect(elementText).not.toContain('Disabled content'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle-provider.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, DoCheck } from '@angular/core'; 2 | import { set, FeatureToggleServiceConfig } from 'feature-toggle-service'; 3 | 4 | @Component({ 5 | selector: 'feature-toggle-provider', 6 | template: '', 7 | }) 8 | export class FeatureToggleProviderComponent implements DoCheck, OnInit { 9 | @Input() 10 | features: FeatureToggleServiceConfig = {}; 11 | 12 | private currentConfig: FeatureToggleServiceConfig = {}; 13 | 14 | ngOnInit() { 15 | if (typeof this.features !== 'object') { 16 | throw new Error('Attribute `features` should not be null or empty'); 17 | } 18 | this.setFeatureToggles(); 19 | } 20 | 21 | ngDoCheck() { 22 | this.setFeatureToggles(); 23 | } 24 | 25 | private setFeatureToggles() { 26 | if (this.currentConfig !== this.features) { 27 | // Using `Object.assign()` method for bundle size decreasing purposes 28 | // It's required since it needs a new memory reference 29 | // for the new object value 30 | this.currentConfig = Object.assign({}, this.features); 31 | set(this.features); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle-route-guard.router.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgxFeatureToggleRouteGuard } from './ngx-feature-toggle-route-guard.router'; 2 | import { Route } from '@angular/router'; 3 | import { set } from 'feature-toggle-service'; 4 | 5 | const fakeRouter = { 6 | navigate: () => {}, 7 | } as any; 8 | 9 | describe('Component: NgxFeatureToggleRouteGuard', () => { 10 | beforeEach(() => { 11 | set({ isFirstFeatureEnabled: true, isSecondFeatureEnabled: false }); 12 | spyOn(console, 'error'); 13 | spyOn(fakeRouter, 'navigate'); 14 | }); 15 | 16 | afterEach(() => { 17 | set({ isFirstFeatureEnabled: false, isSecondFeatureEnabled: false }); 18 | }); 19 | 20 | // We have the same test for all the route guards methods 21 | // So that, we are keeping the same behaviour as before 22 | ['canActivateChild', 'canLoad', 'canActivate'].forEach((routeGuardMethod: string) => { 23 | const method = routeGuardMethod as keyof NgxFeatureToggleRouteGuard; 24 | 25 | describe(`#${method}()`, () => { 26 | it('should return `false` if feature toggle is not configured in application level', () => { 27 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 28 | 29 | expect( 30 | instance[method]({ 31 | path: 'home', 32 | data: { 33 | featureToggle: ['thisFeatureToggleDoesNotExist'], 34 | }, 35 | } as Route), 36 | ).toBeFalsy(); 37 | }); 38 | 39 | it('should return `false` if feature toggle key does not exist in route', () => { 40 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 41 | 42 | const result = instance[method]({ 43 | path: 'home', 44 | } as Route); 45 | 46 | expect(result).toBeFalsy(); 47 | expect(console.error).toHaveBeenCalledWith( 48 | '`NgxFeatureToggleRouteGuard` need to receive `featureToggle` as data as an array or string in your route configuration.', 49 | ); 50 | }); 51 | 52 | it('should return `false` if feature toggle is not added in route as an array', () => { 53 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 54 | 55 | const result = instance[method]({ 56 | data: { 57 | featureToggle: {}, 58 | }, 59 | } as Route); 60 | 61 | expect(result).toBeFalsy(); 62 | expect(console.error).toHaveBeenCalledWith( 63 | '`NgxFeatureToggleRouteGuard` need to receive `featureToggle` as data as an array or string in your route configuration.', 64 | ); 65 | }); 66 | 67 | it('should return `false` if feature toggle is disabled and `redirectTo` is null', () => { 68 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 69 | 70 | expect( 71 | instance[method]({ 72 | data: { 73 | featureToggle: ['isSecondFeatureEnabled'], 74 | }, 75 | } as Route), 76 | ).toBeFalsy(); 77 | 78 | expect(fakeRouter.navigate).not.toHaveBeenCalled(); 79 | }); 80 | 81 | it('should return `false` and redirect to the specific URL if feature toggle is disabled AND route contains `redirectTo`', () => { 82 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 83 | 84 | expect( 85 | instance[method]({ 86 | data: { 87 | featureToggle: ['isSecondFeatureEnabled'], 88 | redirectTo: '/redirect-url', 89 | }, 90 | } as Route), 91 | ).toBeFalsy(); 92 | expect(fakeRouter.navigate).toHaveBeenCalledWith(['/redirect-url']); 93 | }); 94 | 95 | it('should return `false` and redirect to the specific URL if feature toggle is disabled AND `redirectTo` is null', () => { 96 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 97 | const redirectTo = ''; 98 | 99 | expect( 100 | instance[method]({ 101 | data: { 102 | featureToggle: ['isSecondFeatureEnabled'], 103 | redirectTo, 104 | }, 105 | } as Route), 106 | ).toBeFalsy(); 107 | 108 | expect(fakeRouter.navigate).toHaveBeenCalledWith([redirectTo]); 109 | }); 110 | 111 | it('should NOT console errors if code is running in production mode', () => { 112 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 113 | spyOn(instance, 'isDevMode').and.returnValue(false); 114 | 115 | instance[method]({ 116 | data: { 117 | featureToggle: {}, 118 | }, 119 | } as Route); 120 | 121 | instance[method]({ 122 | data: {}, 123 | } as Route); 124 | expect(console.error).not.toHaveBeenCalled(); 125 | }); 126 | 127 | it('should return `true` if feature toggle is enabled', () => { 128 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 129 | 130 | expect( 131 | instance[method]({ 132 | data: { 133 | featureToggle: ['isFirstFeatureEnabled'], 134 | }, 135 | } as Route), 136 | ).toBeTruthy(); 137 | }); 138 | 139 | it('should return `true` if feature toggle is disabled AND route configuration starts with `!`', () => { 140 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 141 | 142 | expect( 143 | instance[method]({ 144 | data: { 145 | featureToggle: ['!isSecondFeatureEnabled'], 146 | }, 147 | } as Route), 148 | ).toBeTruthy(); 149 | }); 150 | 151 | it('should return `true` if combination of feature toggles are thruthy', () => { 152 | const instance = new NgxFeatureToggleRouteGuard(fakeRouter); 153 | 154 | expect( 155 | instance[method]({ 156 | data: { 157 | featureToggle: ['isFirstFeatureEnabled', '!isSecondFeatureEnabled'], 158 | }, 159 | } as Route), 160 | ).toBeTruthy(); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle-route-guard.router.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, isDevMode } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Route, CanActivateChild, Router, CanLoad, CanActivate } from '@angular/router'; 3 | import { isOn } from 'feature-toggle-service'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class NgxFeatureToggleRouteGuard implements CanActivateChild, CanLoad, CanActivate { 7 | constructor(private router: Router) {} 8 | 9 | isDevMode() { 10 | return isDevMode(); 11 | } 12 | 13 | private isOnCheck(route: ActivatedRouteSnapshot | Route): boolean { 14 | if ( 15 | !route || 16 | !route.data || 17 | (typeof route.data.featureToggle !== 'string' && !Array.isArray(route.data.featureToggle)) 18 | ) { 19 | if (this.isDevMode()) { 20 | console.error( 21 | // tslint:disable-next-line: max-line-length 22 | '`NgxFeatureToggleRouteGuard` need to receive `featureToggle` as data as an array or string in your route configuration.', 23 | ); 24 | } 25 | return false; 26 | } 27 | 28 | const hasAllTogglesOn = ([].concat(route.data.featureToggle as any) as string[]).every(toggle => 29 | toggle[0] === '!' ? !isOn(toggle.replace('!', '')) : isOn(toggle), 30 | ); 31 | 32 | if (!hasAllTogglesOn && route.data.redirectTo !== null && route.data.redirectTo !== undefined) { 33 | this.router.navigate([].concat(route.data.redirectTo)); 34 | } 35 | 36 | return hasAllTogglesOn; 37 | } 38 | 39 | canLoad(route: Route): boolean { 40 | return this.isOnCheck(route); 41 | } 42 | 43 | canActivateChild(route: ActivatedRouteSnapshot | Route): boolean { 44 | return this.isOnCheck(route); 45 | } 46 | 47 | canActivate(route: ActivatedRouteSnapshot | Route): boolean { 48 | return this.isOnCheck(route); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { FeatureToggleDirective } from './ngx-feature-toggle.directive'; 4 | import { FeatureToggleProviderComponent } from './ngx-feature-toggle-provider.component'; 5 | import { set } from 'feature-toggle-service'; 6 | 7 | @Component({ 8 | selector: 'kp-container', 9 | template: ` 10 |
11 |
12 |

Feature toggle enabled

13 |
17 | Feature toggle disabled 18 |
19 | 20 |
24 | Feature toggle disabled since it's enabled and it has ! at 25 | front. 26 |
27 | 28 |
32 | Feature toggle enabled since it's disabled and it has ! at 33 | front. 34 |
35 | 36 |
40 |

41 | This is a combined condition. It shows if enableFirstText is 42 | true and enableSecondText is falsy. If both cases are 43 | correct, then the "featureToggle" is enabled and rendering this 44 | component. 45 |

46 |
47 | 48 |
52 |

53 | This is a combined condition, but the content should not be 54 | rendered. It shows if enableFirstText is and 55 | enableSecondText are true. 56 |

57 |
58 |
59 |
60 | `, 61 | }) 62 | class ContainerComponent {} 63 | 64 | describe('Component: FeatureToggle', () => { 65 | let fixture: any; 66 | 67 | beforeEach(async () => { 68 | set({ enableFirstText: true }); 69 | 70 | fixture = TestBed.configureTestingModule({ 71 | declarations: [ 72 | ContainerComponent, 73 | FeatureToggleDirective, 74 | FeatureToggleProviderComponent, 75 | ], 76 | schemas: [NO_ERRORS_SCHEMA], 77 | }).createComponent(ContainerComponent); 78 | fixture.detectChanges(); 79 | }); 80 | 81 | afterEach(() => { 82 | set({ enableFirstText: false }); 83 | }); 84 | 85 | describe('When featureToggle is enabled', () => { 86 | it('should render the component content', () => { 87 | expect( 88 | fixture.nativeElement.querySelector('.feature-toggle-enabled').innerText 89 | ).toContain('Feature toggle enabled'); 90 | }); 91 | 92 | it('should NOT render the component content if feature toggle is enabled and it contains `!` as first string', () => { 93 | expect( 94 | fixture.nativeElement.querySelector( 95 | '.feature-toggle-enabled-with-exclamation-mark' 96 | ) 97 | ).toEqual(null); 98 | }); 99 | }); 100 | 101 | describe('When featureToggle is disabled', () => { 102 | it('should NOT render the component content', () => { 103 | expect( 104 | fixture.nativeElement.querySelector('.feature-toggle-disabled') 105 | ).toEqual(null); 106 | }); 107 | 108 | it('should update when feature toggle data change', () => { 109 | expect( 110 | fixture.nativeElement.querySelector('.feature-toggle-enabled').innerText 111 | ).not.toEqual(null); 112 | 113 | set({ enableFirstText: false }); 114 | fixture.detectChanges(); 115 | 116 | expect( 117 | fixture.nativeElement.querySelector('.feature-toggle-enabled') 118 | ).toEqual(null); 119 | }); 120 | 121 | it('should render the component content if feature toggle is disabled and it contains `!` as first string', () => { 122 | expect( 123 | fixture.nativeElement.querySelector( 124 | '.feature-toggle-disabled-with-exclamation-mark' 125 | ).innerText 126 | ).toContain( 127 | // tslint:disable-next-line: quotemark 128 | "Feature toggle enabled since it's disabled and it has ! at front" 129 | ); 130 | }); 131 | }); 132 | 133 | describe('When featureToggle receives an array of features', () => { 134 | it('should render the component content if `enableFirstText` is true and `enableSecondText` is falsy', () => { 135 | const textContent = fixture.nativeElement.querySelector( 136 | '.combined-feature-toggles-with-truthly-option' 137 | ).innerText; 138 | 139 | expect(textContent).toContain( 140 | // tslint:disable-next-line: max-line-length 141 | 'This is a combined condition. It shows if enableFirstText is true and enableSecondText is falsy. If both cases are correct, then the "featureToggle" is enabled and rendering this component.' 142 | ); 143 | }); 144 | 145 | it('should NOT render the component content if `enableFirstText` and `enableSecondText` are not truthly', () => { 146 | expect( 147 | fixture.nativeElement.querySelector( 148 | '.combined-feature-toggles-with-falsy-option' 149 | ) 150 | ).toEqual(null); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnInit, TemplateRef, ViewContainerRef, DoCheck, isDevMode } from '@angular/core'; 2 | 3 | import { isOn } from 'feature-toggle-service'; 4 | 5 | @Directive({ 6 | // tslint:disable-next-line: directive-selector 7 | selector: '[featureToggle]', 8 | }) 9 | export class FeatureToggleDirective implements OnInit, DoCheck { 10 | @Input() public featureToggle: string[] | string | undefined; 11 | private isOn = false; 12 | 13 | constructor(private templateRef: TemplateRef, private viewContainer: ViewContainerRef) {} 14 | 15 | ngOnInit() { 16 | if (!this.featureToggle) { 17 | throw new Error('Attribute `featureToggle` should not be null or empty'); 18 | } 19 | this.shouldRender(); 20 | } 21 | 22 | ngDoCheck() { 23 | if (this.isOn !== this.isOnCheck(this.featureToggle)) { 24 | this.shouldRender(); 25 | } 26 | } 27 | 28 | private shouldRender() { 29 | this.isOn = this.isOnCheck(this.featureToggle); 30 | if (this.isOn) { 31 | this.viewContainer.createEmbeddedView(this.templateRef); 32 | } else { 33 | this.viewContainer.clear(); 34 | } 35 | } 36 | 37 | isOnCheck(featureToggle: string[] | string | undefined) { 38 | if (typeof featureToggle !== 'string' && !Array.isArray(featureToggle)) { 39 | if (isDevMode()) { 40 | console.error('`NgxFeatureToggle`: `featureToggle` should receive an array or an string as a value.'); 41 | } 42 | return false; 43 | } 44 | 45 | return ([].concat(featureToggle as any) as string[]).every(toggle => 46 | toggle[0] === '!' ? !isOn(toggle.replace('!', '')) : isOn(toggle), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/lib/ngx-feature-toggle.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FeatureToggleProviderComponent } from './ngx-feature-toggle-provider.component'; 4 | import { FeatureToggleDirective } from './ngx-feature-toggle.directive'; 5 | 6 | @NgModule({ 7 | declarations: [FeatureToggleProviderComponent, FeatureToggleDirective], 8 | exports: [FeatureToggleProviderComponent, FeatureToggleDirective], 9 | imports: [CommonModule], 10 | }) 11 | export class FeatureToggleModule {} 12 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-feature-toggle 3 | */ 4 | 5 | export * from './lib/ngx-feature-toggle.directive'; 6 | export * from './lib/ngx-feature-toggle-provider.component'; 7 | export * from './lib/ngx-feature-toggle.module'; 8 | export * from './lib/ngx-feature-toggle-route-guard.router'; 9 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 10 | teardown: { destroyAfterEach: false } 11 | }); 12 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": ["dom", "es2018"], 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | }, 14 | "angularCompilerOptions": { 15 | "annotateForClosureCompiler": true, 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "fullTemplateTypeCheck": true, 19 | "strictInjectionParameters": true, 20 | "enableResourceInlining": true, 21 | "strictTemplates": true 22 | }, 23 | "exclude": ["src/test.ts", "**/*.spec.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "enableIvy": false, 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ngx-feature-toggle/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const { version, name } = require('./../package.json'); 2 | const fs = require('fs'); 3 | 4 | const NG_MODULE_NAME = name.replace('-demo', ''); 5 | 6 | const changePackageVersionWithNewVersion = (packageNameDirectory, version) => { 7 | const pkg = JSON.parse( 8 | fs.readFileSync(`${packageNameDirectory}/package.json`, 'utf8') 9 | ); 10 | const pkgWithNewVersion = { ...pkg, version }; 11 | 12 | fs.writeFileSync( 13 | `${packageNameDirectory}/package.json`, 14 | JSON.stringify(pkgWithNewVersion, null, 2) 15 | ); 16 | }; 17 | 18 | try { 19 | changePackageVersionWithNewVersion( 20 | `${__dirname}/../dist/${NG_MODULE_NAME}`, 21 | version 22 | ); 23 | 24 | changePackageVersionWithNewVersion( 25 | `${__dirname}/../projects/${NG_MODULE_NAME}`, 26 | version 27 | ); 28 | } catch (error) { 29 | throw error; 30 | process.exit(); 31 | } 32 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/node'; 2 | 3 | import { ngExpressEngine } from '@nguniversal/express-engine'; 4 | import * as express from 'express'; 5 | import { join } from 'path'; 6 | 7 | import { AppServerModule } from './src/main.server'; 8 | import { APP_BASE_HREF } from '@angular/common'; 9 | import { existsSync } from 'fs'; 10 | 11 | // The Express app is exported so that it can be used by serverless Functions. 12 | export function app() { 13 | const server = express(); 14 | const distFolder = join(process.cwd(), 'dist/ngx-feature-toggle-demo/browser'); 15 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index'; 16 | 17 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 18 | server.engine( 19 | 'html', 20 | ngExpressEngine({ 21 | bootstrap: AppServerModule, 22 | // tslint:disable-next-line: no-any 23 | }) as any, 24 | ); 25 | 26 | server.set('view engine', 'html'); 27 | server.set('views', distFolder); 28 | 29 | // Example Express Rest API endpoints 30 | // server.get('/api/**', (req, res) => { }); 31 | // Serve static files from /browser 32 | server.get( 33 | '*.*', 34 | express.static(distFolder, { 35 | maxAge: '1y', 36 | }), 37 | ); 38 | 39 | // All regular routes use the Universal engine 40 | server.get('*', (req, res) => { 41 | res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); 42 | }); 43 | 44 | return server; 45 | } 46 | 47 | function run() { 48 | const port = process.env.PORT || 4000; 49 | 50 | // Start up the Node server 51 | const server = app(); 52 | server.listen(port, () => { 53 | console.log(`Node Express server listening on http://localhost:${port}`); 54 | }); 55 | } 56 | 57 | // Webpack will replace 'require' with '__webpack_require__' 58 | // '__non_webpack_require__' is a proxy to Node 'require' 59 | // The below code is to ensure that the server is run only when not requiring the bundle. 60 | declare const __non_webpack_require__: NodeRequire; 61 | const mainModule = __non_webpack_require__.main; 62 | const moduleFilename = (mainModule && mainModule.filename) || ''; 63 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 64 | run(); 65 | } 66 | 67 | export * from './src/main.server'; 68 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ErrorComponent } from './error/error.component'; 4 | import { HomeComponent } from './home/home.component'; 5 | import { set } from 'feature-toggle-service'; 6 | import { CustomerComponent } from './customer/customer.component'; 7 | import { CustomerDetailComponent } from './customer/customer-detail.component'; 8 | import { RestrictPageDueFeatureToggleComponent } from './restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.component'; 9 | import { NgxFeatureToggleRouteGuard } from 'projects/ngx-feature-toggle/src/lib/ngx-feature-toggle-route-guard.router'; 10 | 11 | set({ 12 | enableFirstText: true, 13 | enableSecondText: true, 14 | enableCustomerPage: true, 15 | enableChildrenNavigation: false, 16 | }); 17 | 18 | export const routes: Routes = [ 19 | { 20 | path: 'home', 21 | component: HomeComponent, 22 | canLoad: [NgxFeatureToggleRouteGuard], 23 | canActivate: [NgxFeatureToggleRouteGuard], 24 | data: { 25 | featureToggle: ['enableSecondText'], 26 | redirectTo: '/error', 27 | }, 28 | }, 29 | { 30 | path: 'restrict', 31 | component: RestrictPageDueFeatureToggleComponent, 32 | canActivate: [NgxFeatureToggleRouteGuard], 33 | data: { 34 | featureToggle: ['!enableSecondText'], 35 | redirectTo: '/error', 36 | }, 37 | }, 38 | { 39 | path: 'error', 40 | component: ErrorComponent, 41 | canActivate: [NgxFeatureToggleRouteGuard], 42 | data: { 43 | featureToggle: ['enableFirstText'], 44 | }, 45 | }, 46 | { 47 | path: 'customer', 48 | component: CustomerComponent, 49 | canActivate: [NgxFeatureToggleRouteGuard], 50 | canActivateChild: [NgxFeatureToggleRouteGuard], 51 | data: { 52 | featureToggle: ['enableCustomerPage'], 53 | redirectTo: '/error', 54 | }, 55 | children: [ 56 | { 57 | path: ':id', 58 | component: CustomerDetailComponent, 59 | data: { 60 | featureToggle: ['enableCustomerPage', '!enableChildrenNavigation'], 61 | redirectTo: '/error', 62 | }, 63 | }, 64 | ], 65 | }, 66 | { path: '**', redirectTo: '/home' }, 67 | ]; 68 | 69 | @NgModule({ 70 | imports: [RouterModule.forRoot(routes)], 71 | exports: [RouterModule], 72 | }) 73 | export class AppRoutingModule {} 74 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 3 | import { AppComponent } from './app.component'; 4 | import { FeatureToggleModule } from 'projects/ngx-feature-toggle/src/lib/ngx-feature-toggle.module'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [AppComponent], 10 | imports: [FeatureToggleModule], 11 | schemas: [NO_ERRORS_SCHEMA], 12 | }).compileComponents(); 13 | })); 14 | 15 | it('should create the app', () => { 16 | const fixture = TestBed.createComponent(AppComponent); 17 | const app = fixture.debugElement.componentInstance; 18 | expect(app).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { isPlatformBrowser } from '@angular/common'; 2 | import { Component, Inject, NgZone, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'ngx-app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'], 8 | }) 9 | export class AppComponent implements OnInit, OnDestroy { 10 | featureToggleData: { 11 | [k: string]: boolean; 12 | } = { 13 | enableFirstText: false, 14 | enableSecondText: true, 15 | }; 16 | 17 | intervalId: number | undefined; 18 | 19 | constructor(private zone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {} 20 | 21 | ngOnInit(): void { 22 | if (isPlatformBrowser(this.platformId)) { 23 | // Required because Protractor current behavior 24 | // More details in https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular 25 | this.zone.runOutsideAngular(() => { 26 | this.intervalId = window.setInterval(() => { 27 | this.zone.run(() => { 28 | Object.keys(this.featureToggleData).map( 29 | key => (this.featureToggleData[key] = !this.featureToggleData[key]), 30 | ); 31 | }); 32 | // increase/decrease this number to see the 33 | // current feature toggle component behavior 34 | }, 5000); 35 | }); 36 | } 37 | } 38 | 39 | ngOnDestroy() { 40 | if (this.intervalId) { 41 | clearInterval(this.intervalId); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { AppComponent } from './app.component'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { FeatureToggleModule } from '../../projects/ngx-feature-toggle/src/lib/ngx-feature-toggle.module'; 7 | import { ErrorComponent } from './error/error.component'; 8 | import { HomeModule } from './home/home.module'; 9 | 10 | import { CustomerComponent } from './customer/customer.component'; 11 | import { CustomerDetailComponent } from './customer/customer-detail.component'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent, ErrorComponent, CustomerComponent, CustomerDetailComponent], 15 | imports: [ 16 | BrowserModule.withServerTransition({ appId: 'ngx-feature-toggle-demo' }), 17 | FeatureToggleModule, 18 | AppRoutingModule, 19 | HomeModule, 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent], 23 | }) 24 | export class AppModule {} 25 | -------------------------------------------------------------------------------- /src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { AppComponent } from './app.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | AppModule, 10 | ServerModule, 11 | ], 12 | bootstrap: [AppComponent], 13 | }) 14 | export class AppServerModule {} 15 | -------------------------------------------------------------------------------- /src/app/customer/customer-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'ngx-app-customer-detail', 6 | template: '

Customer Detail: ID {{ id }}

', 7 | }) 8 | export class CustomerDetailComponent implements OnInit { 9 | id: number = 0; 10 | 11 | constructor(private route: ActivatedRoute) {} 12 | 13 | ngOnInit() { 14 | this.route.paramMap.subscribe(params => (this.id = Number(params.get('id')))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/customer/customer.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Helvetica, Arial, 'Lucida Grande', sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/customer/customer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | interface Customer { 5 | id: number; 6 | name: string; 7 | } 8 | 9 | @Component({ 10 | selector: 'app-customer', 11 | styleUrls: ['./customer.component.css'], 12 | template: ` 13 |

Customer List

14 | 24 | 25 | `, 26 | }) 27 | export class CustomerComponent implements OnInit { 28 | customers: Customer[] = []; 29 | 30 | constructor(private router: Router) {} 31 | 32 | ngOnInit() { 33 | this.customers = [ 34 | { id: 1, name: 'Lee' }, 35 | { id: 2, name: 'Kim' }, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/error/error.component.html: -------------------------------------------------------------------------------- 1 |

Error

2 | -------------------------------------------------------------------------------- /src/app/error/error.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/app/error/error.component.scss -------------------------------------------------------------------------------- /src/app/error/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngx-app-error', 5 | templateUrl: './error.component.html', 6 | styleUrls: ['./error.component.scss'], 7 | }) 8 | export class ErrorComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/error/error.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ErrorComponent } from './error.component'; 4 | 5 | @NgModule({ 6 | imports: [CommonModule], 7 | declarations: [ErrorComponent], 8 | }) 9 | export class ErrorModule {} 10 | -------------------------------------------------------------------------------- /src/app/hello.component.html: -------------------------------------------------------------------------------- 1 |

Hello!

2 | 3 |
4 |

This is sparta

5 |

condition is true and "featureToggle" is enabled.

6 |
7 |
8 | -------------------------------------------------------------------------------- /src/app/hello.component.ts: -------------------------------------------------------------------------------- 1 | import { isPlatformBrowser } from '@angular/common'; 2 | import { Component, Input, OnInit, NgZone, OnDestroy, Inject, PLATFORM_ID } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'ngx-app-hello', 6 | templateUrl: './hello.component.html', 7 | styles: [ 8 | ` 9 | h1 { 10 | font-family: Lato; 11 | } 12 | `, 13 | ], 14 | }) 15 | export class HelloComponent implements OnInit, OnDestroy { 16 | @Input() name: string = ''; 17 | 18 | anotherFeatureToggleData: { 19 | [k: string]: boolean; 20 | } = { 21 | enableAnother: true, 22 | }; 23 | 24 | intervalId: number | undefined; 25 | 26 | ngOnInit() { 27 | console.error('HelloComponent - ngDoCheck() - Should not be called', this.name); 28 | if (isPlatformBrowser(this.platformId)) { 29 | // Required because Protractor current behavior 30 | // More details in https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular 31 | this.zone.runOutsideAngular(() => { 32 | this.intervalId = window.setInterval(() => { 33 | this.zone.run(() => { 34 | Object.keys(this.anotherFeatureToggleData).forEach( 35 | key => (this.anotherFeatureToggleData[key] = !this.anotherFeatureToggleData[key]), 36 | ); 37 | }); 38 | // increase/decrease this number to see the 39 | // current feature toggle component behavior 40 | }, 2000); 41 | }); 42 | } 43 | } 44 | 45 | constructor(private zone: NgZone, @Inject(PLATFORM_ID) private platformId: Object) {} 46 | 47 | ngOnDestroy() { 48 | if (this.intervalId) { 49 | clearInterval(this.intervalId); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 |

ngx-feature-toggle

3 |
4 | 5 | {{ featureToggleData | json }} 6 | 7 |
8 |

condition is true and "featureToggle" is enabled.

9 |
10 |
11 |

condition is false and "featureToggle" is disabled. In that case this content should not be rendered.

12 |
13 |
14 |

15 | condition is false and "featureToggle" is disabled 16 | but it has "!" as a prefix of the feature toggle to be checked. In that case this content should be 17 | rendered. 18 |

19 |
20 | 21 |
22 |

23 | This is a combined condition. It shows if enableSecondText is true and enableFirstText is 24 | falsy. If both cases are correct, then the "featureToggle" is enabled and rendering this component. 25 |

26 |
27 | 28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/app/home/home.component.scss -------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgZone } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngx-app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.scss'], 7 | }) 8 | export class HomeComponent { 9 | featureToggleData: any = { 10 | enableFirstText: false, 11 | enableSecondText: true, 12 | }; 13 | 14 | constructor(private zone: NgZone) { 15 | // Required because Protractor current behavior 16 | // More details in https://github.com/angular/protractor/blob/master/docs/timeouts.md#waiting-for-angular 17 | this.zone.runOutsideAngular(() => { 18 | setInterval(() => { 19 | this.zone.run(() => { 20 | Object.keys(this.featureToggleData).map(key => (this.featureToggleData[key] = !this.featureToggleData[key])); 21 | }); 22 | // increase/decrease this number to see the 23 | // current feature toggle component behavior 24 | }, 5000); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { HomeComponent } from './home.component'; 5 | 6 | import { FeatureToggleModule } from '../../../projects/ngx-feature-toggle/src/public-api'; 7 | import { HelloComponent } from '../hello.component'; 8 | 9 | @NgModule({ 10 | declarations: [HomeComponent, HelloComponent], 11 | imports: [BrowserModule, FeatureToggleModule], 12 | providers: [], 13 | bootstrap: [HomeComponent], 14 | }) 15 | export class HomeModule {} 16 | -------------------------------------------------------------------------------- /src/app/restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.component.html: -------------------------------------------------------------------------------- 1 |

Restrict Page

2 | -------------------------------------------------------------------------------- /src/app/restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/app/restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.component.scss -------------------------------------------------------------------------------- /src/app/restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ngx-app-restrict-page-due-feature-toggle', 5 | templateUrl: './restrict-page-due-feature-toggle.component.html', 6 | styleUrls: ['./restrict-page-due-feature-toggle.component.scss'], 7 | }) 8 | export class RestrictPageDueFeatureToggleComponent { 9 | constructor() {} 10 | } 11 | -------------------------------------------------------------------------------- /src/app/restrict-page-due-feature-toggle/restrict-page-due-feature-toggle.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RestrictPageDueFeatureToggleComponent } from './restrict-page-due-feature-toggle.component'; 4 | 5 | @NgModule({ 6 | imports: [CommonModule], 7 | declarations: [RestrictPageDueFeatureToggleComponent], 8 | }) 9 | export class RestrictPageDueFeatureToggleModule {} 10 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` 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 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willmendesneto/ngx-feature-toggle/a2572ba8887c2d3b993572ed8bbf5fde04fe525c/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxSkeletonLoaderDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { AppServerModule } from './app/app.server.module'; 10 | export { renderModuleFactory } from '@angular/platform-server'; 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | }); 16 | -------------------------------------------------------------------------------- /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 Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 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.ts'; 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 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": ["node"] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "exclude": ["src/test.ts", "src/**/*.spec.ts", "src/app/app.server.module.ts", "src/app/main.module.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "importHelpers": true, 12 | "target": "ES2022", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2018", "dom"], 15 | "paths": { 16 | "ngx-feature-toggle": ["dist/ngx-feature-toggle"], 17 | "ngx-feature-toggle/*": ["dist/ngx-feature-toggle/*"] 18 | }, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "useDefineForClassFields": false 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app-server", 5 | "strictNullChecks": false, 6 | "types": ["node"] 7 | }, 8 | "files": ["src/main.server.ts", "src/app/app.server.module.ts", "server.ts"], 9 | "angularCompilerOptions": { 10 | "entryModule": "./src/app/app.server.module#AppServerModule" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | --------------------------------------------------------------------------------