2

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

10
  • 1
    What is your use case that you need to access $compile outside controller context? Having you considered custom directives or even `ng-include? Commented May 31, 2021 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. Commented May 31, 2021 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 Commented May 31, 2021 at 18:13
  • Worst case you could have a compile function you access with angular.element().scope but that just seems hacky to me Commented May 31, 2021 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. Commented May 31, 2021 at 18:17

2 Answers 2

2

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/

Sign up to request clarification or add additional context in comments.

Comments

1
+50

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

13 Comments

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?
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.
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.
Hello @PhineasJ, I found this answer here 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('<div ng-app></div>'); }; } }); 2. var injector = window.angular.injector(['ng', 'mockApp', 'oneModule']);
I appreciate your help or let me know I can create a new question. I will soon update the question with the latest status.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.