1

Suppose I have HTML with AngularJS module/controller as follows:

angular
.module("myModule", [])
.controller("myController", ['$scope', '$compile', function ($scope, $compile) {
    $scope.txt = "<b>SampleTxt</b>";
    $scope.submit = function () {
        var html = $compile($scope.txt)($scope);
        angular.element(document.getElementById("display")).append(html);
    }
}]);

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="myModule" >
    <div ng-controller="myController">
        <form name="myForm">
            <span>Age:</span><input type="number" name="age" ng-model="age"/>
            <textarea ng-model="txt" ></textarea>
            <input type="button" value="submit" ng-click="submit()" />
        </form>
        <div id="display"></div>
    </div>
</body>

The above sample will allow adding an element to AngularJS app during run-time using $compile.

Suppose I want to insert an input element such as a text box name driversLinces with the attribute ng-required="age > 21", suppose I want to insert this element with conditional required feature from the JavaScript console for testing and verification purposes. Also, suppose I want to do the same but I want to modify the ng-required property if an existing element such as the age, how I can do that?

I am thinking to create a function that will access $compile somehow but not sure how. Can you help me? I am able to access the $compile service only from inside the controller.

Note: due to certain limitations and lack of information/resources, I have limited access to the full HTML code. I can access the HTML and AngularJS forms using a UI Modeler. I can add my custom HTML code but I don't know if I can enclose an existing Form Part with my own custom HTML container which is required to add a directive to access the inner parts.

I can access AngularJS scope and ng-form elements using angular.element(). I can trigger my JavaScript on Form-Load, on a click of a button, or when a model value changes. I can add a form element and link it to an AngularJS model. I could not figure out how to access the $compile service from JavaScript.

Update:

I will add more info to explain my objective or the use-case. I want to add custom validation rules and errors to the AngularJS form from JavaScript. The platform I am working with uses AngularJS, but doesn't allow me to get easy access to AngularJS code to add directives, or at least for now, I don't have the needed resources for this purpose. However, this platform provides me with ability to trigger my custom JavaScript code on a click of a button which can be triggered automatically when the form loads (on-load event). Also, I can pass the ID of the button that was clicked. With this, I was able to access the scope using angular.element('#id').scope(). This enabled me to access almost all the other elements. I can see all ng-models and ng form controllers and all its parents in the scope object. Sometimes, I have to access the $parent to reach to the root, but I think eventually I am able to access almost anything from the scope object.

Now, I need to be able to find the form elements and add custom validation rules. I can travers all form elements from the scope object, and I can figure out how to get the element ID and its binding details. All I need now is how to add a custom validation rule and error message on form-load event.

For example, I can use JSON to represent validation rules for AngularJS form as follows:

[
    {
        "id": "employee-name",
        "required": true,
        "msg": "Employee name is required."
    },
    {
        "id": "degree",
        "customValidation": "someJSFunctionName",
        "msg": "The provided degree is invalid. Please review the rules and try again."
    }
]

Then, on form-load event, I want to load the above rules on the form and make them effective. How is this possible? Consider that I have access to the scope object, I can use only JavaScript, and I cannot use AngularJS directives.


Update 2:

Based on answer provided by PhineasJ below, I used the console with the following commands:

var injector = window.angular.injector(['ng']);
var $compile = injector.get('$compile');
var elm = angular.element("my-element-selector");
var elmScope = elm.scope();
elm.attr('ng-required', true);
var elmCompile = $compile(elm[0])(elmScope);

While the above didn't throw any error, however, it is not working as it should. If I make the field elm empty, it won't trigger the ng-required error, though I can see that the required attribute was added after executing the $compile command. I noticed that I have to execute the $compile service every time I update the field value so that the validation rule will reflect, but I don't see the field's ctrl.$error object being updated. It is always empty.

Then I tried the following:

var injector = window.angular.injector(['ng', 'myApp']);
var $compile = injector.get('$compile');

... I got the error Uncaught Error: [$injector:unpr] Unknown provider: $rootElementProvider.

Then I tried the following:

var mockApp = angular.module('mockApp', []).provider({
  $rootElement:function() {
     this.$get = function() {
       return angular.element('<div ng-app></div>');
    };
  }
});
var injector = window.angular.injector(['ng', 'mockApp', 'myApp']);

... no errors were thrown the first time, but when I tried again, I got the error The view engine is already initialized and cannot be further extended. So I am stuck with the $compile service.

I did try adding the rules directly using $validators() and it was a success. See details below:

