1

We have this AngularJS SP application (smart-mirror) in electron browser, which has user createable extensions.

the extensions are small snippets of html that use angular directives and use controllers and services.

to install an extension, one has to edit the main page and insert the script tags for the controller and service functions and a <div ng-include= ...> for the snippet of HTML

hardcoded this single page app works great.

but I want to add the capability to this app (opensource) to dynamically load those elements somehow...

adding the tags to the dom works, BUT are not processed correctly. the HTML is processed before the scripts (from the inserted tags) are run, and when the ng-include inserts the HTML snippet, then controllers are not defined yet...

the body (with the extensions in hard-coded positions commented out)

    <body ng-controller="MirrorCtrl" ng-cloak> 
          <div class="top">
            <div class="top-left">
            <!--  <div ng-include="'plugins/datetime/index.html'"></div>
              <div ng-include="'plugins/calendar/index.html'"></div> -->
            </div>
            <div class="top-right">
            <!--  <div ng-include="'plugins/weather/index.html'"></div>
              <div ng-include="'plugins/traffic/index.html'"></div>
              <div ng-include="'plugins/stock/index.html'"></div>
              <div ng-include="'plugins/tvshows/index.html'"></div>
              <div ng-include="'plugins/ha-display/index.html'"></div> -->
            </div>
          </div>
    ...
    ...
    <script src="filename.service"/>
    <script src= filename.controller"/>
</body>

the calendar extension html (inserted into specific div area of the page)

<ul ng-controller="Calendar" class="calendar fade" ng-show="focus == 'default'" ng-class="config.calendar.showCalendarNames ? 'show-calendar-names' : ''">
    <li class="event" ng-repeat="event in calendar" ng-class="(calendar[$index - 1].label != event.label) ? 'day-marker' : ''">
        <div class="event-details">
            <span class="day">
                      <span ng-bind="event.startName"></span>
            <span ng-if="event.startName != event.endName"> - <span ng-bind="event.endName"></span></span>
            </span>
            <div class="details calendar-name" ng-bind="event.calendarName"></div>
            <span class="summary" ng-bind="event.SUMMARY"></span>
            <div class="details" ng-if="event.start.format('LT') != event.end.format('LT')">
                <span ng-if="event.startName != event.endName"><span ng-bind="event.start.format('M/D')"></span> <span ng-bind="event.start.format('LT')"></span>                - <span ng-bind="event.end.format('M/D')"></span> <span ng-bind="event.end.format('LT')"></span></span>
                <span ng-if="event.startName == event.endName"><span ng-bind="event.start.format('LT')"></span> - <span ng-bind="event.end.format('LT')"></span></span>
            </div>
            <div class="details" ng-if="event.start.format('LT') == event.end.format('LT')">All day</div>
        </div>
    </li>
</ul>

the calendar extension controller (used by the html)

function Calendar($scope, $http, $interval, CalendarService) {

    var getCalendar = function(){
        CalendarService.getCalendarEvents().then(function () {
            $scope.calendar = CalendarService.getFutureEvents();
        }, function (error) {
            console.log(error);
        });
    }

    getCalendar();
    $interval(getCalendar, config.calendar.refreshInterval * 60000 || 1800000)
}
console.log("registering calendar controller")
angular.module('SmartMirror')
    .controller('Calendar', Calendar);

the calendar extension service (used by the controller, shortened for this discussion)

(function () {
    'use strict';

    function CalendarService($window, $http, $q) {
  ...
  ...
        return service;
    }
console.log("registering calendar service")
    angular.module('SmartMirror')
    .factory('CalendarService', CalendarService);

} ());

so a user wanting to add an extension would have to create these files, and edit the main page HTML and insert them

<div ng-include src="filename.html"></div>

in the right place and then add the

<script src="filename.service" >

and

<script src="filename.controller">

in the right place and order, service needs to be done before the controller, as controller uses service.

anyhow, it's easy to add code to locate all the extensions and dynamically insert elements into the dom in their respective places... but...

in the hard coded, the scripts are added after the html in the body

so, I added a new script (processed when the page is loaded), which locates and inserts all the elements to support the extensions in the right places..

and then the script ends.... (last one in the hard-coded HTML) and the HTML directives are processed and boom, the dynamically added scripts have not been loaded or processed, so the controllers are not found...

I CAN create a temp HTML file with all this info in it and load THAT instead of dealing with the dynamic loading, but I think its better to resolve this

I have tried creating my own angular directive and compiling that in, but get stuck in a loop

<divinc src="filename.service"></divinc>

the inserted div is correct, as a child of the divinc directive

