forked from civicrm/civicrm-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request civicrm#11328 from colemanw/CRM-21483
CRM-21483 - Move anguar module crmRouteBinder to core
- Loading branch information
Showing
4 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
// This file declares an Angular module which can be autoloaded | ||
// in CiviCRM. See also: | ||
// http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules | ||
|
||
return array( | ||
'ext' => 'civicrm', | ||
'js' => array('ang/crmRouteBinder.js'), | ||
'css' => array(), | ||
'partials' => array(), | ||
'requires' => array('ngRoute'), | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
(function(angular, $, _) { | ||
angular.module('crmRouteBinder', CRM.angRequires('crmRouteBinder')); | ||
|
||
// While processing a change from the $watch()'d data, we set the "pendingUpdates" flag | ||
// so that automated URL changes don't cause a reload. | ||
var pendingUpdates = null, activeTimer = null, registered = false, ignorable = {}; | ||
|
||
function registerGlobalListener($injector) { | ||
if (registered) return; | ||
registered = true; | ||
|
||
$injector.get('$rootScope').$on('$routeUpdate', function () { | ||
// Only reload if someone else -- like the user or an <a href> -- changed URL. | ||
if (null === pendingUpdates) { | ||
$injector.get('$route').reload(); | ||
} | ||
}); | ||
} | ||
|
||
var formats = { | ||
json: { | ||
watcher: '$watchCollection', | ||
decode: angular.fromJson, | ||
encode: angular.toJson, | ||
default: {} | ||
}, | ||
raw: { | ||
watcher: '$watch', | ||
decode: function(v) { return v; }, | ||
encode: function(v) { return v; }, | ||
default: '' | ||
}, | ||
int: { | ||
watcher: '$watch', | ||
decode: function(v) { return parseInt(v); }, | ||
encode: function(v) { return v; }, | ||
default: 0 | ||
}, | ||
bool: { | ||
watcher: '$watch', | ||
decode: function(v) { return v === '1'; }, | ||
encode: function(v) { return v ? '1' : '0'; }, | ||
default: false | ||
} | ||
}; | ||
|
||
angular.module('crmRouteBinder').config(function ($provide) { | ||
$provide.decorator('$rootScope', function ($delegate, $injector, $parse) { | ||
Object.getPrototypeOf($delegate).$bindToRoute = function (options) { | ||
registerGlobalListener($injector); | ||
|
||
options.format = options.format || 'json'; | ||
var fmt = formats[options.format]; | ||
if (options.default === undefined) { | ||
options.default = fmt.default; | ||
} | ||
var _scope = this; | ||
|
||
var $route = $injector.get('$route'), $timeout = $injector.get('$timeout'); | ||
|
||
var value; | ||
if (options.param in $route.current.params) { | ||
value = fmt.decode($route.current.params[options.param]); | ||
} | ||
else { | ||
value = _.isObject(options.default) ? angular.extend({}, options.default) : options.default; | ||
ignorable[options.param] = fmt.encode(options.default); | ||
} | ||
$parse(options.expr).assign(_scope, value); | ||
|
||
// Keep the URL bar up-to-date. | ||
_scope[fmt.watcher](options.expr, function (newValue) { | ||
var encValue = fmt.encode(newValue); | ||
if ($route.current.params[options.param] === encValue) return; | ||
|
||
pendingUpdates = pendingUpdates || {}; | ||
pendingUpdates[options.param] = encValue; | ||
var p = angular.extend({}, $route.current.params, pendingUpdates); | ||
angular.forEach(ignorable, function(v,k){ if (p[k] === v) delete p[k]; }); | ||
$route.updateParams(p); | ||
|
||
if (activeTimer) $timeout.cancel(activeTimer); | ||
activeTimer = $timeout(function () { | ||
pendingUpdates = null; | ||
activeTimer = null; | ||
ignorable = {}; | ||
}, 50); | ||
}); | ||
}; | ||
|
||
return $delegate; | ||
}); | ||
}); | ||
|
||
})(angular, CRM.$, CRM._); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# crmRouteBinder | ||
|
||
Live-update the URL to stay in sync with controller data. | ||
|
||
## Example | ||
|
||
```js | ||
angular.module('sandbox').config(function($routeProvider) { | ||
$routeProvider.when('/example-route', { | ||
reloadOnSearch: false, | ||
template: '<input ng-model="filters.foo" />', | ||
controller: function($scope) { | ||
$scope.$bindToRoute({ | ||
param: 'f', | ||
expr: 'filters', | ||
default: {foo: 'default-value'} | ||
}); | ||
} | ||
}); | ||
}); | ||
``` | ||
|
||
Things to try out: | ||
|
||
* Navigate to `#/example-route`. Observe that the URL automatically | ||
updates to `#/example-route?f={"foo":"default-value"}`. | ||
* Edit the content in the `<input>` field. Observe that the URL changes. | ||
* Initiate a change in the browser -- by editing the URL bar or pressing | ||
the "Back" button. The page should refresh. | ||
|
||
## Functions | ||
|
||
* `$scope.$bindToRoute(options)` | ||
* The `options` object should contain keys: | ||
* `expr` (string): The name of a scoped variable to sync. | ||
* `param` (string): The name of a query-parameter to sync. (If the `param` is included in the URL, it will initialize the expr.) | ||
* `format` (string): The type of data to put in `param`. May be one of: | ||
* `json` (default): The `param` is JSON, and the `expr` is a decoded object. | ||
* `raw`: The `param` is string, and the `expr` is a string. | ||
* `int`: the `param` is an integer-like string, and the expr is an integer. | ||
* `bool`: The `param` is '0'/'1', and the `expr` is false/true. | ||
* `default` (object): The default data. (If the `param` is not included in the URL, it will initialize the expr.) | ||
|
||
## Suggested Usage | ||
|
||
`$bindToRoute()` was written for a complicated routing scenario with | ||
multiple parameters, e.g. `caseFilters:Object`, `caseId:Int`, `tab:String`, | ||
`activityFilters:Object`, `activityId:Int`. If you're use-case is one or | ||
two scalar values, then stick to vanilla `ngRoute`. This is only for | ||
complicated scenarios. | ||
|
||
If you are using `$bindToRoute()`, should you split up parameters -- with | ||
some using `ngRoute` and some using `$bindToRoute()`? I'd pick one style | ||
and stick to it. You're in a complex use-case where `$bindToRoute()` makes | ||
sense, then you already need to put thought into the different | ||
flows/input-combinations. Having two technical styles will increase the | ||
mental load. | ||
|
||
A goal of `bindToRoute()` is to accept inputs interchangably from the URL or | ||
HTML fields. Using `ngRoute`'s `resolve:` option only addresses the URL | ||
half. If you want one piece of code handling all inputs the same way, you | ||
should avoid `resolve:` and instead write a controller focused on | ||
orchestrating I/O: | ||
|
||
```js | ||
angular.module('sandbox').config(function($routeProvider) { | ||
$routeProvider.when('/example-route', { | ||
reloadOnSearch: false, | ||
template: | ||
'<div filter-toolbar-a="filterSetA" />' | ||
+ '<div filter-toolbar-b="filterSetB" />' | ||
+ '<div filter-toolbar-c="filterSetC" />' | ||
+ '<div data-set-a="dataSetA" />' | ||
+ '<div data-set-b="dataSetB" />' | ||
+ '<div data-set-c="dataSetC" />', | ||
controller: function($scope) { | ||
$scope.$bindToRoute({expr:'filterSetA', param:'a', default:{}}); | ||
$scope.$watchCollection('filterSetA', function(){ | ||
crmApi(...).then(function(...){ | ||
$scope.dataSetA = ...; | ||
}); | ||
}); | ||
|
||
$scope.$bindToRoute({expr:'filterSetB', param:'b', default:{}}); | ||
$scope.$watchCollection('filterSetB', function(){ | ||
crmApi(...).then(function(...){ | ||
$scope.dataSetB = ...; | ||
}); | ||
}); | ||
|
||
$scope.$bindToRoute({expr:'filterSetC', param:'c', default:{}}); | ||
$scope.$watchCollection('filterSetC', function(){ | ||
crmApi(...).then(function(...){ | ||
$scope.dataSetC = ...; | ||
}); | ||
}); | ||
} | ||
}); | ||
}); | ||
``` | ||
|
||
(This example is a little more symmetric than a real one -- because the A, | ||
B, and C datasets look independent. In practice, their loading may be | ||
intermingled.) |