//The elm form controller is found on the $parent scope and this is beyond my control.
//The ng-form element names are generated by the back-end and I have no control over this part. In this case the HTML element 'elm' is the form element name 'ewFormControl123'.
elmScope.$parent.ewFormControl123.$validators.required = 
    function (modelValue, viewValue) {
        console.log(modelValue, viewValue);
        var result = !!(modelValue??null);
        console.log("result = ", result);
        return result;
    }

The above does seem to work fine, however, I am still interested in using $compile by injecting the validation rules or the directives into the HTML code and then run the $compile service over that element. I think injecting the needed parts into the HTML and run $compile is better.

Update 3

With the help of @PhineasJ, I managed to prepare a small sample that uses AngularJS injector and $compile service. This is working successfully, but the same approach is not working on the target application.

w3school original sample: https://www.w3schools.com/angular/tryit.asp?filename=try_ng_ng-required

JS Fiddle sample: https://jsfiddle.net/tarekahf/5gfy01k2/

Following this method, I should be able to load validation rules during run-time for any field as long as there is a selector to grab the element.

I am now struggling with the errors I get when applying the same method on the target application. I have two problems:

  1. If I use const injector = angular.injector(['ng', 'myApp']) with the app name, I get the error: Uncaught Error: [$injector:unpr] Unknown provider: $rootElementProvider
  2. If I don't add the app name, no error is thrown, but the validation rule is not respected.

However, if I use the formController.$validators object, I can a add the validation rule and it is respected. I am not sure why no one is recommending this approach.

I appreciate your help and feedback.

Update 4

I found the solution. Check the answer I am adding below.

Tarek

