How do I dynamically set attributes on the HTML element or add HEAD/(LINK|TITLE) elements in Angular

669 views Asked by At

I have an Angular 12 application that I am trying to make work with both LTR and RTL languages (e.g. English and Arabic respectively) which can be chosen and applied by the end user when using the application. Bootstrap - which I use for UI style - requires that I set the dir attribute on the HTML element and that I reference a separate RTL CSS file from the original LTR CSS file when I want to display Arabic text.

I thought I would be able to set up my index.html like this:

<app-root></app-root>

And then have my app-component.html generate the HTML, HEAD, LINK and BODY elements, etc. like this:

<!doctype html>
<html [lang]="lang" [dir]="dir">

<head>
  <meta charset="utf-8">
  <title>{{title}}</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link *ngIf="dir != 'rtl'" href="/assets/styles/bootstrap.css" rel="stylesheet" />
  <link *ngIf="dir == 'rtl'" href="/assets/styles/bootstrap.rtl.css" rel="stylesheet" />
  <script type="text/javascript" src="/assets/lib/jquery/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="/assets/scripts/bootstrap5.min.js"></script>
</head>

<body>
  <router-outlet></router-outlet>
</body>

</html>

However, when I attempted this layout, the Bootstrap menus (and other JavaScript action related styles) stopped working. I noticed that now, when Chrome fetches index.html, Angular has inserted its bootstrapping scripts ahead of the app-root element.

<link rel="stylesheet" href="styles.css">
<script src="runtime.js" defer></script>
<script src="polyfills.js" defer></script>
<script src="styles.js" defer></script>
<script src="vendor.js" defer></script>
<script src="main.js" defer></script>
<app-root></app-root>

I also noticed from the DevTools Elements view that Chrome had nested my HTML element inside the app-root element of a document of its own creation.

<html>
  <head>
    <link rel="stylesheet" href="styles.css">
    <script src="runtime.js" defer=""></script>
    <script src="polyfills.js" defer=""></script>
    <script src="styles.js" defer=""></script>
    <script src="vendor.js" defer=""></script>
    <script src="main.js" defer=""></script>
  </head>
  <body>
    <app-root _nghost-tyi-c80="" ng-version="12.0.5">
      <html _ngcontent-tyi-c80="" lang="en" dir="ltr">
        <head _ngcontent-tyi-c80="">
          <meta _ngcontent-tyi-c80="" charset="utf-8">
          ...
        </head>
        ...
      </html>
    </app-root>
  </body>
</html>

Snip of the Elements view in DevTools

If I revert my change, then Chrome fetches index.html and I see that Angular inserts its bootstrapping scripts after the app-root element and before the closing body tag. Bootstrap likes this and the menus start working again but I lose control over dynamically choosing which LTR or RTL CSS link gets emitted, and whether the HTML element has meaningful LANG and DIR attributes, and any other changes to HEAD elements like the page title.

Original index.html

<!doctype html>
<html lang="en" dir="ltr">

<head>
  <meta charset="utf-8">
  <title>Snappy Title Goes Here</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="/assets/styles/bootstrap.css" rel="stylesheet" />
  <script type="text/javascript" src="/assets/lib/jquery/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="/assets/scripts/bootstrap5.min.js"></script>
</head>

<body>
  <app-root></app-root>
</body>

</html>

What Chrome sees:

<!doctype html>
<html lang="en" dir="ltr">

<head>
  <meta charset="utf-8">
  <title>Snappy Title Goes Here</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link href="/assets/styles/bootstrap.css" rel="stylesheet" />
  <script type="text/javascript" src="/assets/lib/jquery/jquery-3.3.1.min.js"></script>
  <script type="text/javascript" src="/assets/scripts/bootstrap5.min.js"></script>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <app-root></app-root>
  <script src="runtime.js" defer=""></script>
  <script src="polyfills.js" defer=""></script>
  <script src="styles.js" defer=""></script>
  <script src="vendor.js" defer=""></script>
  <script src="main.js" defer=""></script>
</body>

</html>

What is the correct way to dynamically alter these HTML and HEAD-related attributes and elements so that Chrome doesn't get confused?

1

There are 1 answers

0
Bidyn On

You could go with leaving original index.html with one change, add tag dir-change to bootstrap stylesheet which will allow later to find it.

<link href="/assets/styles/bootstrap.css" dir-change/>

then in app.component.ts add method that will update this link and direction

 changeDirection(dir: 'ltr'| 'rtl'): void {
   const attrName = 'dir-change'
   const originalLink = document.querySelector(`[${attrName}]`);
   const linkAddress = dir === 'ltr' ?
    "/assets/styles/bootstrap.css"
    :"/assets/styles/bootstrap.rtl.css";
   (originalLink as HTMLLinkElement).href = linkAddress;
   document.dir = dir;
 }