HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

A directive allowing to make a whole DOM subtree readonly

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
allowingwholedirectivemakedomsubtreereadonly

Problem

What do you think about this directive? For each input, it traverses the DOM towards the root and if it finds an element with the class readonly, it makes the input readonly.

An ng-readonly directive on the input itself get honored: The input becomes readonly whenever ng-readonly evaluates to true or any enclosing element has the class readonly.

.directive("input", function($parse) {
    return {
        restrict: "E",
        link: function($scope, element, attr) {
            if (attr.type === "radio" || attr.type === "checkbox") {
              return; // for simplicity, let's ignore them
            }

            var org = attr.ngReadonly;
            if (!org && attr.readonly) {
              return; // readonly seems to be set manually, so let's not touch it
            }

            $scope.$watch(function() {
                var readonly = $parse(org)($scope);
                for (var e = element; e.length && !readonly; e=e.parent()) {
                    readonly = e.hasClass("readonly");
                }
                console.log(element.attr("ng-model"), readonly)
                attr.$set("readonly", !!readonly);
            });
        },
    };
})


The purpose is to allow to make a whole subtree readonly, no matter what's inside. It works, but I can imagine there can be performance problems and/or interferences with ng-readonly setting the value independently of this directive.
An explanation why I'm not simply binding to a scope variable

While I really appreciate Thomas' answer, I disagree with this part:

Again, I highly recommend that you simply bind to a scope variable instead of checking for the presence of a class further up the tree.

I really find it both complicated and error-prone: I'm having a big form in which many fields already have their readonly logic (e.g., bank name is read only when a known BIC is entered). Now, I'd have to add is_readonly to all existing ng-readonly directives and add the directi

Solution

Generally speaking, you should avoid looking outside the current directive for anything that would affect your internal state (bar events). The preferred method of communication is scopes. Here is how I would recommend you do this.


    
        
    
    
        
            
        
    


Simply toggling $scope.is_readonly from MyCtrl is enough to disable all child inputs that have ng-readonly="is_readonly", and it's bindable too, so you can add/remove readonly status at runtime.

Now, if there is a genuine need to have the behaviour you described, then you have 2 options.

-
Stick with your current implementation, but this will not be performant, since your DOM traversal will fire any time the scope changes, for any reason, anywhere up the scope tree.

-
Rewrite to use directive's require property.

Here is how you would do the latter.

.directive("readonly", function() {
    return {
        restrict: "C",
        link: function() {},
        controller: function() {},
    }
})
.directive("input", function()
    return {
        restrict: "E",
        // search for optional parent directive named `readonly`
        require: "^?readonly",
        // 4th parameter is `readonly` controller or null.
        link: function($scope, element, attr, has_readonly) {
            var org = attr.ngReadonly;
            if (org) {
                return; // readonly seems to be set manually, so let's not touch it
            } else if (has_readonly) {
                attr.$set("readonly", true)
            }
        },
    };
})


This tells angular that it should pass you a reference to the readonly controller if found, or null otherwise. In this way, you can tell at link time whether the parent node exists. This saves you from performing the DOM traversal yourself, and should be faster too.

Again, I highly recommend that you simply bind to a scope variable instead of checking for the presence of a class further up the tree.

Code Snippets

<div ng-controller="MyCtrl">
    <div>
        <input ng-readonly="is_readonly" />
    </div>
    <div>
        <div>
            <input ng-readonly="is_readonly" />
        </div>
    </div>
</div>
.directive("readonly", function() {
    return {
        restrict: "C",
        link: function() {},
        controller: function() {},
    }
})
.directive("input", function()
    return {
        restrict: "E",
        // search for optional parent directive named `readonly`
        require: "^?readonly",
        // 4th parameter is `readonly` controller or null.
        link: function($scope, element, attr, has_readonly) {
            var org = attr.ngReadonly;
            if (org) {
                return; // readonly seems to be set manually, so let's not touch it
            } else if (has_readonly) {
                attr.$set("readonly", true)
            }
        },
    };
})

Context

StackExchange Code Review Q#57515, answer score: 7

Revisions (0)

No revisions yet.