From 9d1d8374e17794b0769592ee3e123395e7f8b474 Mon Sep 17 00:00:00 2001 From: Joe Fiorini Date: Tue, 12 May 2015 09:46:05 -0400 Subject: [PATCH 1/3] Improve how we pass data when changing URLs This commit adds a couple helpful features to make it easier for us to reference routes, update URLs (both at development time and in the browser at runtime) and pass data between routes. See the UI docs for the specifics on how these features work. --- awx/ui/static/js/app.js | 5 + awx/ui/static/js/docs.js | 1 + .../route-extensions/link-to.directive.js | 74 ++++++++++ .../route-extensions/lookup-route-url.js | 43 ++++++ .../static/js/shared/route-extensions/main.js | 28 ++++ .../route-extensions/model-listener.config.js | 18 +++ .../route-extensions/transition-to.factory.js | 131 ++++++++++++++++++ 7 files changed, 300 insertions(+) create mode 100644 awx/ui/static/js/shared/route-extensions/link-to.directive.js create mode 100644 awx/ui/static/js/shared/route-extensions/lookup-route-url.js create mode 100644 awx/ui/static/js/shared/route-extensions/main.js create mode 100644 awx/ui/static/js/shared/route-extensions/model-listener.config.js create mode 100644 awx/ui/static/js/shared/route-extensions/transition-to.factory.js diff --git a/awx/ui/static/js/app.js b/awx/ui/static/js/app.js index c3f37e1528..f5ca57ab69 100644 --- a/awx/ui/static/js/app.js +++ b/awx/ui/static/js/app.js @@ -33,6 +33,8 @@ import {PortalController} from 'tower/controllers/Portal'; import dataServices from 'tower/services/_data-services'; import dashboardGraphs from 'tower/directives/_dashboard-graphs'; +import routeExtensions from 'tower/shared/route-extensions/main'; + import {JobDetailController} from 'tower/controllers/JobDetail'; import {JobStdoutController} from 'tower/controllers/JobStdout'; import {JobTemplatesList, JobTemplatesAdd, JobTemplatesEdit} from 'tower/controllers/JobTemplates'; @@ -71,6 +73,7 @@ var tower = angular.module('Tower', [ 'RestServices', dataServices.name, dashboardGraphs.name, + routeExtensions.name, 'AuthService', 'Utilities', 'LicenseHelper', @@ -184,6 +187,7 @@ var tower = angular.module('Tower', [ $routeProvider. when('/jobs', { + name: 'jobs', templateUrl: urlPrefix + 'partials/jobs.html', controller: JobsListController, resolve: { @@ -204,6 +208,7 @@ var tower = angular.module('Tower', [ }). when('/jobs/:id', { + name: 'jobDetail', templateUrl: urlPrefix + 'partials/job_detail.html', controller: JobDetailController, resolve: { diff --git a/awx/ui/static/js/docs.js b/awx/ui/static/js/docs.js index 8f51c66bf2..6011068888 100644 --- a/awx/ui/static/js/docs.js +++ b/awx/ui/static/js/docs.js @@ -1 +1,2 @@ import 'tower/shared/multi-select-list/main.js'; +import 'tower/shared/route-extensions/main.js'; diff --git a/awx/ui/static/js/shared/route-extensions/link-to.directive.js b/awx/ui/static/js/shared/route-extensions/link-to.directive.js new file mode 100644 index 0000000000..77d0e31f04 --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/link-to.directive.js @@ -0,0 +1,74 @@ +import {lookupRouteUrl} from './lookup-route-url'; + +/** + * + * @ngdoc directive + * @name routeExtensions.directive:linkTo + * @desription + * The `linkTo` directive looks up a route's URL and generates a link to that route. When a user + * clicks the link, this directive calls the `transitionTo` factory to send them to the given + * URL. For accessibility and fallback purposes, it also sets the `href` attribute of the link + * to the path. + * + * Note that in this example the model object uses a key that matches up with the route parameteer + * name in the route url (in this case `:id`). + * + * **N.B.** The below example currently won't run. It's included to show an example of using + * the `linkTo` directive within code. In order for this to run, we will need to run + * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). + * + * @example + * + + + angular.module('simpleRouteExample', ['ngRoute', 'routeExtensions']) + .config(['$routeProvider', function($route) { + $route.when('/posts/:id', { + name: 'post', + template: '

{{post.title}}

{{post.body}}

', + controller: 'post' + }); + }]).controller('post', function($scope) { + }); +
+ +
+ + {{featuredPost.title}} + +
+
+
+ * + */ +export default + [ '$route', + 'transitionTo', + function($routeProvider, transitionTo) { + return { + restrict: 'E', + transclude: true, + template: '', + scope: { + routeName: '@route', + model: '&' + }, + link: function(scope, element, attrs) { + + var model = scope.$eval(scope.model); + scope.url = lookupRouteUrl(scope.routeName, $routeProvider.routes, model); + + element.find('[data-transition-to]').on('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + transitionTo(scope.routeName, model); + }); + + } + }; + } + ] diff --git a/awx/ui/static/js/shared/route-extensions/lookup-route-url.js b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js new file mode 100644 index 0000000000..7b9c6cc50d --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js @@ -0,0 +1,43 @@ +export function lookupRouteUrl(name, routes, models) { + var route = _.find(routes, {name: name}); + + if (angular.isUndefined(route)) { + throw "Unknown route " + name; + } + + var routeUrl = route.originalPath; + + if (!angular.isUndefined(models) && angular.isObject(models)) { + var match = routeUrl.match(route.regexp); + var keyMatchers = match.slice(1); + + routeUrl = + keyMatchers.reduce(function(url, keyMatcher) { + var value; + var key = keyMatcher.replace(/^:/, ''); + + var model = models[key]; + + if (angular.isArray(model)) { + value = _.compact(_.pluck(model, key)); + + if (_.isEmpty(value)) { + value = _.pluck(model, 'id'); + } + + value = value.join(',') + } else if (angular.isObject(model)) { + value = model[key]; + + if (_.isEmpty(value)) { + value = model.id; + } + } + + return url.replace(keyMatcher, value); + }, routeUrl); + + } + + return routeUrl; +} diff --git a/awx/ui/static/js/shared/route-extensions/main.js b/awx/ui/static/js/shared/route-extensions/main.js new file mode 100644 index 0000000000..3f2c85d267 --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/main.js @@ -0,0 +1,28 @@ +import linkTo from './link-to.filter'; +import transitionTo from './transition-to.factory'; +import modelListener from './model-listener.config'; + +/** + * @ngdoc overview + * @name routeExtensions + * @description + * + * # routeExtensions + * + * Adds a couple useful features to ngRoute: + * - Adds a `name` property to route objects; used to identify the route in transitions & links + * - Adds the ability to pass model data when clicking a link that goes to a route + * - Adds a directive that generates a route's URL from the route name & given models + * - Adds the ability to specify models in route resolvers + * + * ## Usage + * + * If you need to generate a link to a route, then use the {@link routeExtensions.directive:linkTo `linkTo directive`}. If you need to transition to a route in JavaScript code, then use the {@link routeExtensions.factory:transitionTo `transitionTo service`}. + * +*/ +export default + angular.module('routeExtensions', + ['ngRoute']) + .factory('transitionTo', transitionTo) + .run(modelListener) + .directive('linkTo', linkTo); diff --git a/awx/ui/static/js/shared/route-extensions/model-listener.config.js b/awx/ui/static/js/shared/route-extensions/model-listener.config.js new file mode 100644 index 0000000000..d1b0b22357 --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/model-listener.config.js @@ -0,0 +1,18 @@ +export default + [ '$rootScope', + '$routeParams', + function($rootScope, $routeParams) { + var offRouteChangeStart = + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { + if (angular.isUndefined(newRoute.model)) { + var keys = Object.keys(newRoute.params); + var models = keys.reduce(function(model, key) { + model[key] = newRoute.locals[key]; + return model; + }, {}); + + $routeParams.model = models; + } + }); + } + ] diff --git a/awx/ui/static/js/shared/route-extensions/transition-to.factory.js b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js new file mode 100644 index 0000000000..71e68216ad --- /dev/null +++ b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js @@ -0,0 +1,131 @@ +import {lookupRouteUrl} from './lookup-route-url'; + +/** + * @ngdoc service + * @name routeExtensions.service:transitionTo + * @description + * The `transitionTo` service generates a URL given a route name and model parameters, then + * updates the browser's URL via `$location.path`. Use this in situations where you cannot + * use the `linkTo` directive, for example to redirect the user after saving an object. + * + * @param {string} routeName The name of the route whose URL you want to redirect to (corresponds + * name property of route) + * @param {object} model The model you want to use to generate the URL and be passed to the new + * route. This object follows a strict key/value naming convention where + * the keys match the parameters listed in the route's URL. For example, + * a URL of `/posts/:id` would require a model object like: `{ id: post }`, + * where `post` is the object you want to pass to the new route. + * + * **N.B.** The below example currently won't run. It's included to show an example of using + * the `transitionTo` function within code. In order for this to run, we will need to run + * the code in an iframe (using something like `dgeni` instead of `grunt-ngdocs`). + * + * @example + * + + + angular.module('transitionToExample', + ['ngRoute', + 'routeExtensions' + ]) + .config(function($routeProvider, $locationProvider) { + $routeProvider + .when('/post/:id', + { name: 'post', + template: '

{{post.title}}

{{post.body}}

' + }); + + $locationProvider.html5Mode(true); + $locationProvider.hashPrefix('!'); + }) + .controller('post', ['$scope', function($scope) { + + }]) + .controller('postForm', ['$scope', 'transitionTo', function($scope, transitionTo) { + $scope.post = { + id: 1, + title: 'A post', + body: 'Some text' + }; + + $scope.savePost = function() { + transitionTo('post', { id: $scope.post }); + } + }]); +
+ +
+ Edit Post + + + + +
+
+ +
+ + */ + +function safeApply(fn, $rootScope) { + var currentPhase = $rootScope.$$phase; + + if (currentPhase === '$apply' || currentPhase === '$digest') { + fn(); + } else { + $rootScope.$apply(fn); + } +} + +export default + [ '$location', + '$rootScope', + '$route', + '$q', + function($location, $rootScope, $route, $q) { + return function(routeName, model) { + var deferred = $q.defer(); + var url = lookupRouteUrl(routeName, $route.routes, model); + + var offRouteChangeStart = + $rootScope.$on('$routeChangeStart', function(e, newRoute) { + if (newRoute.$$route.name === routeName) { + deferred.resolve(newRoute, model); + newRoute.params.model = model; + } + + offRouteChangeStart(); + }); + + var offRouteChangeSuccess = + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { + if (newRoute.$$route.name === routeName) { + deferred.resolve(newRoute); + } + + offRouteChangeSuccess(); + }); + + var offRouteChangeError = + $rootScope.$on('$routeChangeError', function(e, newRoute, prevRoute, rejection) { + if (newRoute.$$route.name === routeName) { + deferred.reject(newRoute, previousRoute, rejection); + } + + offRouteChangeError(); + }); + + safeApply(function() { + $location.path(url); + }, $rootScope); + + return deferred; + } + } + ] From 8828ca089e568c63dc6aed0c4443627ee278e13c Mon Sep 17 00:00:00 2001 From: Joe Fiorini Date: Wed, 13 May 2015 09:36:31 -0400 Subject: [PATCH 2/3] jshint fixes --- .../route-extensions/link-to.directive.js | 4 ++-- .../route-extensions/lookup-route-url.js | 2 +- .../route-extensions/model-listener.config.js | 23 +++++++++---------- .../route-extensions/transition-to.factory.js | 6 ++--- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/awx/ui/static/js/shared/route-extensions/link-to.directive.js b/awx/ui/static/js/shared/route-extensions/link-to.directive.js index 77d0e31f04..b79ffaef99 100644 --- a/awx/ui/static/js/shared/route-extensions/link-to.directive.js +++ b/awx/ui/static/js/shared/route-extensions/link-to.directive.js @@ -57,7 +57,7 @@ export default routeName: '@route', model: '&' }, - link: function(scope, element, attrs) { + link: function(scope, element) { var model = scope.$eval(scope.model); scope.url = lookupRouteUrl(scope.routeName, $routeProvider.routes, model); @@ -71,4 +71,4 @@ export default } }; } - ] + ]; diff --git a/awx/ui/static/js/shared/route-extensions/lookup-route-url.js b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js index 7b9c6cc50d..54e505a2ae 100644 --- a/awx/ui/static/js/shared/route-extensions/lookup-route-url.js +++ b/awx/ui/static/js/shared/route-extensions/lookup-route-url.js @@ -25,7 +25,7 @@ export function lookupRouteUrl(name, routes, models) { value = _.pluck(model, 'id'); } - value = value.join(',') + value = value.join(','); } else if (angular.isObject(model)) { value = model[key]; diff --git a/awx/ui/static/js/shared/route-extensions/model-listener.config.js b/awx/ui/static/js/shared/route-extensions/model-listener.config.js index d1b0b22357..7f7112f08a 100644 --- a/awx/ui/static/js/shared/route-extensions/model-listener.config.js +++ b/awx/ui/static/js/shared/route-extensions/model-listener.config.js @@ -2,17 +2,16 @@ export default [ '$rootScope', '$routeParams', function($rootScope, $routeParams) { - var offRouteChangeStart = - $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { - if (angular.isUndefined(newRoute.model)) { - var keys = Object.keys(newRoute.params); - var models = keys.reduce(function(model, key) { - model[key] = newRoute.locals[key]; - return model; - }, {}); + $rootScope.$on('$routeChangeSuccess', function(e, newRoute) { + if (angular.isUndefined(newRoute.model)) { + var keys = Object.keys(newRoute.params); + var models = keys.reduce(function(model, key) { + model[key] = newRoute.locals[key]; + return model; + }, {}); - $routeParams.model = models; - } - }); + $routeParams.model = models; + } + }); } - ] + ]; diff --git a/awx/ui/static/js/shared/route-extensions/transition-to.factory.js b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js index 71e68216ad..6acee4326f 100644 --- a/awx/ui/static/js/shared/route-extensions/transition-to.factory.js +++ b/awx/ui/static/js/shared/route-extensions/transition-to.factory.js @@ -113,7 +113,7 @@ export default }); var offRouteChangeError = - $rootScope.$on('$routeChangeError', function(e, newRoute, prevRoute, rejection) { + $rootScope.$on('$routeChangeError', function(e, newRoute, previousRoute, rejection) { if (newRoute.$$route.name === routeName) { deferred.reject(newRoute, previousRoute, rejection); } @@ -126,6 +126,6 @@ export default }, $rootScope); return deferred; - } + }; } - ] + ]; From 5afd5f5fad6ead29d3dd1c1eaee9ac30a44ea329 Mon Sep 17 00:00:00 2001 From: Joe Fiorini Date: Tue, 12 May 2015 17:04:28 -0400 Subject: [PATCH 3/3] Fix issue in route-extensions --- awx/ui/static/js/shared/route-extensions/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/static/js/shared/route-extensions/main.js b/awx/ui/static/js/shared/route-extensions/main.js index 3f2c85d267..f041598bd9 100644 --- a/awx/ui/static/js/shared/route-extensions/main.js +++ b/awx/ui/static/js/shared/route-extensions/main.js @@ -1,4 +1,4 @@ -import linkTo from './link-to.filter'; +import linkTo from './link-to.directive'; import transitionTo from './transition-to.factory'; import modelListener from './model-listener.config';