From 52eef7f1c83022f027d4beaba6231a6de17dd32c Mon Sep 17 00:00:00 2001 From: colemanw Date: Fri, 20 Feb 2026 09:58:40 -0500 Subject: [PATCH] Angular - Add service for on-demand loading modules --- CRM/Core/Page.php | 2 +- Civi/Angular/Page/Modules.php | 86 ++++++----------------------------- js/angular-crmResource/all.js | 63 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 73 deletions(-) diff --git a/CRM/Core/Page.php b/CRM/Core/Page.php index 47f6336caefa..0e9fe783c539 100644 --- a/CRM/Core/Page.php +++ b/CRM/Core/Page.php @@ -470,7 +470,7 @@ public function invalidKey() { * For ajax-loaded pages, this returns scripts, styles and settings as structured data * rather than as markup. Those resources are handled clientside by civi.crmSnippet.refresh(). */ - private function addAjaxResources() { + protected function addAjaxResources() { $ajaxRegion = CRM_Core_Region::instance('ajax-snippet'); // Ensure all resources are added to the region before processing it $ajaxRegion->getFinalItems(); diff --git a/Civi/Angular/Page/Modules.php b/Civi/Angular/Page/Modules.php index a1d9a8fc3651..1812cd760eb0 100644 --- a/Civi/Angular/Page/Modules.php +++ b/Civi/Angular/Page/Modules.php @@ -5,65 +5,24 @@ use Civi\Angular\Manager; /** - * This page aggregates data from Angular modules. - * - * Example: Aggregate metadata about all modules in JSON format. - * civicrm/ajax/angular-modules?format=json - * - * Example: Aggregate metadata for crmUi and crmUtil modules. - * civicrm/ajax/angular-modules?format=json&modules=crmUi,crmUtil - * - * Example: Aggregate *.js files for all modules. - * civicrm/ajax/angular-modules?format=js - * - * Example: Aggregate *.css files for all modules. - * civicrm/ajax/angular-modules?format=css + * Page callback to load Angular modules. */ class Modules extends \CRM_Core_Page { /** - * Generate asset content (when accessed via older, custom - * "civicrm/ajax/anulgar-modules" route). + * Ajax callback used for on-demand loading of Angular modules. * - * @deprecated - * - * @throws \CRM_Core_Exception + * e.g. civicrm/ajax/angular-modules?modules=crmSearchDisplayList */ public function run() { - /** - * @var \Civi\Angular\Manager $angular - */ - $angular = \Civi::service('angular'); - $moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String'), $angular); - - switch (\CRM_Utils_Request::retrieve('format', 'String')) { - case 'json': - case '': - $this->send( - 'application/javascript', - json_encode($this->getMetadata($moduleNames, $angular)) - ); - break; + $moduleNames = $this->parseModuleNames(\CRM_Utils_Request::retrieve('modules', 'String')); - case 'js': - $this->send( - 'application/javascript', - $this->digestJs($angular->getResources($moduleNames, 'js', 'path')) - ); - break; - - case 'css': - $this->send( - 'text/css', - \CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n") - ); - break; - - default: - throw new \CRM_Core_Exception("Unrecognized format"); + if ($moduleNames) { + $loader = \Civi::service('angularjs.loader'); + $loader->addModules($moduleNames); + $this->addAjaxResources(); } - - \CRM_Utils_System::civiExit(); + \CRM_Core_Page_AJAX::returnJsonResponse($this->ajaxResponse); } /** @@ -79,13 +38,13 @@ public static function buildAngularModules($event) { switch ($event->asset) { case 'angular-modules.json': - $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular); + $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL); $event->mimeType = 'application/json'; $event->content = json_encode($page->getMetadata($moduleNames, $angular)); break; case 'angular-modules.js': - $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular); + $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL); $event->mimeType = 'application/javascript'; $files = array_merge( // FIXME: The `resetLocationProviderHashPrefix.js` has to stay in sync with `\Civi\Angular\AngularLoader::load()`. @@ -96,7 +55,7 @@ public static function buildAngularModules($event) { break; case 'angular-modules.css': - $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL, $angular); + $moduleNames = $page->parseModuleNames($event->params['modules'] ?? NULL); $event->mimeType = 'text/css'; $event->content = \CRM_Utils_File::concat($angular->getResources($moduleNames, 'css', 'path'), "\n"); @@ -132,11 +91,10 @@ public function digestJs($files) { /** * @param string $modulesExpr * Comma-separated list of module names. - * @param \Civi\Angular\Manager $angular * @return array * Any well-formed module names. All if moduleExpr is blank. */ - public function parseModuleNames($modulesExpr, $angular) { + public function parseModuleNames($modulesExpr): array { if ($modulesExpr) { $moduleNames = preg_grep( '/^[a-zA-Z0-9\-_\.]+$/', @@ -145,6 +103,7 @@ public function parseModuleNames($modulesExpr, $angular) { return $moduleNames; } else { + $angular = \Civi::service('angular'); $moduleNames = array_keys($angular->getModules()); return $moduleNames; } @@ -171,21 +130,4 @@ public function getMetadata(array $moduleNames, Manager $angular): array { return $result; } - /** - * Send a response. - * - * @param string $type - * Content type. - * @param string $data - * Content. - */ - public function send($type, $data) { - // Encourage browsers to cache for a long time - 1 year - $ttl = 60 * 60 * 24 * 364; - \CRM_Utils_System::setHttpHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $ttl)); - \CRM_Utils_System::setHttpHeader("Content-Type", $type); - \CRM_Utils_System::setHttpHeader("Cache-Control", "max-age=$ttl, public"); - echo $data; - } - } diff --git a/js/angular-crmResource/all.js b/js/angular-crmResource/all.js index bad03ba486ad..a9c9ae546824 100644 --- a/js/angular-crmResource/all.js +++ b/js/angular-crmResource/all.js @@ -66,6 +66,69 @@ }; }); + const modulesPending = {}; + angular.module('crmResource').factory('crmResourceLoader', function ($q) { + return { + // Load Angular modules dynamically + loadModules: function (moduleNames) { + // Filter out modules that are already loaded. + const loadedModules = CRM.angular.modules || []; + const modulesToLoad = moduleNames.filter(name => !loadedModules.includes(name)); + + // Early return if all modules are already loaded. + if (modulesToLoad.length === 0) { + return $q.resolve(); + } + + // Collect promises for modules that are already pending + const pendingPromises = []; + const newModulesToLoad = []; + + modulesToLoad.forEach(name => { + if (modulesPending[name]) { + pendingPromises.push(modulesPending[name]); + } else { + newModulesToLoad.push(name); + } + }); + + // If all requested modules are pending, return combined promise + if (newModulesToLoad.length === 0) { + return $q.all(pendingPromises); + } + + // Create a new promise for the modules being loaded + const deferred = $q.defer(); + + // Track all new modules with the same promise + newModulesToLoad.forEach(name => { + modulesPending[name] = deferred.promise; + }); + + const snippet = $('
'); + const settings = { + url: CRM.url('civicrm/ajax/angular-modules', {modules: newModulesToLoad.join(',')}), + }; + + $(snippet).crmSnippet(settings) + .on('crmLoad', function () { + // Clean up pending tracking and resolve + newModulesToLoad.forEach(name => delete modulesPending[name]); + deferred.resolve(); + }) + .on('crmLoadFail', function () { + // Clean up pending tracking and reject + newModulesToLoad.forEach(name => delete modulesPending[name]); + deferred.reject(); + }) + .crmSnippet('refresh'); + + // Return combined promise of new and pending modules + return $q.all([deferred.promise, ...pendingPromises]); + } + }; + }); + angular.module('crmResource').config(function($provide) { $provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) { var origGet = $delegate.get;