1

I'm pretty new to angular, so I hope this question isn't too basic. I'm trying to run a function in angular, but I seem to have an issue with the scope of my variables. This is what it looks like:

export class Skulder14q1Component {

  button1 = document.getElementById('button1');
  button2 = document.getElementById('button2');
  button3 = document.getElementById('button3');
  button4 = document.getElementById('button4');
  showallbutton = document.getElementById('showallbutton');

  onShowAllOptions() {
    this.button1.classList.toggle('hide');
    this.button2.classList.toggle('hide');
    this.button3.classList.toggle('hide');
    this.button4.classList.toggle('hide');
    this.showallbutton.classList.add('hide');
  }

Now the function is run by the press of a button, but when pressing the button, the console log returns this:

> Skulder14q1Component.html:12 ERROR TypeError: Cannot read property 'classList' of null
    at Skulder14q1Component.onShowAllOptions (skulder14q1.component.ts:28)
    at Object.eval [as handleEvent] (Skulder14q1Component.html:15)
    at handleEvent (core.js:10251)
    at callWithDebugContext (core.js:11344)
    at Object.debugHandleEvent [as handleEvent] (core.js:11047)
    at dispatchEvent (core.js:7710)
    at core.js:8154
    at HTMLButtonElement.<anonymous> (platform-browser.js:988)
    at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
    at Object.onInvokeTask (core.js:3811)

Changing to this code instead works:

onShowAllOptions() {
  const button1 = document.getElementById('button1');
  const button2 = document.getElementById('button2');
  const button3 = document.getElementById('button3');
  const button4 = document.getElementById('button4');
  const showallbutton = document.getElementById('showallbutton');
  button1.classList.toggle('hide');
  button2.classList.toggle('hide');
  button3.classList.toggle('hide');
  button4.classList.toggle('hide');
  showallbutton.classList.add('hide');
}

I feel like I'm missing something basic, and I hope you can help me out. Thanks!

1 Answer 1

1

First of all, I would recommend against managing the DOM too much through standard browser methods like document.getElementById, etc. Using the Angular abstraction is recommended, you can read more here.

This out of the way, we can get to the why.

When the Skulder14q1Component is initialized, the DOM (the HTML elements / the template) for it has not been created yet. So when you try to find the buttons, they are not there yet, so getElementById returns null.

Components in Angular go through a life-cycle, and you can hook into this life-cycle by adding certain methods to your class. you can read more about those in the docs on lifecycle hooks.

In your case, you need the AfterViewInit hook. To implement it, we need to do two things:

  1. implement the interface AfterViewInit

    class Skulder14q1Component implements AfterViewInit {
    ...
    }
    
  2. Add the method

    class Skulder14q1Component implements AfterViewInit {
        button1: HTMLElement;
        button2: HTMLElement;
        button3: HTMLElement;
        button4: HTMLElement;
        showAllButton: HTMLElement;
    
        ngAfterViewInit() {
            this.button1 = document.getElementById('button1');
            this.button2 = document.getElementById('button2');
            this.button3 = document.getElementById('button3');
            this.button4 = document.getElementById('button4');
            this.showallbutton = document.getElementById('showallbutton');
        }
    
        // when this is called later, the buttons will have been set up
        onShowAllOptions() {
            this.button1.classList.toggle('hide');
            this.button2.classList.toggle('hide');
            this.button3.classList.toggle('hide');
            this.button4.classList.toggle('hide');
            this.showallbutton.classList.add('hide');
        }
    }
    

The code in the method will run after Angular has prepared the template for you.


Edit: In my experience, ViewChild is not used that often, since you want the elements inside your templates (especially if they are other components) to be responsible for their own behaviour, and not control everything from an omnipotent class.

Here is a StackBlitz I made for your use-case precisely.

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

7 Comments

Interesting! I'll take a look at angular abstraction. However, how do I call the method on click, if it's placed in ngAfterViewInit() ?
@ThomasJensen I updated the template with the function. I assume that you had a (click) handler in your template, so you can still use that. The life-cycle hook is only there to initialize the properties.
Wow it works! And your explanation was perfect, thank you! And what I also got from your message, was that it is better to tap into the lifecycle hooks, instead of manipulating the DOM through the normal methods. Is that correct?
@ThomasJensen You're welcome. Lifecycle hooks allow you to run your custom functions at certain points of components life. E.g. onInit to fetch some data, onChanges to update some state. The abstractions over DOM are a completely separate thing. Typically if you want to access an element inside you component's template, you would use ViewChild on a property to let angular initialize it for you at the right time.
Perfect, ViewChild might be just what I need. Thank you again!
|

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.