15

I've been reading some articles about Angular 2 pitfalls and what to avoid, one of those things revolves around not accessing the DOM directly.

I noticed that the Renderer is quite useful since it contains some methods that can help avoid the DOM pitfall. However, I noticed that it doesn't contain any get functions, only set functions such as setElementAttribute, setElementClass and so on.

So my question is rather simple, how do you use the above functions but as the get and remove version? Do they live in another class or how do you work with retrieving attributes or classes for example?

Chrillewoodz
  • 27,055
  • 21
  • 92
  • 175

7 Answers7

22

To remove attributes from the DOM you provide a value of null.

To set an attribute (attribute value can be an empty string if you wish):

myrenderer.setElementAttribute(elementRef.nativeElement, 'attributename', 'attributevalue');

To remove an attribute:

myrenderer.setElementAttribute(elementRef.nativeElement, 'attributename', null);

To get an element attribute value, you have the nativeElement which you pass to setElementAttribute, so you can use that to get the attribute value using standard Javascript:

elementRef.nativeElement.getAttribute('attributename');
David
  • 1,179
  • 12
  • 15
10

Angular2 doesn't provide any support to get anything from the DOM except ElementRef and events.
The Angular2 way is to maintain the state in the model and update the DOM to reflect that state.

If you need to read from the DOM you can use direct DOM access or provide a custom Renderer that provides the features you're missing in the default Renderer.

