A good way to get a feel for a web application is to first figure out the component hierarchy. To aid in understanding the component structure we will create a simple bookmarklet that highlights custom components.

Understanding an App is easier once you have a mental map how pages are structured. Because it shows how the (hopefully) flat component structure and actual components work together. We'll look how we can identify app components structure by adding some custom styling to all none standard HTML tags using a bookmarklet.

The end result is a bookmarklet that shows all components on the current page:

Angular component demo interaction

Try it out yourself or read on to see how it works.

Highlight app component HTML tags

The goal is to quickly see how a web app is composed by showing all web components.

<body>
  <my-app>
    <div>
      <my-list>
        <my-item></my-item>
        <my-item></my-item>
      </my-list>
    </div>
  </my-app>
</body>

Say you have the basic application structure from above. We will look how we can determine and style the my-app, my-list and my-item components so you can easily identify them as a developer.

In order to use the technique as widely as possible we will look at a library agnostic solution. That means we can't hook into the your view library of choice to determine all components. We are stuck with what we can gather from the DOM. Luckily we can do this because custom components should not overlap with any standard HTML tags. So we can just select all none standard tags.

This approach means that:

  • Only components that have custom HTML tags in the final page are shown, so Angular components are selected, React components are not.
  • We have an admittedly hacky approach to do this, but hey, as long as it works, right :).

Selecting components

The first step in showing new HTML tags is finding them on the current page. Luckily these custom elements have one very clear property, they are not a standard HTML tag.

A quick look at MDN yields the following list:

knownTags = ['address', 'article', 'aside', 'footer', 'h1','h2','h3','h4','h5','h6', 'header', 'hgroup', 'nav', 'section', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li', 'main', 'ol', 'p', 'pre', 'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr','caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'button', 'datalist', 'fieldset', 'form', 'input', 'label', 'legend', 'meter', 'optgroup', 'option', 'output', 'progress', 'select', 'textarea' ]

We want to select all elements that do not have these tags. For that we can use the CSS not selector like:

:not( address ):not( article ) ...

We create the selector and find nodes using:

notCss = knownTags.map(tag => `:not( ${ tag } )`).join('')
nodes = document.querySelectorAll(`body ${ notCss }`)

The actual full CSS selector is very long:

:not( address ):not( article ):not( aside ):not( footer ):not( h1 ):not( h2 ):not( h3 ):not( h4 ):not( h5 ):not( h6 ):not( header ):not( hgroup ):not( nav ):not( section ):not( dd ):not( div ):not( dl ):not( dt ):not( figcaption ):not( figure ):not( hr ):not( li ):not( main ):not( ol ):not( p ):not( pre ):not( ul ):not( a ):not( abbr ):not( b ):not( bdi ):not( bdo ):not( br ):not( cite ):not( code ):not( data ):not( dfn ):not( em ):not( i ):not( kbd ):not( mark ):not( q ):not( rp ):not( rt ):not( rtc ):not( ruby ):not( s ):not( samp ):not( small ):not( span ):not( strong ):not( sub ):not( sup ):not( time ):not( u ):not( var ):not( wbr ):not( caption ):not( col ):not( colgroup ):not( table ):not( tbody ):not( td ):not( tfoot ):not( th ):not( thead ):not( tr ):not( button ):not( datalist ):not( fieldset ):not( form ):not( input ):not( label ):not( legend ):not( meter ):not( optgroup ):not( option ):not( output ):not( progress ):not( select ):not( textarea )

Next up is actually highlighting these non standard elements in the DOM.

Highlighting

To highlight the list of DOM nodes we'll append a absolute positioned styled element. Nothing to fancy.

One thing of note is using adding the CSS to the first style sheet we encounter. Of course, we could also inline the CSS, but this just seemed a bit nicer.

The result is as follows:

nodesArray.map( node => {
  const outer = document.createElement('div');
  outer.className='debug-wrapper';
  const inner = document.createElement('div');
  inner.innerHTML = node.nodeName.toLowerCase();
  inner.className='debug-component';

  node.prepend(outer);
  outer.appendChild(inner);
});

const rules = [`
.debug-wrapper {
    position: relative;
}`,`
.debug-component {
    position: absolute;
    right: 0;
    z-index: 777;
    color: lightgray;
    font-size: 8px;
}`,`
.debug-component:hover {
    color: black;
    background-color: lightgray;
    font-size: 12px;
    padding: 0.2em;
}`];

const sheet = window.document.styleSheets[0];
rules.map(rule => sheet.insertRule(rule, sheet.cssRules && sheet.cssRules.length || 0) );

For the complete code see the GitHub project.

Creating a bookmarklet

Now one way to apply this code to the current page is to drop in in the browser console. That however is a bit tedious, so let us look another deployment method: a bookmarklet.