tarekahf
  • 738
  • 1
  • 16
  • 42
  • 1
    What is your use case that you need to access $compile outside controller context? Having you considered custom directives or even `ng-include? – charlietfl May 31 '21 at 18:02
  • I am on a platform that uses AngularJS with an additional layer on top. I don't have time and resources to dig and figure out how to add controller and directives as this will take a long time, and I don't think I can link HTML to a directive. I can easily write JavaScript functions and I can access elements on the active page using a script button. I am able to access the `scope` using `angular.element()`. I need to modify `ng-readonly` and `ng-required` form JavaScript for certain HTML existing elements. I want to be able to test it from the console and trigger it on form load. – tarekahf May 31 '21 at 18:10
  • Numerous ways to link html to a directive. Remote template file, template function, template string, $templateCache service etc. Directive can also use $compile – charlietfl May 31 '21 at 18:13
  • Worst case you could have a compile function you access with `angular.element().scope` but that just seems hacky to me – charlietfl May 31 '21 at 18:14
  • ... continued ... there are existing HTML and AngularJS forms which can be accessed via a modeler UI (I cannot modify the raw HTML source). I can also add my own custom HTML code on the page and I can trigger JavaScript code on Page Load, if a model variable value changes, and on a click of a button. I can add elements and link them to `nd-model`. I don't know if I can enclose existing HTML form parts with my own customer HTML container which is required to link it to a directive. I need to do something as quickly as possible as I don't have much room for trial and error. – tarekahf May 31 '21 at 18:17
  • what does this UI Modeler (not familiar with that term) provide you...html, JS Objects or??? – charlietfl May 31 '21 at 18:28
  • Also is this angular app really using V1.2? that will be quite limiting also – charlietfl May 31 '21 at 18:30
  • This is a BPMN based platform similar to Activiti project that was forked into Camunda. I am using something similar. When you add an element to a Form (which is basically AngularJS Form), the properties you can change are limited. I want to be able to have full control to all `ng-form` AngularJS elements using JavaScript. How I can tell the AngularJS version used? – tarekahf May 31 '21 at 18:47
  • @charlietfl I have updated the post with more details. I appreciate it if you can help. – tarekahf Jul 16 '21 at 18:08
  • OK will look at it this weekend – charlietfl Jul 16 '21 at 18:26

2 Answers2

1

To invoke $compile function outside AngularJS, yon can use:

const injector = window.angular.injector(['ng', 'your-module']);
injector.invoke(function($rootScope, $compile) {
  $compile('<div custom-directive></div>')($rootScope);
})

To add dynamic validation rules, you can reference custom validation. The controller Scope can also be retrieve from angular.element().scope as you mentioned.

Here's a demo on how to dynamically load validators and compile it. https://plnkr.co/edit/4ay3Ig2AnGYjLQ92?preview

PhineasJ
  • 303
  • 1
  • 7
  • Fantastic! I will fiddle with a test model using the info you provided. Are you saying that I can use the scope retrieved from `angular.element.scope` to modify the form field validation rules? I noticed that I can access the angular form elements/controllers from the scope, but I didn't figure out if you can modify the validation rules. The reference you provided for custom validation is referring to custom validation by adding the rules inside HTML and using directives to control them, which I don't have access to. Can you provide your feedback please? – tarekahf Jul 23 '21 at 16:43
  • Hello, @tarekahf. I think you can load your validation directives javascript files, then use the $compile function to link the directives dynamically. I modified the answer to provide a demo based on the official validator demo. – PhineasJ Jul 26 '21 at 03:13
  • I tried `const injector = window.angular.injector(['ng', 'your-module']);` but I get the error `Uncaught Error: [$injector:unpr] Unknown provider: $rootElementProvider`. I am researching it now and looks like I have to make `$rootElementProvider` available while initializing AngularJS app. In the meantime, if you can point me how to do that, it will be great. – tarekahf Jul 26 '21 at 18:20
  • Hello @PhineasJ, I found this answer here https://stackoverflow.com/a/28400389/4180447. Followed the steps from the console, I didn't get error the first time, them I tried to repeat the steps again, now I'm getting the error `Uncaught Error: The view engine is already initialized and cannot be further extended`. Steps: 1. ``var mockApp = angular.module('mockApp', []).provider({ $rootElement:function() { this.$get = function() { return angular.element('
    '); }; } });`` 2. ``var injector = window.angular.injector(['ng', 'mockApp', 'oneModule']);``
    – tarekahf Jul 26 '21 at 19:46
  • I appreciate your help or let me know I can create a new question. I will soon update the question with the latest status. – tarekahf Jul 26 '21 at 19:49
  • Hi @tarekahf. I think you can provide a plunker, so that I can know what exactly happened when getting the injector. The demo I provide is really simple, only contains one angular module. If your app contains multiple angular module, maybe it will get this error. I'm not sure, we can try to analysis it in plunker : ) – PhineasJ Jul 27 '21 at 01:58
  • You can also try to get $compile with code `injector.invoke(function($rootScope, $compile) {})` – PhineasJ Jul 27 '21 at 02:37
  • The problem is that I am hitting this error `Uncaught Error: The view engine is already initialized and cannot be further` when executing this code `var injector = window.angular.injector(['ng', 'oneModule'])`. Let me try the same on a sample small application. – tarekahf Jul 28 '21 at 14:38
  • Hi @PhineasJ, developed a small sample following your code, and it worked at my end. I used both approaches `const injector = angular.injector(['ng', 'myApp'])` and `const injector = angular.injector(['ng'])` and both worked. I am able to add a new element and modify the `ng-required` attribute and it is working successfully. The only difference at my end is that I am compiling the element not the entire controller. However, it is not working on the actual application. I noticed that the actual application doesn't have `ng-controller`... – tarekahf Aug 02 '21 at 06:25
  • … cont. Note that it is partially working if I use `const injector = angular.injector(['ng'])` (without the app name). However, the binding is not working and the rule `ng-required` is not respected at all. Do you have any idea how I can troubleshoot this issue? – tarekahf Aug 02 '21 at 06:39
  • Hi @tarekahf. I have no idea if you don't provide a plunker. To use the injector, you can reference the document here: https://docs.angularjs.org/api/ng/function/angular.injector. To use the ng-required, you can reference the document: https://docs.angularjs.org/api/ng/directive/ngRequired – PhineasJ Aug 03 '21 at 02:21
  • I totally understand. It is not feasible to provide a plunker that will reproduce the issue. I tried it on a small scale, and your method worked (with small variation). I will soon update the question with the latest code sample I used and worked. – tarekahf Aug 03 '21 at 16:28
  • Hi @PhineasJ, I finally found the solution. Thanks a lot for your help. Check the updated question. – tarekahf Aug 03 '21 at 20:43
1

The fix was to use the following command to get the injector:

var injector = angular.element("#myAngularAppID").injector();

where myAngularAppID id the element ID of the HTML element where ng-app is defined.

The following variations of the injector statements didn't work:

//Without using the app
var injector = window.angular.injector(['ng']);
//With the app
var injector = window.angular.injector(['ng', 'myApp'])

Once the above correction was implemented, the problem was solved.

Special thanks @PhineasJ. Also, the following references helped me:

  1. Injector returns undefined value?
  2. angularjs compile ng-controller and interpolation

Check the JS Fiddle which has all possible variations for using $compile to add HTML elements dynamically and bind them to AngularJS Scope:

https://jsfiddle.net/tarekahf/5gfy01k2/

tarekahf
  • 738
  • 1
  • 16
  • 42