angular.module('SmartMirror')
.directive("divincl", ["$compile" ,function($compile){
      return {
        priority: 100, 
        terminal: true,
        compile: function(scope, element, attrs) {
                var html =  "<div ng-include=\"" + element['incl']+ "\" onload='function(){console.log(\'html loaded\')}'></div>"

                var templateGoesHere = angular.element(document.getElementById(element['id']));
                templateGoesHere.html(html);
                //document.body.innerHTML='';
               var v= $compile(templateGoesHere);
                //scope.$apply();
              return function linkFn(scope) {
                 v(scope) // Link compiled element to scope
              }
        }
      }
}]);

advice on how to solve this problem.. Thanks

pbibergal
  • 2,901
  • 1
  • 17
  • 19
Sam
  • 51
  • 1
  • 8
  • what you want to do is nested angularjs applications. There was a question here : https://stackoverflow.com/questions/18184617/angularjs-how-to-nest-applications-within-an-angular-app/22202715#comment98706108_22202715 . I made it work with the comment of Martin Thurau: here was his example: https://codepen.io/mthur/pen/QvaoQY . I personnally use that kind of system to run subapplications, will write this evening a full answer. – Pierre Emmanuel Lallemant May 20 '19 at 13:41
  • did you look what I wrote before ? do you need me to write an answer or do you think it isn't what you need ? – Pierre Emmanuel Lallemant May 20 '19 at 17:51
  • sorry, gone all day.. i created the 'plugin' elements as in the linked example ```
    – Sam May 21 '19 at 02:23
  • i do NOT know how you format in comments...says bracket with `code`, but that didn't work – Sam May 21 '19 at 02:24
  • code for above ` a[0].appendChild(ss) put the div tree in the right element of the main app html tree var p = document.getElementById(plugin) angular.module(plugin, []); var e=angular.element(p) e.injector = function() { return false; } angular.bootstrap(e, [plugin]); delete e.injector(); ` – Sam May 21 '19 at 02:24
  • and I see in the elements view, the ng-include has been executed as the content is in the element block but I still get that the controller is not registered.. I see AFTER the unable to find controller message the console logs for the service and controller registration messages – Sam May 21 '19 at 02:25

1 Answers1

0

In order to make an angularjs 1.7 application load dynamically extensions, there are 2 ways:

  • either use "nested angularjs applications", which is clearly an advanced use of angularjs and will require you to communicate between 2 angularjs applications, to use $scope.$apply to tell the other app to update etc..
  • either don't load them dynamically in the frontend, but in your backend when generating the html page which contains the application. Try to list all the extensions from the start.

I recommend you to forget the use of ng-include too, and the fact of trying to add <script></script> inside a directive of your application.

First, you need to re-understand how an angularjs application is started.

  • When you load your main application, you have a script in which angular.module, angular.directive, angular.value, angular.config, angular.run ... calls are made. This is the declaration step
  • If you declare a module MyApp and that in your html you have a DOM element with ng-app="MyApp", angularjs will automatically run angular.bootstrap() on this DOM element in order to start MyApp. The execution of the application starts here. You cannot declare anything anymore in the module MyApp.

Secondly, I think that <script></script> code inside templates is sanitized and removed by angular. Plus, even if you execute the code, since the declaration step has finished, you are not supposed to create new directives or register new services, it won't work.

A good way is that when you load your plugin, you:

  • Load the script of the plugin from the start, it must declare a new module like MyPlugin1.
  • In the directive which will contain the plugin, put the code of the link I sent you, which makes possible to insert a sub-application. In the end you will have a <div ng-app="MyPlugin1"></div> inside your directive's template
  • Then call angular.bootstrap on that node, which will make possible to start the sub application.

If you do this, you can run the sub application, but you didn't pass it parameters. In order to pass it parameters, you can put the code of the module MyPlugin1 inside a function, in order to have an application factory. Then use app.value('param1', parameter1) to initialize the app.

For example:

function declarePlugin1(myParam1, myParam2) {
  var app = angular.module('MyPlugin1', []);
  // app.directive();

  app.value('myParam1', myParam1);
  app.value('myParam2', myParam2);
}

And inside the directive call declarePlugin1("test", 42);, which will declare the application MyPlugin1 with the initialized values, and then angular.bootstrap to tell angularjs to start this application.

You can pass callbacks too, in order to communicate between the 2 applications.

  • 1
    thanks... i was coming to the same conclusion. i can do approach 2 easily, but I wanted to try to make dynamic loading work. figured it would be complex... but we can't have all this cross talk explicit communications. – Sam May 21 '19 at 11:14
  • I have created the html content prior to startup now, so its 'dynamic' in that no one has to edit the file anymore – Sam May 21 '19 at 19:59
  • nice. It's not 'on the fly' but if it make the job it's cool :) – Pierre Emmanuel Lallemant May 22 '19 at 04:24