skip to Main Content

I am running a project in Angular 9.1.13, on Centos 7.

I have a component with static HTML code, as follows:

<ul id="myUL">
   <li><span class="caret">Top 1</span>
      <ul class="nested">
         <li>Sub 1</li>
         <li>Sub 2</li>
      </ul>
   </li>
</ul>

My CSS looks as follows:

ul, #myUL {
  list-style-type: none;
}

#myUL {
  margin: 0;
  padding: 0;
}

.caret {
  cursor: pointer;
  user-select: none;
}

.caret::before {
  content: "25B6";
  display: inline-block;
  margin-right: 6px;
}

.caret-down::before {
  transform: rotate(90deg);
}

.nested {
  display: none;
}

.active {
  display: block;
}

And my component executes this code in the ngOnInit() method:

...
const toggler = document.getElementsByClassName('caret');
let i;
     
for (i = 0; i < toggler.length; i++) {
   toggler[i].addEventListener('click', function() {
      this.parentElement.querySelector('.nested').classList.toggle('active');
      this.classList.toggle('caret-down');      
   });
}
...

With things as described, everything works just fine. I can click that red icon, and the sublist can be shown or hidden:

enter image description here

The problem comes however if I have to generate the content of the list dynamically.

In order to do so, the HTML looks as follows:

<ul id="myUL"></ul> <!-- Deliberately empty unordered list -->

On the ngOnInit() method above, and before the code I had already mentioned, I prepend this other chunk of code:

...
const myUL = document.getElementById('myUL');

myUL.innerHTML = 
   '<li><span class="caret">Top 1</span><ul class="nested"><li>Sub 1</li><li>Sub 2</li></ul></li>';

// The rest of the code mentioned above
...

With that code, things don’t look the same. The sublist is not collapsible anymore, styles are not applied… although surprisingly, click events are actually attached to the caret elements:

enter image description here

So, the question is… how can I achieve the same result as in the first screenshot, but dynamically generating the list content.

3

Answers


  1. You should use ng-deep and disable scope styles for elements, because you are trying to create these elemenets by yourself

    #myUL ::ng-deep {
      .caret {
        cursor: pointer;
        user-select: none;
      }
    
      .caret::before {
        content: "25B6";
        display: inline-block;
        margin-right: 6px;
      }
    
      .caret-down::before {
        transform: rotate(90deg);
      }
    
      .nested {
        display: none;
      }
    
      .active {
        display: block;
      } 
    
    }
    
    Login or Signup to reply.
  2. You can generate the list dynamically in the html using angular. I would setup my a caret object / list in my component and then take advantage of *ngFor and [ngClass] to achieve what you’re trying to do. Something like:

    component.ts

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.scss'],
    })
    export class AppComponent {
      carets = [
        {
          name: 'Top 1',
          children: [{ name: 'Sub 1' }, { name: 'Sub 2' }],
          toggle: false,
        },
        {
          name: 'Top 2',
          children: [{ name: 'Sub 1' }, { name: 'Sub 2' }],
          toggle: false,
        },
      ];
    }
    

    component.html

    <ul id="myUL">
      <li *ngFor="let caret of carets" [ngClass]="caret.toggle ? 'active' : ''" >
        <span
          class="caret"
          [ngClass]="caret.toggle ? 'caret-down' : ''"
          (click)="caret.toggle = !caret.toggle"
          >{{ caret.name }}</span
        >
        <ul [ngClass]="caret.toggle ? '' : 'nested'">
          <li *ngFor="let child of caret.children">{{ child.name }}</li>
        </ul>
      </li>
    </ul>
    
    Login or Signup to reply.
  3. Disclaimer: this answer only applies if you are required to inject html as a string. For example if you receive the string from an api. Otherwise you should be using Angular directives to generate the content.

    This is because Angular uses View Encapsulation. It does this by generating a unique identifier and adding it as an attribute to all of your html elements in the component, something like _ngcontent-nxs-c94="". Then it appends [_ngcontent-nxs-c94] to all of your css declarations inside the component. Inspect your elements and you’ll see what I mean.

    If you’re injecting html after the component has been compiled, that html will obviously not have the attribute and your css will not affect it. You’re not meant to inject html like this, since Angular provides all of the tools to generate content dynamically. But if it’s absolutely required then you just need to add the CSS globally (ie. it does not get the unique attribute appended). Any file in the styles array located in angular.json will be compiled as a global style, by default styles.css is the only file here.

    So just add your css to a file that that is in the styles array. Either in styles.css or create your own (you can make it right next to your original css file). But this brings us back to the days before View Encapsulation, so we need to make sure the selectors are unique. That’s pretty simple since you can just wrap all of the css in your component’s html tag. Keep in mind this will affect any other nested components so you may need to add some more unique class names.

    In a global style file

    app-my-component {
      ul,
      #myUL {
        list-style-type: none;
      }
    
      #myUL {
        margin: 0;
        padding: 0;
      }
    
      .caret {
        cursor: pointer;
        user-select: none;
      }
    
      .caret::before {
        content: "25B6";
        display: inline-block;
        margin-right: 6px;
      }
    
      .caret-down::before {
        transform: rotate(90deg);
      }
    
      .nested {
        display: none;
      }
    
      .active {
        display: block;
      }
    }
    

    So this lets you apply global styles to a component while still maintaining view encapsulation within your original css file. The other option is to disable view encapsulation for the component, but I wouldn’t recommend, since there may be css that you do want encapsulated.

    Disabling View Encapsulation (not recommended)

    @Component({
      selector: 'app-one',
      templateUrl: './one.component.html',
      styleUrls: ['./one.component.scss'],
      encapsulation: ViewEncapsulation.None
    })
    export class OneComponent {
      ...
    }
    

    That’ll compile ./one.component.scss globally, just the same as if it were in the styles array. If you do this, make sure the css isn’t going to affect other components.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search