Skip to content

Commit

Permalink
CRM-20600, CRM-20112 - AngularLoader - Add helper for base-pages and …
Browse files Browse the repository at this point in the history
…injections

This commit extracts the `Civi\Angular\Page\Main::registerResources()` and
creates a new utility-class, `AngularLoader`.  The `AngularLoader` can be
used in new Angular base-pages, e.g.

```php
class Example extends CRM_Core_Page {
  public function run() {
    $loader = new \Civi\Angular\AngularLoader();
    $loader->setPageName('civicrm/foo/bar');
    $loader->setModules(array('crmApp', '...'));
    $loader->load();
    return parent::run();
  }
}
```

The `AngularLoader` only loads the resources or assets (JS/CSS/HTML files).
To start Angular, you'll need to call `ng-app` or `angular.bootstrap(...)`.
One way to do this is to define a page-template:

```html
<!-- Example.tpl -->
<div ng-app="crmApp">
  <div ng-view></div>
</div>
```

Or you can reuse the existing template:

```php
  public function getTemplateFileName() {
    return 'Civi/Angular/Page/Main.tpl';
  }
```

Note: This is framed as a utility-class which loads the Angular resource
files.  It's not a page-class/base-class.  This approach allows us to call
the utility from other situations -- e.g.  inject AngularJS onto an
pre-existing page via hook.  Doing that may or may not be wise, but the
class-hierarchy shouldn't be the issue.
  • Loading branch information
totten committed Jun 6, 2017
1 parent 323b9aa commit aa1f975
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 66 deletions.
236 changes: 236 additions & 0 deletions Civi/Angular/AngularLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<?php
namespace Civi\Angular;

/**
* The AngularLoader loads any JS/CSS/JSON resources
* required for setting up AngularJS.
*
* The AngularLoader stops short of bootstrapping AngularJS. You may
* need to `<div ng-app="..."></div>` or `angular.bootstrap(...)`.
*
* @code
* $loader = new AngularLoader();
* $loader->setPageName('civicrm/case/a');
* $loader->setModules(array('crmApp'));
* $loader->load();
* @endCode
*
* @link https://docs.angularjs.org/guide/bootstrap
*/
class AngularLoader {

/**
* The weight to assign to any Angular JS module files.
*/
const DEFAULT_MODULE_WEIGHT = 200;

/**
* The resource manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
*/
protected $res;

/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
*/
protected $angular;

/**
* The region of the page into which JavaScript will be loaded.
*
* @var string
*/
protected $region;

/**
* @var string
* Ex: 'civicrm/a'.
*/
protected $pageName;

/**
* @var array
* A list of modules to load.
*/
protected $modules;

/**
* AngularLoader constructor.
*/
public function __construct() {
$this->res = \CRM_Core_Resources::singleton();
$this->angular = \Civi::service('angular');
$this->region = \CRM_Utils_Request::retrieve('snippet', 'String') ? 'ajax-snippet' : 'html-header';
$this->pageName = isset($_GET['q']) ? $_GET['q'] : NULL;
$this->modules = array();
}

/**
* Register resources required by Angular.
*/
public function load() {
$angular = $this->getAngular();
$res = $this->getRes();

$moduleNames = $this->findActiveModules();
if (!$this->isAllModules($moduleNames)) {
$assetParams = array('modules' => implode(',', $moduleNames));
}
else {
// The module list will be "all modules that the user can see".
$assetParams = array('nonce' => md5(implode(',', $moduleNames)));
}

$res->addSettingsFactory(function () use (&$moduleNames, $angular, $res, $assetParams) {
// TODO optimization; client-side caching
$result = array_merge($angular->getResources($moduleNames, 'settings', 'settings'), array(
'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
'angular' => array(
'modules' => $moduleNames,
'requires' => $angular->getResources($moduleNames, 'requires', 'requires'),
'cacheCode' => $res->getCacheCode(),
'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
),
));
return $result;
});

$res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->getRegion(), FALSE);
$res->addScriptFile('civicrm', 'js/crm.angular.js', 101, $this->getRegion(), FALSE);

$headOffset = 0;
$config = \CRM_Core_Config::singleton();
if ($config->debug) {
foreach ($moduleNames as $moduleName) {
foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) {
$res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
// addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
}
}
}
else {
// Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
// $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
$aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams);
$res->addScriptUrl($aggScriptUrl, 120, $this->getRegion());

// FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons.
//$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $res->getCacheCode(), FALSE, NULL, FALSE);
//$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams);
//$res->addStyleUrl($aggStyleUrl, 120, $this->getRegion());

foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) {
$res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->getRegion());
}
}
}

/**
* Get a list of all Angular modules which should be activated on this
* page.
*
* @return array
* List of module names.
* Ex: array('angularFileUpload', 'crmUi', 'crmUtil').
*/
public function findActiveModules() {
return $this->angular->resolveDependencies(array_merge(
$this->getModules(),
$this->angular->resolveDefaultModules($this->getPageName())
));
}

/**
* @param $moduleNames
* @return int
*/
private function isAllModules($moduleNames) {
$allModuleNames = array_keys($this->angular->getModules());
return count(array_diff($allModuleNames, $moduleNames)) === 0;
}

/**
* @return \CRM_Core_Resources
*/
public function getRes() {
return $this->res;
}

/**
* @param \CRM_Core_Resources $res
*/
public function setRes($res) {
$this->res = $res;
}

/**
* @return \Civi\Angular\Manager
*/
public function getAngular() {
return $this->angular;
}

/**
* @param \Civi\Angular\Manager $angular
*/
public function setAngular($angular) {
$this->angular = $angular;
}

/**
* @return string
*/
public function getRegion() {
return $this->region;
}

