javascriptangularjsvalidationcompilationangularjs-forms

How to invoke AngularJS $compile from outside AngularJS module/code?


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


Solution

  • 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/