12
\$\begingroup\$

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 directive to every input field missing it.

Moreover, the form includes some partials, which have no idea about the outer scope. Again, I'd have to add change the partial as above and make it to get is_readonly from the outer scope.

\$\endgroup\$
  • 1
    \$\begingroup\$ I haven't worked with angular, so I can't read this syntax, but +1 for a simple, well explained review request. \$\endgroup\$ – Viziionary Jul 20 '14 at 19:59
  • \$\begingroup\$ org is undefined \$\endgroup\$ – Pierre Menard Jul 23 '14 at 18:38
  • \$\begingroup\$ @PierreMenard Fixed, sorry for that. \$\endgroup\$ – maaartinus Jul 23 '14 at 23:30
  • \$\begingroup\$ Partials can access outer scope. ng-include creates a new child scope for the included fragment. Also, instead of having ng-readonly interrogate scope variables directly, have it call a function on your scope. Then you can have arbitrarily complicated logic without encoding it in your templates. \$\endgroup\$ – Thomas Jul 24 '14 at 2:54
  • \$\begingroup\$ I came back to say much of what is in Thomas's answer. Doing the work in a $watch seems un-idiomatic and non-performant. The readonly class is being applied based on a model update, you should hook into that. \$\endgroup\$ – Pierre Menard Jul 24 '14 at 15:38
7
+50
\$\begingroup\$

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.

<div ng-controller="MyCtrl">
    <div>
        <input ng-readonly="is_readonly" />
    </div>
    <div>
        <div>
            <input ng-readonly="is_readonly" />
        </div>
    </div>
</div>

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.

  1. 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.

  2. 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.

\$\endgroup\$
  • \$\begingroup\$ I disagree with the scope variable proposal (see my updated question), but I like the idea using require. I'll try to get it working. \$\endgroup\$ – maaartinus Jul 24 '14 at 1:19
  • \$\begingroup\$ I couldn't get it working and now I know why: At the enclosing element, I'm using ng-readonly which adds the readonly attribute later. And I know now, why I don't like using a scope variable: There are two parts of the form, each of them dependent on its own expression. Expressing this in the DOM is trivial, expressing it on every input is ugly, and having two different $scope.is_readonly would be even uglier. But maybe creating a scope for each part is the way to go? \$\endgroup\$ – maaartinus Aug 15 '14 at 5:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.