/**
* @param string $region
*/
public function setRegion($region) {
$this->region = $region;
}

/**
* @return string
* Ex: 'civicrm/a'.
*/
public function getPageName() {
return $this->pageName;
}

/**
* @param string $pageName
* Ex: 'civicrm/a'.
*/
public function setPageName($pageName) {
$this->pageName = $pageName;
}

/**
* @return array
*/
public function getModules() {
return $this->modules;
}

/**
* @param array $modules
*/
public function setModules($modules) {
$this->modules = $modules;
}

}
80 changes: 14 additions & 66 deletions Civi/Angular/Page/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @link https://issues.civicrm.org/jira/browse/CRM-14479
*/
class Main extends \CRM_Core_Page {

/**
* The weight to assign to any Angular JS module files.
*/
Expand All @@ -19,23 +20,25 @@ class Main extends \CRM_Core_Page {
* Do not use publicly. Inject your own copy!
*
* @var \CRM_Core_Resources
* @deprecated
*/
public $res;


/**
* The Angular module manager.
*
* Do not use publicly. Inject your own copy!
*
* @var \Civi\Angular\Manager
* @deprecated
*/
public $angular;

/**
* The region of the page into which JavaScript will be loaded.
*
* @var String
* @deprecated
*/
public $region;

Expand Down Expand Up @@ -71,82 +74,27 @@ public function run() {
* Register resources required by Angular.
*/
public function registerResources() {
$page = $this; // PHP 5.3 does not propagate $this to inner functions.

$allModuleNames = array_keys($this->angular->getModules());
$moduleNames = $this->getModules();
if (count(array_diff($allModuleNames, $moduleNames))) {
$assetParams = array('modules' => implode(',', $moduleNames));
}
else {
// The module list will be "all modules that the user can see".
$assetParams = array('nonce' => md5(implode(',', $moduleNames)));
}

$this->res->addSettingsFactory(function () use (&$moduleNames, $page, $assetParams) {
// TODO optimization; client-side caching
return array_merge($page->angular->getResources($moduleNames, 'settings', 'settings'), array(
'resourceUrls' => \CRM_Extension_System::singleton()->getMapper()->getActiveModuleUrls(),
'angular' => array(
'modules' => array_merge(array('ngRoute'), $moduleNames),
'requires' => $page->angular->getResources($moduleNames, 'requires', 'requires'),
'cacheCode' => $page->res->getCacheCode(),
'bundleUrl' => \Civi::service('asset_builder')->getUrl('angular-modules.json', $assetParams),
),
));
});

$this->res->addScriptFile('civicrm', 'bower_components/angular/angular.min.js', 100, $this->region, FALSE);
$this->res->addScriptFile('civicrm', 'js/crm.angular.js', 101, $this->region, FALSE);

$headOffset = 0;
$config = \CRM_Core_Config::singleton();
if ($config->debug) {
foreach ($moduleNames as $moduleName) {
foreach ($this->angular->getResources($moduleName, 'css', 'cacheUrl') as $url) {
$this->res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->region);
}
foreach ($this->angular->getResources($moduleName, 'js', 'cacheUrl') as $url) {
$this->res->addScriptUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->region);
// addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
}
}
}
else {
// Note: addScriptUrl() bypasses the normal string-localization of addScriptFile(),
// but that's OK because all Angular strings (JS+HTML) will load via crmResource.
// $aggScriptUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=js&r=' . $page->res->getCacheCode(), FALSE, NULL, FALSE);
$aggScriptUrl = \Civi::service('asset_builder')->getUrl('angular-modules.js', $assetParams);
$this->res->addScriptUrl($aggScriptUrl, 120, $this->region);

// FIXME: The following CSS aggregator doesn't currently handle path-adjustments - which can break icons.
//$aggStyleUrl = \CRM_Utils_System::url('civicrm/ajax/angular-modules', 'format=css&r=' . $page->res->getCacheCode(), FALSE, NULL, FALSE);
//$aggStyleUrl = \Civi::service('asset_builder')->getUrl('angular-modules.css', $assetParams);
//$this->res->addStyleUrl($aggStyleUrl, 120, $this->region);

foreach ($this->angular->getResources($moduleNames, 'css', 'cacheUrl') as $url) {
$this->res->addStyleUrl($url, self::DEFAULT_MODULE_WEIGHT + (++$headOffset), $this->region);
}
}
$loader = new \Civi\Angular\AngularLoader();
$loader->setPageName('civicrm/a');
$loader->setModules(array('crmApp'));
$loader->load();

// If trying to load an Angular page via AJAX, the route must be passed as a
// URL parameter, since the server doesn't receive information about
// URL fragments (i.e, what comes after the #).
\CRM_Core_Resources::singleton()->addSetting(array(
'crmApp' => array(
'defaultRoute' => NULL,
),
'angularRoute' => \CRM_Utils_Request::retrieve('route', 'String'),
));
}

/**
* Get a list of Angular modules to include on this page.
*
* @return array
* List of module names.
* Ex: array('angularFileUpload', 'crmUi', 'crmUtil').
* @inheritdoc
*/
public function getModules() {
return array_keys($this->angular->getModules());
public function getTemplateFileName() {
return 'Civi/Angular/Page/Main.tpl';
}

}
10 changes: 10 additions & 0 deletions ang/crmApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
var crmApp = angular.module('crmApp', CRM.angular.modules);
crmApp.config(['$routeProvider',
function($routeProvider) {

if (CRM.crmApp.defaultRoute) {
$routeProvider.when('/', {
template: '<div></div>',
controller: function($location) {
$location.path(CRM.crmApp.defaultRoute);
}
});
}

$routeProvider.otherwise({
template: ts('Unknown path')
});
Expand Down

0 comments on commit aa1f975

Please sign in to comment.