Search code examples
domecmascript-6polymerpolymer-2.xdom-events

Polymer 2 - Perform action every time element is shown via iron-pages/iron-selector


I'm attempting to create a logout page that will work even after that element has been attached once to the DOM. This occurs when you get a login, then logout, then login again, and attempt to log back out.

For instance, the shell has

<iron-selector selected="[[page]]" attr-for-selected="name">
  <a name="logout" href="[[rootPath]]logout">
    <paper-icon-button icon="my-icons:sign-out" title="Logout" hidden$="[[!loggedIn]]"></paper-icon-button>
  </a>
  <a name="login" href="[[rootPath]]login">
    <paper-icon-button icon="my-icons:sign-in" title="Login" hidden$="[[loggedIn]]"></paper-icon-button>
  </a>
</iron-selector>

<<SNIP>>

<iron-pages selected="[[page]]" attr-for-selected="name" fallback-selection="view404" role="main">
  <my-search name="search"></my-search>
  <my-login name="login"></my-login>
  <my-logout name="logout"></my-logout>
  <my-view404 name="view404"></my-view404>
</iron-pages>

I also have an observer for page changes in the shell:

static get observers() {
  return [
    '_routePageChanged(routeData.page)',
  ];
}

_routePageChanged(page) {
  this.loggedIn = MyApp._computeLogin();
  if (this.loggedIn) {
    this.page = page || 'search';
  } else {
    window.history.pushState({}, 'Login', '/login');
    window.dispatchEvent(new CustomEvent('location-changed'));
    sessionStorage.clear();
    this.page = 'login';
  }
}

This works well as when I click on the icon to logout, it attaches the my-logout element just fine and performs what in ready() or connectedCallback() just fine.

my-logout has

ready() {
  super.ready();
  this._performLogout();
}

The issue comes when, without refreshing the browser and causing a DOM refresh, you log back in and attempt to log out a second time. Since the DOM never cleared, my-logout is still attached, so neither ready() nor connectedCallback() fire.

I've figured out a way of working around this, but it feels very kludgy. Basically, I can add an event listener to the element that will perform this._performLogout(); when the icon is selected:

ready() {
  super.ready();
  this._performLogout();

  document.addEventListener('iron-select', (event) => {
    if (event.detail.item === this) {
      this._performLogout();
    }
  });
}

Like I said, it works, but I dislike having a global event listener, plus I have to call the logout function the first time the element attaches and I have to listen as the listener isn't active till after the first time the element is attached.


Solution

  • There does not appear to be a "one size fits all" solution to this. The central question is, "Do you want the parent to tell the child, or for the child to listen on the parent?". The "answer" I came up with in the question works if you want to listen to the parent, but because I don't like the idea of a global event listener, the below is how to use <iron-pages> to tell a child element that it has been selected for viewing.

    We add the selected-attribute property to <iron-pages>:

    <iron-pages selected="[[page]]" attr-for-selected="name" selected-attribute="selected" fallback-selection="view404" role="main">
      <my-search name="search"></my-search>
      <my-login name="login"></my-login>
      <my-logout name="logout"></my-logout>
      <my-view404 name="view404"></my-view404>
    </iron-pages>
    

    Yes, this looks a little confusing considering the attr-for-selected property. attr-for-selected says, "What attribute should I match on these child elements with the value of my selected property?" So when I click on

    <iron-selector selected="[[page]]" attr-for-selected="name">
      <a name="logout" href="[[rootPath]]logout"><paper-icon-button icon="my-icons:sign-out" title="Logout" hidden$="[[!loggedIn]]"></paper-icon-button></a>
    </iron-selector>
    

    it will set the <my-logout> internally as the selected element and display it. What selected-attribute="selected" does is to set an attribute on the child element. If you look in the browser JS console, you will see that the element now looks like

    <my-login name="login"></my-logout>
    <my-logout name="login" class="iron-selected" selected></my-logout>
    

    We can define an observer in that in the <my-logout> element that checks for changes

    static get properties() {
      return {
        // Other properties
        selected: {
          type: Boolean,
          value: false,
          observer: '_selectedChanged',
        },
      };
    }
    
    _selectedChanged(selected) {
      if (selected) {
        this._performLogout();
      }
    }
    

    The if statement is so that we only fire the logic when we are displayed, not when we leave. One advantage of this is that we don't care if the element has already been attached to the DOM or not. When <iron-selector>/<iron-pages> selects the <my-logout> the first time, the attribute is set, the element attaches, the observer fires, the observer sees that selected is now true (as opposed to the defined false) and runs the logic.