Webcomponents

Can I connect components via custom code?

There might be cases where you would like to make a component react to changes in another component, but TEI Publisher does not provide the necessary connection by default. Components in Publisher mainly communicate via signals sent into channels. You can thus always connect two components via custom code by listening to events sent by a first component and - when received - trigger another component.

Let’s assume you would like certain parts of your document to be collapsed initially. You are thus outputting a pb-collapse. However, instead of preloading the collapsed content, it should be loaded only when the user actually wants to look at it and thus expands the pb-collapse. We’re thus using a pb-view (or pb-load) for the content:

<pb-collapse emit="meta">
    <div slot="collapse-trigger">About this text</div>
    <pb-view slot="collapse-content" on-update src="intro" subscribe="meta"></pb-view>
</pb-collapse>

The pb-view will load content from a different document (defined by a pb-document with id intro). The @on-update property instructs the view to not load content immediately, but only when it receives a pb-update event.

We thus need to wire the pb-collapse with the pb-view by sending a pb-update event to the view whenever the collapse is expanded. pb-collapse will emit a pb-collapse-open event in this case. We can thus connect the pb-collapse-open signal with pb-update by inserting a few lines of custom code into the HTML template we use to display the text:

<script>
    window.addEventListener('load', () => {
        document.addEventListener('pb-collapse-open', (ev) => {
            if (ev.detail && ev.detail.key && ev.detail.key === 'meta') {
                document.dispatchEvent(new CustomEvent('pb-update', {
                    detail: {
                        key: 'meta'
                    }
                }));
            }
        });
    });
</script>

Important here is the key property in the event details: within TEI Publisher, it always indicates the channel for which an event should apply. It corresponds to the channels we defined with @subscribe and @emit on the HTML elements above. If there’s no key, it means that the event is targetted at the global default channel. For simple cases, emitting events to the default channel might be enough, but if your HTML gets more complex, you want to be sure that this particular pb-collapse is sending its events to the correct pb-view - and not all of them.

Using utility methods

Beginning with version 1.14.1 of pb-components (check your version), there’s a utility class which can be used to simplify the code above:

<script>
window.addEventListener('load', () =>
    pbEvents.subscribe('pb-collapse-open', 'meta', () => pbEvents.emit('pb-update', 'meta'), true)
);
</script>

The arguments to subscribe are:

  1. the name of the event: you can find the different events emitted by each component in the component docs
  2. the channel(s) to subscribe to. Can also be null for the default channel. To subscribe to multiple channels at once, pass in an array.
  3. a callback function to be called when the event is triggered. This will receive the event as parameter.
  4. should the event handler fire only once?

By passing ‘true’ for the fourth argument, we prevent the pb-update event being resent whenever the user expands the collapse again. Loading the content of the collapse once should be enough.

Alternatively you can use the subscribeOnce function if you would like the event handler to only execute once. This function returns a Promise, which resolves when the event is received:

<script>
window.addEventListener('load', () =>
    pbEvents.subscribeOnce('pb-collapse-open', 'meta')
        .then(() => pbEvents.emit('pb-update', 'meta'))
);
</script>

The first two arguments for emit are the same as for subscribe. The third argument can be used to pass data to the event. The event handler can access this information in the ev.detail property as in the following snippet:

<script>
pbEvents.subscribe('pb-update', ['transcription', 'translation'], (ev) => {
    console.log(ev.detail); // should include foo: 'baz'
}, true);
pbEvents.emit('pb-update', 'translation', {
    foo: 'baz'
});
</script>

Demo

You can find a demo of the code covered above in the pb-components documentation.

Accessing the Content of a pb-view

Quite often you may want to inspect the HTML rendition of your TEI as displayed in a pb-view, e.g. to add event listeners, extract images etc. Unfortunately, pb-view renders the content into the so-called shadow DOM, i.e. a private section which is not visible to the world outside the pb-view. The advantage of this approach is that anything inside the shadow will not interfere with the rest of the page, allowing us to display completely heterogenous documents on the same page. The disadvantage is that code outside the pb-view can not “look into” the component.

Fortunately it is possible though to intercept the events pb-view emits. Of particularily interest is the pb-update event: this event is emitted by pb-view whenever it has finished loading new content, and the event details include a reference to the actual content rendered by the component. You can access it as an HTML element in ev.detail.root.

For example, let’s assume that our ODD transformation outputs an HTML element with class credits. We don’t want to show this as part of the rendered text though, but instead display it prominently on the top of the page. This can be done as follows:

pbEvents.subscribe("pb-update", "transcription", (ev) => {
    const credits = ev.detail.root.querySelector(".credits");
    const creditsTarget = document.getElementById("credits");
    if (credits && creditsTarget) {
      creditsTarget.innerHTML = credits.innerHTML;
    }
    credits.style.display = 'none';
  });

The code first tries to find an element with CSS class .credits. If it exists, it’s content is copied into another element with id credits contained somewhere else on the page. The original element is then hidden.