Examples for custom renderers

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Is there a way to extend the Renderer? And would that be a good idea? Or would it be better to create a completely custom one? – Chrillewoodz Jun 26 '16 at 15:23
  • I didn't have a closer look at the `Renderer` yet, but I'm pretty sure extending is a good idea. – Günter Zöchbauer Jun 26 '16 at 16:17
  • Ok, I will have a look. However there is this one scenario which I wonder what the correct way is, let's say I have a function which receives an `$event` as parameter. Then I use `$event.target.getAttribute('some-attr')` to get an attribute from the target element. Is this the correct way to do it when not having access to the `nativeElement` of the target? Worth noting is that I've bound the event listener itself using `Renderer` in the first place which is where the event comes from. – Chrillewoodz Jun 26 '16 at 16:21
  • I'm not sure. It's IMHO also direct DOM access which is generally discouraged, but maybe it's fine with events. – Günter Zöchbauer Jun 26 '16 at 16:24
  • I know, that's what giving me a headache on how to actually do things the right way.. – Chrillewoodz Jun 26 '16 at 16:26
  • Why did you get into the situation that you need to read from the DOM? – Günter Zöchbauer Jun 26 '16 at 16:30
  • Well I have a component which is basically a menu with transcluded items, and each of these items needs to have an active state, but since this is, from what I've tried before, hard to achieve with transcluded items bound to the component model I rely on setting an active attribute instead. Would prefer to go in a different direction however. – Chrillewoodz Jun 26 '16 at 16:36
  • I'm sure this is an issue the Angular team is going to address rather sooner than later. In the meantime I would just use what works and switch to new features when they become available. You could for example use `dispatchEvent()` to fire bubbling events from transcluded elements. – Günter Zöchbauer Jun 26 '16 at 16:46
  • I hope they do. Anyways, back to the question at hand. I found an example which shows how to extend `DomRenderer` (http://www.yearofmoo.com/2016/02/rendering-in-angular2.html). I also looked in the source code for angular and found this: https://github.com/angular/angular/blob/master/modules/%40angular/platform-browser/src/dom/dom_renderer.ts and this api which exposes it: https://github.com/angular/angular/blob/master/modules/%40angular/core/src/render/api.ts, so if you extend `DomRenderer` I must also extend `Renderer` with new `abstract`s right? – Chrillewoodz Jun 26 '16 at 18:14
  • 1
    If you're interested in creating an example with new get/remove attribute classes I could reward you with the bounty. – Chrillewoodz Jun 26 '16 at 18:15
  • This guy is everywhere answering questions lol You go to Java, Node, Angular, everywhere. – eyoeldefare Feb 10 '18 at 02:51
  • @EyoelD Actually I care mostly about Dart but the people often add weird tags to their questions ;p – Günter Zöchbauer Feb 10 '18 at 14:52
4

In case someone is still looking for this (as I did), i shall add up a bit on David's answer which was on Angular's native renderer.

You have all this requested functionality in newest Angular Renderer2

Particularly if you want to completely remove attributes (ex. invalid aria tags in community components that fail accessibility tests) from elements and not set their value to null, there is

renderer2.removeAttribute(elementRef.nativeElement, 'AttributeName');

EDIT: You should use AfterViewInit() lifecycle, as described in other answers, as the initial view must be rendered before you make any custom DOM changes.

Kostis Tr
  • 71
  • 4
3

I don't like accessing the dom in Angular but this use case you may need to. The only way to disable the annoying auto complete seems to be to add the attribute "readonly" and remove it after the form loads.

ngAfterViewInit() {
      window.setTimeout(function () {

         var arr: HTMLCollection = document.getElementsByClassName('form-control');
         for (var i = 0; i < arr.length; i++) {
           if (arr[i].hasAttribute("readonly")) {
             arr[i].removeAttribute('readonly');
           }
         }

   }, 500);
}
RandallTo
  • 395
  • 2
  • 11
  • Why are you using ngOnInit *and* setting a time out? You could use another Angular Lyfecycle Hook, like ngDoCheck for example. – Juan Sánchez Jul 04 '18 at 15:10
  • 1
    judasane: Thanks! The AfterViewInit or AfterContentInit does the trick without using the timeout. The DoCheck gets called more than once so it is the reason for using AfterViewInit as that seems to be the last or near to last event of the life cycle that gets called once. New to angular 5, still learning. – RandallTo Jul 05 '18 at 16:48
  • judasane: I had commented too soon. I does appear that chrome, and possibly other browsers, implement their auto complete a 1/2 second later after dom writing angular performs. It is nothing to do with angular but just the nature of the browser feature as to the timing of inserting auto complete text. It appears the only mechanism to defeat this is using a timeout. – RandallTo Jul 05 '18 at 17:30
  • also ` var arr: HTMLCollection` should be ` const arr: HTMLCollection ` and `for (var i = 0; i < arr.length; i++) {` should be `for (let i = 0; i < arr.length; i++) {`. I also added a `disable-autocomplete` CSS class and iterated through that, removing the `readonly` attribute, to speed things up a bit. Also made the background-color of the fields white so as not to get a flash as the readonly attribute gets removed. – Yvonne Aburrow Mar 15 '19 at 16:21
  • @RandallTo - Still works in 2020. Absofreakinglutely awesome! Had to remove hard-coded width and height from images to make them responsive. – Shawn Spencer Aug 03 '20 at 23:14
  • @RandallTo hasAttribute is always returning false, but i can see those attributes when printing on console. Could you please let me know what can be done here. I am trying to remove "aria-expanded" attribute. – Alii Sep 21 '22 at 05:04
2

Since getAttribute is just a method, you could use invokeElementMethod:

var attr = renderer.invokeElementMethod(elementRef.nativeElement, 'getAttribute', []);

This approach will not work if you switch to server-side rendering (except event callbacks like mouse click).

Extending DOMRenderer effectively means tight coupling to browser implementation, which is the same as direct nativeElement manipulation.


It seems that you should not invoke getters at all. So the question is why do you need to know attribute value or class name?

You could create specific directive or template variable and use it with ViewChild/ViewChildren, or create appropriate data model and bind with [class.name]="nameEnabled"

kemsky
  • 14,727
  • 3
  • 32
  • 51
  • Hmm, I'm intrigued. I'll try this out tomorrow and see if it'll do the job. It would solve the issue I discussed with Gunter with event target manipulation. – Chrillewoodz Jun 26 '16 at 20:23
  • What about `classList` and other similar properties though? – Chrillewoodz Jun 26 '16 at 20:56
  • `classList` could be emulated using `class` attribute. – kemsky Jun 26 '16 at 21:46
  • yeah, I don't know why would you want to do that. If your DOM element needs a special state that you want to set/get using DOM attr/data then probably it's an edge case and you want to create a custom directive for it. – Tiberiu Popescu Jun 26 '16 at 22:29
  • I thought of this as well but `invokeElementMethod` doesn't return anything https://github.com/angular/angular/issues/8386 (not checked myself since the issue was created). – Günter Zöchbauer Jun 27 '16 at 04:24
  • indeed, values are not returned, motivation is serialization issues with web workers. – kemsky Jun 27 '16 at 09:03
2

Solution based on @RandallTo 's answer above.

Angular

ngAfterViewInit() {
      window.setTimeout(function () {

         const arr: HTMLCollection = document.getElementsByClassName('disable-autocomplete');
         for (let i = 0; i < arr.length; i++) {
             arr[i].removeAttribute('readonly');
         }

   }, 500);
}

HTML

<input type="text" name="username" readonly="" class="form-control disable-autocomplete"/>

CSS

.disable-autocomplete {
  background-color: #fff;
}

Adding the white background colour means that you won't get a flash as the form loads with readonly fields (which are grey by default) which then turn white when the readonly attribute is removed.

You don't need the if statement in my version because you only set readonly and .disable-autocomplete on the fields for which you want to disable autocomplete.

For example you might want to allow autocomplete on the email field but not in the username field.

Yvonne Aburrow
  • 2,602
  • 1
  • 17
  • 47
  • hasAttribute is always returning false, but i can see those attributes when printing on console. Could you please let me know what can be done here. I am trying to remove "aria-expanded" attribute – Alii Sep 21 '22 at 05:07
  • you could add `arr[i].removeAttribute('aria-expanded');` after `arr[i].removeAttribute('readonly');` – Yvonne Aburrow Sep 22 '22 at 17:04
0

To remove a class, you still can use setElementClass, the isBool should be set to false. See this link for more info https://github.com/angular/angular/blob/9de76ebfa545ad0a786c63f166b2b966b996e64c/modules/%40angular/platform-browser/src/dom/dom_renderer.ts#L237

Tuong Le
  • 18,533
  • 11
  • 50
  • 44