spfx-extension

How to add an event listener to an element that is created from a promise in SPFx


I am following the Microsoft tutorial for creating an Application Customizer. That works great, but now I would like to add some custom things like a curtain menu to my site.

I can't seem to figure out how to add an event listener to an element that is being rendered from a promise. The element doesn't exist initially, so if I try the ID, I get a null error. If I use a class it gives me an inline script error.

I've searched for a solution, but don't see one that applies to what I am doing.

I know this code is a mess, but I've been trying so many different methods, I can't seem to find one that works.

import { override } from '@microsoft/decorators';
import { Log } from '@microsoft/sp-core-library';
import {
  BaseApplicationCustomizer,
  PlaceholderContent,
  PlaceholderName,
  PlaceholderProvider
} from '@microsoft/sp-application-base';
import { Dialog } from '@microsoft/sp-dialog';

import * as strings from 'HideSideNavApplicationCustomizerStrings';
// import * as strings from './myStrings';
import styles from './AppCustomizer.module.scss';
import {escape} from '@microsoft/sp-lodash-subset';
import Placeholder from '@microsoft/sp-application-base/lib/extensibility/placeholder/Placeholder';

const LOG_SOURCE: string = 'HideSideNavApplicationCustomizer';
let openBtn = "openBtn";

export interface IHideSideNavApplicationCustomizerProperties {
  // This is an example; replace with your own property
  testMessage: string;
  Top: string;
  openBtn: string;
}

/** A Custom Action which can be run during execution of a Client Side Application */
export default class HideSideNavApplicationCustomizer
  extends BaseApplicationCustomizer<IHideSideNavApplicationCustomizerProperties> {

    private _topPlaceholder: PlaceholderContent | undefined;

  @override
  public onInit(): Promise<void> {  
    this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
    return Promise.resolve();
  }

  private _renderPlaceHolders(): void {
    console.log('calling _renderPlaceHolders');
    console.log(
      "Available placeholders: ",
      this.context.placeholderProvider.placeholderNames
      .map(name => PlaceholderName[name])
      .join(", ")
      );

    if(!this._topPlaceholder){
      this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
        PlaceholderName.Top,
        { onDispose: this.onDispose}
      )
    }

    if(!this._topPlaceholder) {
      console.error("The expected placeholder (Top) was not found.");
      return;
    }
    
    if(this.properties){
      let topString: string = `
      <!-- The overlay -->
      <div id="myNav" class="navClose overlay">
      
        <!-- Button to close the overlay navigation -->
        <a href="javascript:void(0)" class="closebtn" onclick="closeNav()">&times;</a>
      
        <!-- Overlay content -->
        <div class="overlay-content">
          <a href="#">About</a>
          <a href="#">Services</a>
          <a href="#">Clients</a>
          <a href="#">Contact</a>
        </div>
      
      </div>
      
      <!-- Use any element to open/show the overlay navigation menu -->
      <span class="navOpen">open</span>

      `;

      if(!topString){
        topString = "(Top property was not defined.)";
      }
    
      if(this._topPlaceholder.domElement){
        this._topPlaceholder.domElement.innerHTML=topString;
        let navState :string = "closed";
        const navClose = document.getElementsByClassName("navClose").item(0);
        this._topPlaceholder.domElement.addEventListener("click", function(e){
          
          if(navState == "closed"){
            navClose.setAttribute(this.style.width, "100%");
            navState = "opened";
          }
          else{
            navClose.setAttribute(this.style.width, "0");
            navState = "closed";
          }        
        });
      } 
    }
  }

  private _onDispose(): void {
    console.log('[HideSideNavApplicationCustomizer._onDispose] Dispose custom top and bottom.')
  }
}

Solution

  • So I think I finally figured this out. I wasn't using 'this' correctly after the promise was made, and there were a number of syntax errors with how I was trying to update the attributes on the elements.

    Here's my updated code in case anyone is trying to do something similar.

    import { override } from '@microsoft/decorators';
    import { Log } from '@microsoft/sp-core-library';
    import {
      BaseApplicationCustomizer,
      PlaceholderContent,
      PlaceholderName,
      PlaceholderProvider
    } from '@microsoft/sp-application-base';
    import styles from './AppCustomizer.module.scss';
    
    const LOG_SOURCE: string = 'HideSideNavApplicationCustomizer';
    let openBtn = "openBtn";
    
    export interface IHideSideNavApplicationCustomizerProperties {
      // This is an example; replace with your own property
      testMessage: string;
      Top: string;
    }
    
    /** A Custom Action which can be run during execution of a Client Side Application */
    export default class HideSideNavApplicationCustomizer
      extends BaseApplicationCustomizer<IHideSideNavApplicationCustomizerProperties> {
    
        private _topPlaceholder: PlaceholderContent | undefined;
    
      @override
      public onInit(): Promise<void> {  
        this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
        return Promise.resolve();
      }
    
      private _renderPlaceHolders(): void {
        console.log('calling _renderPlaceHolders');
        console.log(
          "Available placeholders: ",
          this.context.placeholderProvider.placeholderNames
          .map(name => PlaceholderName[name])
          .join(", ")
          );
    
        if(!this._topPlaceholder){
          this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
            PlaceholderName.Top,
            { onDispose: this.onDispose}
          )
        }
    
        if(!this._topPlaceholder) {
          console.error("The expected placeholder (Top) was not found.");
          return;
        }
        
        if(this.properties){
          let topString: string = `
          <!-- The overlay -->
          <div id="nav" class="${styles.overlay}">
          
            <!-- Button to close the overlay navigation -->
            <a href="javascript:void(0)" id="navClose" class="${styles.closebtn}">&times;</a>
          
            <!-- Overlay content -->
            <div class="${styles['overlay-content']}">
              <a href="#">About</a>
              <a href="#">Services</a>
              <a href="#">Clients</a>
              <a href="#">Contact</a>
            </div>
          
          </div>
          
          <!-- Use any element to open/show the overlay navigation menu -->
          <span id="navOpen">open</span>
          `;
        
          if(!topString){
            topString = "(Top property was not defined.)";
          }
        
          if(this._topPlaceholder.domElement){
            const top = this._topPlaceholder.domElement;
            top.innerHTML=topString;
            
            let nav = top.querySelector('#nav');
            let navOpen = top.querySelector('#navOpen');
            let navClose = top.querySelector('#navClose');
    
            navOpen.addEventListener("click", () => {
              nav.setAttribute("style","width:75%;");
            });
    
            navClose.addEventListener("click", () => {
              nav.setAttribute("style","width:0%;");
            });
          }
        }
      }
    
      private _onDispose(): void {
        console.log('[HideSideNavApplicationCustomizer._onDispose] Dispose custom top and bottom.')
      }
    }