Creating a bookmarklet is surprisingly simple, stupidly simple. You just dump your entire script inside an anchor tag like:

<a href="javascript:<my-custom-code>">My custom code is executed if dragged to bookmarks list and pressed</a>

Highlighting bookmark

Applying the custom tag highlighting code to a bookmarklet is done by creating an anchor tag with the following href

<a href="javascript:&#x27;use strict&#x27;;(function(){var knownTags=[&#x27;address&#x27;,&#x27;article&#x27;,&#x27;aside&#x27;,&#x27;footer&#x27;,&#x27;h1&#x27;,&#x27;h2&#x27;,&#x27;h3&#x27;,&#x27;h4&#x27;,&#x27;h5&#x27;,&#x27;h6&#x27;,&#x27;header&#x27;,&#x27;hgroup&#x27;,&#x27;nav&#x27;,&#x27;section&#x27;,&#x27;dd&#x27;,&#x27;div&#x27;,&#x27;dl&#x27;,&#x27;dt&#x27;,&#x27;figcaption&#x27;,&#x27;figure&#x27;,&#x27;hr&#x27;,&#x27;li&#x27;,&#x27;main&#x27;,&#x27;ol&#x27;,&#x27;p&#x27;,&#x27;pre&#x27;,&#x27;ul&#x27;,&#x27;a&#x27;,&#x27;abbr&#x27;,&#x27;b&#x27;,&#x27;bdi&#x27;,&#x27;bdo&#x27;,&#x27;br&#x27;,&#x27;cite&#x27;,&#x27;code&#x27;,&#x27;data&#x27;,&#x27;dfn&#x27;,&#x27;em&#x27;,&#x27;i&#x27;,&#x27;kbd&#x27;,&#x27;mark&#x27;,&#x27;q&#x27;,&#x27;rp&#x27;,&#x27;rt&#x27;,&#x27;rtc&#x27;,&#x27;ruby&#x27;,&#x27;s&#x27;,&#x27;samp&#x27;,&#x27;small&#x27;,&#x27;span&#x27;,&#x27;strong&#x27;,&#x27;sub&#x27;,&#x27;sup&#x27;,&#x27;time&#x27;,&#x27;u&#x27;,&#x27;var&#x27;,&#x27;wbr&#x27;,&#x27;caption&#x27;,&#x27;col&#x27;,&#x27;colgroup&#x27;,&#x27;table&#x27;,&#x27;tbody&#x27;,&#x27;td&#x27;,&#x27;tfoot&#x27;,&#x27;th&#x27;,&#x27;thead&#x27;,&#x27;tr&#x27;,&#x27;button&#x27;,&#x27;datalist&#x27;,&#x27;fieldset&#x27;,&#x27;form&#x27;,&#x27;input&#x27;,&#x27;label&#x27;,&#x27;legend&#x27;,&#x27;meter&#x27;,&#x27;optgroup&#x27;,&#x27;option&#x27;,&#x27;output&#x27;,&#x27;progress&#x27;,&#x27;select&#x27;,&#x27;textarea&#x27;];var notCss=knownTags.map(function(tag){return&#x27;:not( &#x27;+tag+&#x27; )&#x27;;}).join(&#x27;&#x27;);var nodes=document.querySelectorAll(&#x27;body &#x27;+notCss);var nodesArray=Array.prototype.slice.call(nodes);nodesArray.map(function(node){var outer=document.createElement(&#x27;div&#x27;);outer.className=&#x27;debug-wrapper&#x27;;var inner=document.createElement(&#x27;div&#x27;);inner.innerHTML=node.nodeName.toLowerCase();inner.className=&#x27;debug-component&#x27;;node.prepend(outer);outer.appendChild(inner);});var rules=[&#x27;\n  .debug-wrapper {\n      position: relative;\n  }&#x27;,&#x27;\n  .debug-component {\n      position: absolute;\n      right: 0;\n      z-index: 777;\n      color: lightgray;\n      font-size: 8px;\n  }&#x27;,&#x27;\n  .debug-component:hover {\n      color: black;\n      background-color: lightgray;\n      font-size: 12px;\n      padding: 0.2em;\n  }&#x27;];var sheet=window.document.styleSheets[0];rules.map(function(rule){return sheet.insertRule(rule,sheet.cssRules&amp;&amp;sheet.cssRules.length||0);});})();">Highlight custom components Bookmarklet (Drag and drop this to you bookmarks list)</a>

The resulting link can be added to your bookmarks bar and used to highlight custom components on any page:

Highlight custom components Bookmarklet (Drag and drop this to you bookmarks list)

Using the bookmarklet is remarkably easy, just drag and drop the link above to your browsers bookmarks list.

Next, navigate to a page with custom tags (An old Angular App for example) and press the bookmarklet while you are on the page.

For example try the Angular material site

Custom web components highlighted in Angular heroes example

Sources and further reading