├── .editorconfig ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets └── bower │ ├── LICENSE │ ├── README.md │ ├── bower.json │ └── version.txt ├── example ├── NEWdashboard │ ├── dashboard.js │ └── newDashboard.html ├── admin │ ├── admin.html │ └── admin.js ├── app.js ├── components.module.js ├── dashboard │ ├── dashboard.html │ └── dashboard.js ├── features.json └── index.html ├── grunt ├── clean.js ├── copy.js ├── jshint.js └── uglify.js ├── package.json ├── scripts ├── angular-feature-toggle │ ├── tag-release.sh │ └── untag-release.sh ├── bower │ ├── publish.sh │ ├── repos.inc │ └── unpublish.sh ├── jenkins │ ├── master.sh │ ├── release.sh │ └── undo-release.sh └── utils.inc └── src └── angular-feature-toggle.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = tab 12 | # indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .idea/ 3 | 4 | # package managers 5 | bower_components/ 6 | node_modules/ 7 | 8 | # build elements 9 | .tmp/ 10 | dist/ 11 | build/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "esnext": true, 7 | "eqeqeq": true, 8 | "eqnull": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": "nofunc", 12 | "maxlen": 300, 13 | "maxstatements": 30, 14 | "newcap": true, 15 | "node": true, 16 | "strict": true, 17 | "trailing": true, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": true, 21 | "white": true, 22 | "globals": { 23 | "define": false, 24 | "require": false, 25 | "describe": false, 26 | "beforeEach": false, 27 | "afterEach": false, 28 | "it": false, 29 | "expect": false, 30 | "inject": false, 31 | "jasmine": false, 32 | "spyOn": false, 33 | "fdescribe": false, 34 | "xdescribe": false, 35 | "fit": false, 36 | "xit": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint camelcase: false */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = function (grunt) { 6 | 7 | // load all grunt tasks 8 | require('load-grunt-tasks')(grunt); 9 | 10 | // load all grunt configuration 11 | require('load-grunt-config')(grunt); 12 | 13 | // Register Tasks 14 | grunt.registerTask('default', 'Start working on this project.', [ 15 | 'jshint' 16 | ]); 17 | 18 | // Bower 19 | grunt.registerTask('bower', 'Build production ready assets and views.', [ 20 | 'clean', 21 | 'copy:bower' 22 | ]); 23 | 24 | // Build 25 | grunt.registerTask('build', 'Build production ready assets and views.', [ 26 | 'clean', 27 | 'copy:dist', 28 | 'uglify' 29 | ]); 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Internet Systems Consortium license 2 | =================================== 3 | 4 | Copyright (c) 2015, Mixpo, LLC. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose 7 | with or without fee is hereby granted, provided that the above copyright notice 8 | and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 12 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 14 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 15 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 16 | THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/angular-feature-toggle.svg)](http://badge.fury.io/js/angular-feature-toggle) 2 | 3 | # angular-feature-toggle 4 | 5 | ## Synopsis 6 | This library lets you manage features in the frontend using a semver notation. 7 | Inspired by [angular-feature-flags](https://github.com/mjt01/angular-feature-flags). 8 | 9 | ## Examples and Demo 10 | Check out the [example](https://github.com/yairhaimo/angular-feature-toggle/tree/master/example) directory for a simple usage of the library or this [plunker](http://plnkr.co/edit/j49u6oqQ6ulppqUphhMq) for the same demo. 11 | 12 | ## Installation 13 | ```sh 14 | npm install angular-feature-toggle --save 15 | or 16 | bower install angular-feature-toggle 17 | ``` 18 | ```js 19 | angular.module('main.app', ['yh.featureToggle']) 20 | ``` 21 | ## Configuration 22 | angular-feature-toggle uses a semver notation per feature and expects a configuration of this nature: 23 | ```js 24 | { 25 | "feature1": "1.5.1", 26 | "feature2": "0.5.6" 27 | } 28 | ``` 29 | This configuration toggles features inside the code according to their version conditions. 30 | ``` 31 | //Example for "feature1" : "1.5.1" 32 | //^1.0.0 - true 33 | //~1.5.0 - true 34 | //~1.6.0 - false 35 | //^2 - false 36 | //* - true 37 | ``` 38 | For more information regarding the semver notation head over to the [semver](http://semver.org/) and the [node-semver](https://github.com/npm/node-semver) sites. 39 | 40 | **NOTE**: In order to configure itself at angular's config phase angular-feature-toggle is, at the moment, dependant on a property named 'angularFeaturesConf' on the global window object. 41 | 42 | ##### Manual setting 43 | For hardcoded values you can set the property manually: 44 | ```js 45 | window.angularFeaturesConf = {dashboard: '1.0.0', admin: '0.5.1'}; 46 | ``` 47 | For a dynamic feature loading method take a look at the [serverside feature loading example](#serversideLoading). 48 | 49 | 50 | ## ui.router 51 | angular-feature-toggle detects if ui.router is in use and wraps it with a feature-toggle helper function. 52 | You can now define your states this way: 53 | ```js 54 | $stateProvider 55 | .state('master.dashboard', 56 | { 57 | url: '/dashboard', 58 | templateUrl: 'dashboard/dashboard.html', 59 | controller: 'DashboardController', 60 | controllerAs: 'dashboard', 61 | feature: 'dash', //optional 62 | version: '^0.5.1' 63 | } 64 | ) 65 | .state('master.dashboard', 66 | { 67 | url: '/dashboard', 68 | templateUrl: 'NEWdashboard/newDashboard.html', 69 | controller: 'NewDashboardController', 70 | controllerAs: 'dashboard', 71 | feature: 'dash', //optional 72 | version: '^1' 73 | } 74 | ); 75 | ``` 76 | Note that both states have the same name but different version conditions (***^0.5.1*** vs ***^1***). 77 | If the version is not satisfied for a specific state definition then that definition will be **ignored**. 78 | If more than one version of the same state is satisfied the first one will be defined and a warning message will be logged for the following ones. 79 | The 'feature' property is optional, if omitted the state name is considered to be the feature name. 80 | **NOTE**: if you omit the version parameter in the state definition then a regular state will be defined. 81 | 82 | 83 | ## Toggle features using a directive 84 | There are two directives you can use in order to toggle features: **show-if-feature** and **hide-if-feature**. 85 | Basically they act as ng-if and the opposite of ng-if. They add/remove elements from the DOM if a feature is enabled or satisfies a certain version. 86 | ```html 87 | 88 |
89 | This is the admin panel 90 |
91 | 92 |
93 | Widgets coming soon... 94 |
95 | ``` 96 | With a specific version: 97 | ```html 98 | 99 |
100 | This is the admin panel 101 |
102 | 103 |
104 | This is the NEW and improved admin panel 105 |
106 | ``` 107 | 108 | ## Toggle features programmatically 109 | You can use the ***featureToggle*** factory to check feature version: 110 | ```js 111 | .controller('HomeController', function(featureToggle) { 112 | if (featureToggle.isEnabled('admin')) { 113 | this.message = 'welcome administrator!'; 114 | } 115 | if (featureToggle.isVersion('admin', '^2')) { 116 | this.items = [1,2,3]; 117 | } 118 | }); 119 | ``` 120 | The featureToggle is also a provider and can be used inside ***.config*** blocks: 121 | ```js 122 | .config(function(featureToggleProvider) { 123 | if (featureToggleProvider.isVersion('dashboard', '~3.5.0')) { 124 | // do something 125 | } 126 | }); 127 | ``` 128 | **NOTE**: since angular-feature-toggle configures itself in a ***.config*** block it must be defined as a module dependancy in order for its .config block to run prior to the one that is using it. 129 | 130 | ## Serverside feature configuration loading 131 | Since we intialize the feature configuration in a ***.config*** block (in order to support state versioning) we cannot use the $http service to load the feature configuration from the server ($http is not injectable into a config block). 132 | What we can do is first load the configuration with vanilla javascript and only once its loaded manually bootstrap the angular applcation: 133 | ```js 134 | angular.element(document).ready(function() { 135 | fetch('/example/features.json').then(function(response) { 136 | response.json().then(function(features) { 137 | window.angularFeaturesConf = features; 138 | angular.bootstrap(document, ['app']); 139 | }) 140 | }); 141 | }); 142 | ``` 143 | This example uses the new [***fetch***](https://developers.google.com/web/updates/2015/03/introduction-to-fetch?hl=en) API for brevity but you can use any ajax function you wish. 144 | 145 | ## Limitations and TODOs 146 | * Dependency on a global window property - angularFeaturesConf 147 | * Cannot load feature configuration for a specific user since the feature initialization is done in the config phase 148 | * Reduce library size (uses the entire node-semver library atm, need only a subset of that) 149 | -------------------------------------------------------------------------------- /assets/bower/LICENSE: -------------------------------------------------------------------------------- 1 | Internet Systems Consortium license 2 | =================================== 3 | 4 | Copyright (c) 2015, Mixpo, LLC. 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose 7 | with or without fee is hereby granted, provided that the above copyright notice 8 | and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 12 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 14 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 15 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 16 | THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /assets/bower/README.md: -------------------------------------------------------------------------------- 1 | Bower packaged angular-feature-toggle 2 | ===================================== 3 | 4 | This repo is for distribution `bower`. The source for this module is in the 5 | [mixpo angular-feature-toggle repo](https://github.com/Mixpo/angular-feature-toggle). 6 | Please file issues and pull requests against that repo. 7 | 8 | Install 9 | ------- 10 | 11 | You can install this package with `bower`. 12 | 13 | ### bower 14 | 15 | ```shell 16 | bower install angular-feature-toggle 17 | ``` 18 | -------------------------------------------------------------------------------- /assets/bower/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-feature-toggle", 3 | "version": "0.2.1", 4 | "description": "feature toggling capabilities for angular", 5 | "license": "ISC", 6 | "main": "./src/angular-feature-toggle.js", 7 | "dependencies": { 8 | "angular": ">= 1.0.8" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "package.json", 15 | "dist", 16 | "lib", 17 | "config", 18 | "example", 19 | "test", 20 | "Gruntfile.js", 21 | "grunt" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git@github.com:yairhaimo/angular-feature-toggle.git" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/bower/version.txt: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | -------------------------------------------------------------------------------- /example/NEWdashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | angular.module('app.components') 4 | .config(function($stateProvider, featureToggleProvider) { 5 | $stateProvider 6 | .state('dashboard', { 7 | url: '/dashboard', 8 | templateUrl: 'NEWdashboard/newDashboard.html', 9 | controller: 'NewDashboardController', 10 | controllerAs: 'dashboard', 11 | feature: 'dash', 12 | version: '^1' 13 | } 14 | ); 15 | }) 16 | .controller('NewDashboardController', function(featureToggle) { 17 | var dashboard = this; 18 | if (featureToggle.isVersion('dashboard', '^1.5')) { 19 | dashboard.message = 'this is the **SUPER NEW** dashboard (dashboard is ^1.5)'; 20 | } 21 | else { 22 | dashboard.message = 'this is the **NEW** dashboard (dashboard is ^1 but NOT ^1.5)'; 23 | } 24 | }) 25 | .directive('dashboardChart', 26 | function() { 27 | return { 28 | restrict: 'AE', 29 | template: '

shown only if admin is enabled

' 30 | }; 31 | } 32 | ) 33 | .directive('dashboardChart2', 34 | function() { 35 | return { 36 | restrict: 'AE', 37 | template: '

shown only if admin is ^1.2.0

' 38 | }; 39 | } 40 | ) 41 | 42 | })(); 43 | -------------------------------------------------------------------------------- /example/NEWdashboard/newDashboard.html: -------------------------------------------------------------------------------- 1 |

{{dashboard.message}}

2 | 3 | 4 | -------------------------------------------------------------------------------- /example/admin/admin.html: -------------------------------------------------------------------------------- 1 |

{{admin.message}}

2 | -------------------------------------------------------------------------------- /example/admin/admin.js: -------------------------------------------------------------------------------- 1 | angular.module('app.components') 2 | .config(function($stateProvider, featureToggleProvider) { 3 | $stateProvider 4 | .state('admin', { 5 | url: '/admin', 6 | templateUrl: 'admin/admin.html', 7 | controller: 'AdminController', 8 | controllerAs: 'admin', 9 | version: '*' 10 | } 11 | ); 12 | }) 13 | .controller('AdminController', function() { 14 | var admin = this; 15 | admin.message = 'this is the admin panel'; 16 | }); 17 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('app', ['ui.router', 'yh.featureToggle', 'app.components']) 5 | .run(function($rootScope, featureToggle) { 6 | $rootScope.featureToggle = featureToggle; 7 | }); 8 | 9 | 10 | // get config from server and manually bootstrap 11 | angular.element(document).ready(function() { 12 | fetch('/example/features.json').then(function(response) { 13 | response.json().then(function(features) { 14 | window.angularFeaturesConf = features; 15 | angular.bootstrap(document, ['app']); 16 | }) 17 | }); 18 | }); 19 | 20 | 21 | })(); 22 | -------------------------------------------------------------------------------- /example/components.module.js: -------------------------------------------------------------------------------- 1 | angular.module('app.components', ['ui.router', 'yh.featureToggle']); 2 | -------------------------------------------------------------------------------- /example/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |

{{dashboard.message}}

2 | -------------------------------------------------------------------------------- /example/dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | angular.module('app.components') 2 | .config(function($stateProvider, featureToggleProvider) { 3 | $stateProvider 4 | .state('dashboard', { 5 | url: '/dashboard', 6 | templateUrl: 'dashboard/dashboard.html', 7 | controller: 'DashboardController', 8 | controllerAs: 'dashboard', 9 | feature: 'dash', 10 | version: '^0' 11 | } 12 | ); 13 | }) 14 | .controller('DashboardController', function() { 15 | var dashboard = this; 16 | dashboard.message = 'this is the old dashboard (dashboard is ^0)'; 17 | }); 18 | -------------------------------------------------------------------------------- /example/features.json: -------------------------------------------------------------------------------- 1 | { 2 | "dash": "1.6.0", 3 | "admin": "1.0.2" 4 | } 5 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DASHBOARD 9 |
10 | Admin 11 | (shown only if admin is enabled) 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /grunt/clean.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: [{ 4 | dot: true, 5 | src: [ 6 | 'dist' 7 | ] 8 | }] 9 | }, 10 | bower: { 11 | files: [{ 12 | dot: true, 13 | src: [ 14 | 'build' 15 | ] 16 | }] 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /grunt/copy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: [{ 4 | expand: true, 5 | dot: true, 6 | cwd: 'src/', 7 | src: [ 8 | '*.js' 9 | ], 10 | dest: 'dist/' 11 | }] 12 | }, 13 | bower: { 14 | files: [{ 15 | expand: true, 16 | dot: true, 17 | cwd: 'src/', 18 | src: [ 19 | '*.js' 20 | ], 21 | dest: 'build/' 22 | }, { 23 | expand: true, 24 | dot: true, 25 | cwd: 'assets/bower/', 26 | src: [ 27 | '**' 28 | ], 29 | dest: 'build/' 30 | }] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /grunt/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | jshintrc: '.jshintrc' 4 | }, 5 | all: [ 6 | 'Gruntfile.js' 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /grunt/uglify.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: { 4 | 'dist/angular-feature-toggle.min.js': ['src/angular-feature-toggle.js'] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-feature-toggle", 3 | "version": "0.2.1", 4 | "description": "feature toggling capabilities for angular", 5 | "main": "dist/angular-feature-toggle.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mixpo/angular-feature-toggle.git" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "feature-toggle" 16 | ], 17 | "author": "yairhaimo", 18 | "contributors": [{ 19 | "name": "Hays Clark", "email": "hclark@mixpo.com" 20 | }], 21 | "license": "ISC", 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "grunt": "^0.4.5", 25 | "grunt-cli": "^0.1.13", 26 | "grunt-contrib-clean": "^0.7.0", 27 | "grunt-contrib-copy": "^0.8.2", 28 | "grunt-contrib-jshint": "^0.11.3", 29 | "grunt-contrib-uglify": "^0.11.0", 30 | "load-grunt-config": "^0.19.1", 31 | "load-grunt-tasks": "^3.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scripts/angular-feature-toggle/tag-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Tags a release 4 | # so that travis can do the actual release. 5 | 6 | echo "#############################################" 7 | echo "## Tag angular.feature.toggle for a release #" 8 | echo "#############################################" 9 | 10 | ARG_DEFS=( 11 | "--action=(prepare|publish)" 12 | "--commit-sha=(.*)" 13 | # the version number of the release. 14 | # e.g. 1.2.12 or 1.2.12-rc.1 15 | "--version-number=([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)" 16 | "--version-name=(.+)" 17 | ) 18 | 19 | function checkVersionNumber() { 20 | BRANCH_PATTERN=$(readJsonProp "package.json" "branchPattern") 21 | if [[ $VERSION_NUMBER != $BRANCH_PATTERN ]]; then 22 | echo "version-number needs to match $BRANCH_PATTERN on this branch" 23 | usage 24 | fi 25 | } 26 | 27 | function init { 28 | cd ../.. 29 | checkVersionNumber 30 | TAG_NAME="v$VERSION_NUMBER" 31 | } 32 | 33 | function prepare() { 34 | git tag "$TAG_NAME" -m "chore(release): $TAG_NAME codename($VERSION_NAME)" "$COMMIT_SHA" 35 | } 36 | 37 | function publish() { 38 | # push the tag to github 39 | git push origin $TAG_NAME 40 | } 41 | 42 | source $(dirname $0)/../utils.inc 43 | -------------------------------------------------------------------------------- /scripts/angular-feature-toggle/untag-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Untags a release. 4 | 5 | echo "###############################################" 6 | echo "## Untag angular.feature.toggle for a release #" 7 | echo "###############################################" 8 | 9 | ARG_DEFS=( 10 | "--action=(prepare|publish)" 11 | # the version number of the release. 12 | # e.g. 1.2.12 or 1.2.12-rc.1 13 | "--version-number=([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)" 14 | ) 15 | 16 | function init { 17 | TMP_DIR=$(resolveDir ../../.tmp) 18 | TAG_NAME="v$VERSION_NUMBER" 19 | } 20 | 21 | function prepare() { 22 | : 23 | } 24 | 25 | function publish() { 26 | # push the tag deletion to github 27 | tags=`git ls-remote --tags git@github.com:yairhaimo/angular-feature-toggle` 28 | if [[ $tags =~ "refs/tags/v$VERSION_NUMBER^" ]]; then 29 | echo "-- Creating dummy git repo for angular.js with origin remote" 30 | mkdir $TMP_DIR/empty-angular.js 31 | cd $TMP_DIR/empty-angular.js 32 | git init 33 | git remote add origin git@github.com:yairhaimo/angular-feature-toggle.git 34 | git push origin ":$TAG_NAME" 35 | else 36 | echo "-- Tag v$VERSION_NUMBER does not exist on remote. Moving on" 37 | fi 38 | } 39 | 40 | source $(dirname $0)/../utils.inc 41 | -------------------------------------------------------------------------------- /scripts/bower/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for updating the bower repos from current local build. 4 | 5 | echo "#################################" 6 | echo "#### Update bower ###############" 7 | echo "#################################" 8 | 9 | ARG_DEFS=( 10 | "--action=(prepare|publish)" 11 | ) 12 | 13 | function init { 14 | TMP_DIR=$(resolveDir ../../.tmp) 15 | BUILD_DIR=$(resolveDir ../../build) 16 | NEW_VERSION=$(cat $BUILD_DIR/version.txt) 17 | PROJECT_DIR=$(resolveDir ../..) 18 | # get the npm dist-tag from a custom property (distTag) in package.json 19 | DIST_TAG=$(readJsonProp "$PROJECT_DIR/package.json" "distTag") 20 | } 21 | 22 | 23 | function prepare { 24 | # 25 | # clone repos 26 | # 27 | for repo in "${REPOS[@]}" 28 | do 29 | echo "-- Cloning bower-$repo" 30 | git clone git@github.com:Mixpo/bower-$repo.git $TMP_DIR/bower-$repo 31 | done 32 | 33 | 34 | # 35 | # move the files from the build 36 | # 37 | 38 | for repo in "${REPOS[@]}" 39 | do 40 | if [ -f $BUILD_DIR/$repo.js ] # ignore i18l 41 | then 42 | echo "-- Updating files in bower-$repo" 43 | cp $BUILD_DIR/$repo.* $TMP_DIR/bower-$repo/ 44 | fi 45 | done 46 | 47 | 48 | # 49 | # Run local precommit script if there is one 50 | # 51 | for repo in "${REPOS[@]}" 52 | do 53 | if [ -f $TMP_DIR/bower-$repo/precommit.sh ] 54 | then 55 | echo "-- Running precommit.sh script for bower-$repo" 56 | cd $TMP_DIR/bower-$repo 57 | $TMP_DIR/bower-$repo/precommit.sh 58 | cd $SCRIPT_DIR 59 | fi 60 | done 61 | 62 | 63 | # 64 | # update bower.json 65 | # tag each repo 66 | # 67 | for repo in "${REPOS[@]}" 68 | do 69 | echo "-- Updating version in bower-$repo to $NEW_VERSION" 70 | cd $TMP_DIR/bower-$repo 71 | replaceJsonProp "bower.json" "version" ".*" "$NEW_VERSION" 72 | replaceJsonProp "bower.json" "angular.*" ".*" "$NEW_VERSION" 73 | # we don't have a package.json in our bower repos 74 | #replaceJsonProp "package.json" "version" ".*" "$NEW_VERSION" 75 | #replaceJsonProp "package.json" "angular.*" ".*" "$NEW_VERSION" 76 | 77 | git add -A 78 | 79 | echo "-- Committing and tagging bower-$repo" 80 | git commit -m "v$NEW_VERSION" 81 | git tag v$NEW_VERSION 82 | cd $SCRIPT_DIR 83 | done 84 | } 85 | 86 | function publish { 87 | for repo in "${REPOS[@]}" 88 | do 89 | echo "-- Pushing bower-$repo" 90 | cd $TMP_DIR/bower-$repo 91 | git push origin master 92 | git push origin v$NEW_VERSION 93 | 94 | # we don't have a package.json in our bower repos 95 | # don't publish every build to npm 96 | #if [ "${NEW_VERSION/+sha}" = "$NEW_VERSION" ] ; then 97 | #echo "-- Publishing to npm as $DIST_TAG" 98 | #npm publish --tag=$DIST_TAG 99 | #fi 100 | 101 | cd $SCRIPT_DIR 102 | done 103 | } 104 | 105 | source $(dirname $0)/repos.inc 106 | source $(dirname $0)/../utils.inc 107 | -------------------------------------------------------------------------------- /scripts/bower/repos.inc: -------------------------------------------------------------------------------- 1 | #!/bin/false 2 | # -*- mode: sh; -*- vim: set filetype=sh: 3 | 4 | REPOS=( 5 | angular-feature-toggle 6 | ) 7 | -------------------------------------------------------------------------------- /scripts/bower/unpublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for removing tags from the bower repos 4 | 5 | echo "#################################" 6 | echo "#### Untag bower ################" 7 | echo "#################################" 8 | 9 | ARG_DEFS=( 10 | "--action=(prepare|publish)" 11 | "--version-number=([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)" 12 | ) 13 | 14 | function init { 15 | TMP_DIR=$(resolveDir ../../.tmp) 16 | } 17 | 18 | function prepare { 19 | : 20 | } 21 | 22 | function publish { 23 | for repo in "${REPOS[@]}" 24 | do 25 | tags=`git ls-remote --tags git@github.com:yairhaimo/bower-$repo` 26 | if [[ $tags =~ "refs/tags/v$VERSION_NUMBER" ]]; then 27 | echo "-- Creating dummy git repo for bower-$repo with origin remote" 28 | mkdir $TMP_DIR/bower-$repo 29 | cd $TMP_DIR/bower-$repo 30 | git init 31 | git remote add origin git@github.com:yairhaimo/bower-$repo.git 32 | git push origin :v$VERSION_NUMBER 33 | echo "-- Deleting v$VERSION_NUMBER tag from bower-$repo" 34 | cd $SCRIPT_DIR 35 | else 36 | echo "-- No remote tag matching v$VERSION_NUMBER exists on bower-$repo" 37 | fi 38 | done 39 | } 40 | 41 | source $(dirname $0)/repos.inc 42 | source $(dirname $0)/../utils.inc 43 | -------------------------------------------------------------------------------- /scripts/jenkins/master.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "#################################" 4 | echo "#### Update master ##############" 5 | echo "#################################" 6 | 7 | ARG_DEFS=( 8 | "[--no-test=(true|false)]" 9 | ) 10 | 11 | function init { 12 | if [[ ! $VERBOSE ]]; then 13 | VERBOSE=false 14 | fi 15 | VERBOSE_ARG="--verbose=$VERBOSE" 16 | 17 | TMP_DIR=../../.tmp 18 | if [[ -d ${TMP_DIR} ]]; then 19 | printf '%s\n' "Removing ($TMP_DIR)" 20 | rm -rf ${TMP_DIR} 21 | fi 22 | mkdir ${TMP_DIR} 23 | } 24 | 25 | function build { 26 | cd ../.. 27 | 28 | if [[ $NO_TEST == "true" ]]; then 29 | npm install --color false 30 | #grunt ci-checks package --no-color 31 | grunt bower --no-color 32 | else 33 | echo "Add test call here." 34 | # ./jenkins_build.sh 35 | fi 36 | 37 | cd $SCRIPT_DIR 38 | } 39 | 40 | function phase { 41 | ACTION_ARG="--action=$1" 42 | 43 | #../code.angularjs.org/publish.sh $ACTION_ARG $VERBOSE_ARG 44 | ../bower/publish.sh $ACTION_ARG $VERBOSE_ARG 45 | } 46 | 47 | function run { 48 | build 49 | 50 | # First prepare all scripts (build, test, commit, tag, ...), 51 | # so we are sure everything is all right 52 | phase prepare 53 | # only then publish to github 54 | phase publish 55 | } 56 | 57 | source $(dirname $0)/../utils.inc 58 | -------------------------------------------------------------------------------- /scripts/jenkins/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # tags the current commit as a release and publishes all artifacts to 4 | # the different repositories. 5 | # Note: This will also works if the commit is in the past! 6 | 7 | echo "#################################" 8 | echo "#### cut release ############" 9 | echo "#################################" 10 | 11 | ARG_DEFS=( 12 | # require the git dryrun flag so the script can't be run without 13 | # thinking about this! 14 | "--git-push-dryrun=(true|false)" 15 | # The sha to release. Needs to be the same as HEAD. 16 | # given as parameter to double check. 17 | "--commit-sha=(.*)" 18 | # the version number of the release. 19 | # e.g. 1.2.12 or 1.2.12-rc.1 20 | "--version-number=([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)" 21 | # the codename of the release 22 | "--version-name=(.+)" 23 | ) 24 | 25 | function init { 26 | if [[ $(git rev-parse HEAD) != $(git rev-parse $COMMIT_SHA) ]]; then 27 | echo "HEAD is not at $COMMIT_SHA" 28 | usage 29 | fi 30 | 31 | if [[ ! $VERBOSE ]]; then 32 | VERBOSE=false 33 | fi 34 | VERBOSE_ARG="--verbose=$VERBOSE" 35 | } 36 | 37 | function build { 38 | cd ../.. 39 | 40 | npm install --color false 41 | grunt ci-checks package --no-color 42 | 43 | cd $SCRIPT_DIR 44 | } 45 | 46 | function phase { 47 | ACTION_ARG="--action=$1" 48 | ../angular.js/tag-release.sh $ACTION_ARG $VERBOSE_ARG\ 49 | --version-number=$VERSION_NUMBER --version-name=$VERSION_NAME\ 50 | --commit-sha=$COMMIT_SHA 51 | 52 | if [[ $1 == "prepare" ]]; then 53 | # The build requires the tag to be set already! 54 | build 55 | fi 56 | 57 | ../code.angularjs.org/publish.sh $ACTION_ARG $VERBOSE_ARG 58 | ../bower/publish.sh $ACTION_ARG $VERBOSE_ARG 59 | } 60 | 61 | function run { 62 | # First prepare all scripts (build, commit, tag, ...), 63 | # so we are sure everything is all right 64 | phase prepare 65 | # only then publish to github 66 | phase publish 67 | } 68 | 69 | source $(dirname $0)/../utils.inc 70 | -------------------------------------------------------------------------------- /scripts/jenkins/undo-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "#################################" 4 | echo "#### undo a release ############" 5 | echo "#################################" 6 | 7 | ARG_DEFS=( 8 | # require the git dryrun flag so the script can't be run without 9 | # thinking about this! 10 | "--git-push-dryrun=(true|false)" 11 | # the version number of the release. 12 | # e.g. 1.2.12 or 1.2.12-rc.1 13 | "--version-number=([0-9]+\.[0-9]+\.[0-9]+(-[a-z]+\.[0-9]+)?)" 14 | ) 15 | 16 | function init { 17 | if [[ ! $VERBOSE ]]; then 18 | VERBOSE=false 19 | fi 20 | VERBOSE_ARG="--verbose=$VERBOSE" 21 | } 22 | 23 | function phase { 24 | ACTION_ARG="--action=$1" 25 | VERSION_NUMBER_ARG="--version-number=$VERSION_NUMBER" 26 | ../angular.js/untag-release.sh $ACTION_ARG $VERBOSE_ARG\ 27 | --version-number=$VERSION_NUMBER 28 | 29 | ../code.angularjs.org/unpublish.sh $ACTION_ARG $VERSION_NUMBER_ARG $VERBOSE_ARG 30 | ../bower/unpublish.sh $ACTION_ARG $VERSION_NUMBER_ARG $VERBOSE_ARG 31 | } 32 | 33 | function run { 34 | # First prepare all scripts (build, commit, tag, ...), 35 | # so we are sure everything is all right 36 | phase prepare 37 | # only then publish to github 38 | phase publish 39 | } 40 | 41 | source $(dirname $0)/../utils.inc 42 | -------------------------------------------------------------------------------- /scripts/utils.inc: -------------------------------------------------------------------------------- 1 | # This file provides: 2 | # - a default control flow 3 | # * initializes the environment 4 | # * able to mock "git push" in your script and in all sub scripts 5 | # * call a function in your script based on the arguments 6 | # - named argument parsing and automatic generation of the "usage" for your script 7 | # - intercepting "git push" in your script and all sub scripts 8 | # - utility functions 9 | # 10 | # Usage: 11 | # - define the variable ARGS_DEF (see below) with the arguments for your script 12 | # - include this file using `source utils.inc` at the end of your script. 13 | # 14 | # Default control flow: 15 | # 0. Set the current directory to the directory of the script. By this 16 | # the script can be called from anywhere. 17 | # 1. Parse the named arguments 18 | # 2. If the parameter "git_push_dryrun" is set, all calls to `git push` in this script 19 | # or in child scripts will be intercepted so that the `--dry-run` and `--porcelain` is added 20 | # to show what the push would do but not actually do it. 21 | # 3. If the parameter "verbose" is set, the `-x` flag will be set in bash. 22 | # 4. The function "init" will be called if it exists 23 | # 5. If the parameter "action" is set, it will call the function with the name of that parameter. 24 | # Otherwise the function "run" will be called. 25 | # 26 | # Named Argument Parsing: 27 | # - The variable ARGS_DEF defines the valid command arguments 28 | # * Required args syntax: --paramName=paramRegex 29 | # * Optional args syntax: [--paramName=paramRegex] 30 | # * e.g. ARG_DEFS=("--required_param=(.+)" "[--optional_param=(.+)]") 31 | # - Checks that: 32 | # * all arguments match to an entry in ARGS_DEF 33 | # * all required arguments are present 34 | # * all arguments match their regex 35 | # - Afterwards, every parameter value will be stored in a variable 36 | # with the name of the parameter in upper case (with dash converted to underscore). 37 | # 38 | # Special arguments that are always available: 39 | # - "--action=.*": This parameter will be used to execute a function with that name when the 40 | # script is started 41 | # - "--git_push_dryrun=true": This will intercept all calls to `git push` in this script 42 | # or in child scripts so that the `--dry-run` and `--porcelain` is added 43 | # to show what the push would do but not actually do it. 44 | # - "--verbose=true": This will set the `-x` flag in bash so that all calls will be logged 45 | # 46 | # Utility functions: 47 | # - readJsonProp 48 | # - replaceJsonProp 49 | # - resolveDir 50 | # - getVar 51 | # - serVar 52 | # - isFunction 53 | 54 | # always stop on errors 55 | set -e 56 | 57 | function usage { 58 | echo "Usage: ${0} ${ARG_DEFS[@]}" 59 | exit 1 60 | } 61 | 62 | 63 | function parseArgs { 64 | local REQUIRED_ARG_NAMES=() 65 | 66 | # -- helper functions 67 | function varName { 68 | # everything to upper case and dash to underscore 69 | echo ${1//-/_} | tr '[:lower:]' '[:upper:]' 70 | } 71 | 72 | function readArgDefs { 73 | local ARG_DEF 74 | local AD_OPTIONAL 75 | local AD_NAME 76 | local AD_RE 77 | 78 | # -- helper functions 79 | function parseArgDef { 80 | local ARG_DEF_REGEX="(\[?)--([^=]+)=(.*)" 81 | if [[ ! $1 =~ $ARG_DEF_REGEX ]]; then 82 | echo "Internal error: arg def has wrong format: $ARG_DEF" 83 | exit 1 84 | fi 85 | AD_OPTIONAL="${BASH_REMATCH[1]}" 86 | AD_NAME="${BASH_REMATCH[2]}" 87 | AD_RE="${BASH_REMATCH[3]}" 88 | if [[ $AD_OPTIONAL ]]; then 89 | # Remove last bracket for optional args. 90 | # Can't put this into the ARG_DEF_REGEX somehow... 91 | AD_RE=${AD_RE%?} 92 | fi 93 | } 94 | 95 | # -- run 96 | for ARG_DEF in "${ARG_DEFS[@]}" 97 | do 98 | parseArgDef $ARG_DEF 99 | 100 | local AD_NAME_UPPER=$(varName $AD_NAME) 101 | setVar "${AD_NAME_UPPER}_OPTIONAL" "$AD_OPTIONAL" 102 | setVar "${AD_NAME_UPPER}_RE" "$AD_RE" 103 | if [[ ! $AD_OPTIONAL ]]; then 104 | REQUIRED_ARG_NAMES+=($AD_NAME) 105 | fi 106 | done 107 | } 108 | 109 | function readAndValidateArgs { 110 | local ARG_NAME 111 | local ARG_VALUE 112 | local ARG_NAME_UPPER 113 | 114 | # -- helper functions 115 | function parseArg { 116 | local ARG_REGEX="--([^=]+)=?(.*)" 117 | 118 | if [[ ! $1 =~ $ARG_REGEX ]]; then 119 | echo "Can't parse argument $i" 120 | usage 121 | fi 122 | 123 | ARG_NAME="${BASH_REMATCH[1]}" 124 | ARG_VALUE="${BASH_REMATCH[2]}" 125 | ARG_NAME_UPPER=$(varName $ARG_NAME) 126 | } 127 | 128 | function validateArg { 129 | local AD_RE=$(getVar ${ARG_NAME_UPPER}_RE) 130 | 131 | if [[ ! $AD_RE ]]; then 132 | echo "Unknown option: $ARG_NAME" 133 | usage 134 | fi 135 | 136 | if [[ ! $ARG_VALUE =~ ^${AD_RE}$ ]]; then 137 | echo "Wrong format: $ARG_NAME" 138 | usage; 139 | fi 140 | 141 | # validate that the "action" option points to a valid function 142 | if [[ $ARG_NAME == "action" ]] && ! isFunction $ARG_VALUE; then 143 | echo "No action $ARG_VALUE defined in this script" 144 | usage; 145 | fi 146 | } 147 | 148 | # -- run 149 | for i in "$@" 150 | do 151 | parseArg $i 152 | validateArg 153 | setVar "${ARG_NAME_UPPER}" "$ARG_VALUE" 154 | done 155 | } 156 | 157 | function checkMissingArgs { 158 | local ARG_NAME 159 | for ARG_NAME in "${REQUIRED_ARG_NAMES[@]}" 160 | do 161 | ARG_VALUE=$(getVar $(varName $ARG_NAME)) 162 | 163 | if [[ ! $ARG_VALUE ]]; then 164 | echo "Missing: $ARG_NAME" 165 | usage; 166 | fi 167 | done 168 | } 169 | 170 | # -- run 171 | readArgDefs 172 | readAndValidateArgs "$@" 173 | checkMissingArgs 174 | 175 | } 176 | 177 | # getVar(varName) 178 | function getVar { 179 | echo ${!1} 180 | } 181 | 182 | # setVar(varName, varValue) 183 | function setVar { 184 | eval "$1=\"$2\"" 185 | } 186 | 187 | # isFunction(name) 188 | # - to be used in an if, so return 0 if successful and 1 if not! 189 | function isFunction { 190 | if [[ $(type -t $1) == "function" ]]; then 191 | return 0 192 | else 193 | return 1 194 | fi 195 | } 196 | 197 | # readJsonProp(jsonFile, property) 198 | # - restriction: property needs to be on a single line! 199 | function readJsonProp { 200 | echo $(sed -En 's/.*"'$2'"[ ]*:[ ]*"(.*)".*/\1/p' $1) 201 | } 202 | 203 | # replaceJsonProp(jsonFile, propertyRegex, valueRegex, replacePattern) 204 | # - note: propertyRegex will be automatically placed into a 205 | # capturing group! -> all other groups start at index 2! 206 | function replaceJsonProp { 207 | replaceInFile $1 '"('$2')"[ ]*:[ ]*"'$3'"' '"\1": "'$4'"' 208 | } 209 | 210 | # replaceInFile(file, findPattern, replacePattern) 211 | function replaceInFile { 212 | sed -i .tmp -E "s/$2/$3/" $1 213 | rm $1.tmp 214 | } 215 | 216 | # resolveDir(relativeDir) 217 | # - resolves a directory relative to the current script 218 | function resolveDir { 219 | echo $(cd $SCRIPT_DIR; cd $1; pwd) 220 | } 221 | 222 | function git_push_dryrun_proxy { 223 | echo "## git push dryrun proxy enabled!" 224 | export ORIGIN_GIT=$(which git) 225 | 226 | function git { 227 | local ARGS=("$@") 228 | local RC 229 | if [[ $1 == "push" ]]; then 230 | ARGS+=("--dry-run" "--porcelain") 231 | echo "####### START GIT PUSH DRYRUN #######" 232 | echo "${ARGS[@]}" 233 | fi 234 | if [[ $1 == "commit" ]]; then 235 | echo "${ARGS[@]}" 236 | fi 237 | $ORIGIN_GIT "${ARGS[@]}" 238 | RC=$? 239 | if [[ $1 == "push" ]]; then 240 | echo "####### END GIT PUSH DRYRUN #######" 241 | fi 242 | return $RC 243 | } 244 | 245 | export -f git 246 | } 247 | 248 | function main { 249 | # normalize the working dir to the directory of the script 250 | cd $(dirname $0);SCRIPT_DIR=$(pwd) 251 | 252 | ARG_DEFS+=("[--git-push-dryrun=(true|false)]" "[--verbose=(true|false)]") 253 | parseArgs "$@" 254 | 255 | # --git_push_dryrun argument 256 | if [[ $GIT_PUSH_DRYRUN == "true" ]]; then 257 | git_push_dryrun_proxy 258 | fi 259 | 260 | # --verbose argument 261 | if [[ $VERBOSE == "true" ]]; then 262 | set -x 263 | fi 264 | 265 | if isFunction init; then 266 | init "$@" 267 | fi 268 | 269 | # jump to the function denoted by the --action argument, 270 | # otherwise call the "run" function 271 | if [[ $ACTION ]]; then 272 | $ACTION "$@" 273 | else 274 | run "$@" 275 | fi 276 | } 277 | 278 | 279 | main "$@" 280 | -------------------------------------------------------------------------------- /src/angular-feature-toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create AngularJS semver provider. 3 | */ 4 | var semver = require('semver'); 5 | (function (semver, angular) { 6 | 'use strict'; 7 | 8 | angular 9 | .module('semver', []) 10 | .provider('semver', [function () { 11 | return buildWrapper(semver); 12 | }]); 13 | 14 | /** 15 | * Dynamically builds a semver wrapper object that 16 | * meets AngularJS provider requirements. 17 | * 18 | * @param semver 19 | * @returns {{$get: Function}} 20 | */ 21 | function buildWrapper(semver) { 22 | var wrapper = { 23 | $get: function () { 24 | } 25 | }; 26 | for (var key in semver) { 27 | wrapper[key] = semver[key]; 28 | } 29 | return wrapper; 30 | } 31 | })(semver, window.angular); 32 | 33 | 34 | /** 35 | * Create AngularJS featureToggle provider. 36 | */ 37 | (function (window, angular) { 38 | 'use strict'; 39 | 40 | var ng = angular.module('angularFeatureToggle', ['semver']); 41 | ng.config(['featureToggleProvider', '$injector', function (featureToggleProvider, $injector) { 42 | initFeatures(featureToggleProvider); 43 | overrideUIRouterStateFn($injector, featureToggleProvider); 44 | 45 | /** 46 | * 47 | * @param featureToggleProvider 48 | */ 49 | /* @ngInject */ 50 | function initFeatures(featureToggleProvider) { 51 | if (window.angularFeaturesConf) { 52 | featureToggleProvider.init(window.angularFeaturesConf); 53 | } 54 | else { 55 | window.console.warn('could not detect features'); 56 | } 57 | } 58 | 59 | /** 60 | * config ui router 61 | * 62 | * @param $injector 63 | * @param featureToggleProvider 64 | */ 65 | /* @ngInject */ 66 | function overrideUIRouterStateFn($injector, featureToggleProvider) { 67 | try { 68 | var $stateProvider = $injector.get('$stateProvider'); 69 | 70 | // the app uses ui.router, configure it 71 | var oldStateFn = $stateProvider.state; 72 | $stateProvider.state = function (name, conf) { 73 | // enable state if feature version is satisfied or undefined 74 | if ((conf.version === undefined) || (featureToggleProvider.isVersion(conf.feature || name, conf.version))) { 75 | try { 76 | return oldStateFn.call($stateProvider, name, conf); 77 | } 78 | catch (e) { 79 | window.console && window.console.warn('state ' + name + ' is already defined'); // jshint ignore:line 80 | return $stateProvider; 81 | } 82 | } 83 | // else return stateProvider for further state declaration chaining 84 | else { 85 | return $stateProvider; 86 | } 87 | }; 88 | } catch (e) { 89 | // the app doesnt use ui.router - silent failure 90 | } 91 | } 92 | }]); 93 | 94 | ng.provider('featureToggle', ['semverProvider', function (semverProvider) { 95 | /* jshint validthis:true */ 96 | var semver = semverProvider; 97 | 98 | // define Feature model 99 | function Feature(version) { 100 | this.version = version; 101 | } 102 | 103 | Feature.prototype.isVersion = function (versionToCheck) { 104 | return semver.satisfies(this.version, versionToCheck); 105 | }; 106 | 107 | Feature.prototype.isEnabled = function () { 108 | return semver.satisfies(this.version, '*'); 109 | }; 110 | 111 | ///////////////// 112 | var features = {}; 113 | 114 | var service = { 115 | init: init, 116 | features: features, 117 | isVersion: isVersion, 118 | isEnabled: isEnabled, 119 | $get: featureToggleFactory 120 | }; 121 | return service; 122 | 123 | /** 124 | * 125 | * @param featuresObj 126 | */ 127 | function init(featuresObj) { 128 | features = featuresObj; 129 | } 130 | 131 | /** 132 | * 133 | * @param feature 134 | * @param versionToCheck 135 | * @returns {*} 136 | */ 137 | function isVersion(feature, versionToCheck) { 138 | return semver.satisfies(features[feature], versionToCheck); 139 | } 140 | 141 | /** 142 | * 143 | * @param feature 144 | * @returns {boolean} 145 | */ 146 | function isEnabled(feature) { 147 | return !!features[feature]; 148 | } 149 | 150 | /** 151 | * 152 | * @returns {{isVersion: isVersion, isEnabled: isEnabled}} 153 | */ 154 | function featureToggleFactory() { 155 | return { 156 | isVersion: isVersion, 157 | isEnabled: isEnabled 158 | }; 159 | } 160 | }]); 161 | 162 | ng.directive('showIfFeature', ['featureToggle', function (featureToggle) { 163 | var ddo = { 164 | restrict: 'AE', 165 | transclude: 'element', 166 | terminal: true, 167 | priority: 999, 168 | link: link 169 | }; 170 | 171 | return ddo; 172 | 173 | /** 174 | * 175 | * @param scope 176 | * @param element 177 | * @param attrs 178 | * @param ctrl 179 | * @param $transclude 180 | */ 181 | function link(scope, element, attrs, ctrl, $transclude) { 182 | var featureEl, childScope, featureName; 183 | var featureVersion = '*'; 184 | var args = attrs.showIfFeature.split(/\s+/); 185 | featureName = args[0]; 186 | if (args.length > 1) { 187 | featureVersion = args[1]; 188 | } 189 | 190 | if (featureToggle.isVersion(featureName, featureVersion)) { 191 | childScope = scope.$new(); 192 | $transclude(childScope, function (clone) { 193 | featureEl = clone; 194 | element.after(featureEl).remove(); 195 | }); 196 | } else { 197 | if (childScope) { 198 | childScope.$destroy(); 199 | childScope = null; 200 | } 201 | if (featureEl) { 202 | featureEl.after(element).remove(); 203 | featureEl = null; 204 | } 205 | } 206 | } 207 | }]); 208 | 209 | ng.directive('hideIfFeature', ['featureToggle', function hideIfFeature(featureToggle) { 210 | var ddo = { 211 | restrict: 'AE', 212 | transclude: 'element', 213 | terminal: true, 214 | priority: 999, 215 | link: link 216 | }; 217 | 218 | return ddo; 219 | 220 | /** 221 | * 222 | * @param scope 223 | * @param element 224 | * @param attrs 225 | * @param ctrl 226 | * @param $transclude 227 | */ 228 | function link(scope, element, attrs, ctrl, $transclude) { 229 | var featureEl, childScope, featureName; 230 | var featureVersion = '*'; 231 | var args = attrs.hideIfFeature.split(/\s+/); 232 | featureName = args[0]; 233 | if (args.length > 1) { 234 | featureVersion = args[1]; 235 | } 236 | 237 | if (featureToggle.isVersion(featureName, featureVersion)) { 238 | if (childScope) { 239 | childScope.$destroy(); 240 | childScope = null; 241 | } 242 | if (featureEl) { 243 | featureEl.after(element).remove(); 244 | featureEl = null; 245 | } 246 | } else { 247 | childScope = scope.$new(); 248 | $transclude(childScope, function (clone) { 249 | featureEl = clone; 250 | element.after(featureEl).remove(); 251 | }); 252 | } 253 | } 254 | }]); 255 | 256 | })(window, window.angular); 257 | --------------------------------------------------------------------------------