I created a custom web component button, that has a href
attribute. If one clicks on the button, I use Javascript to navigate to its own href
attribute.
Unfortunately, after migrating a large code base of anchor tags that were styled to look like buttons, I have realized that my custom web component has many disadvantages:
Items 2-3 could be fixed in Javascript, but item 1 is not trivial.
I did some research about extending built-in elements using the is
keyword, but that just took me down a rabbit hole of bug reports of Safari refusing to implement the web component spec for extending HTML elements.
Is there any way to add an anchor tag's behaviour to a custom web component, like via a mixin?
Just use @ungap/custom-elements polyfill and you're good to go with Safari as well as even Internet Explorers, both with customized built-in elements, and autonomous custom elements.
Then an implementation could look like this:
class CustomAnchor extends HTMLAnchorElement {
constructor() {
super();
this.addEventListener('click', this.click.bind(this));
}
click(event) {
if (event.getModifierState('Control') || event.getModifierState('Meta')) return; // allow control-click or cmd-click (mac) to work as usual
event?.preventDefault();
console.log(this.href);
// do whatever you like here
}
}
customElements.define('custom-anchor', CustomAnchor, { extends: 'a' });
<script src="//unpkg.com/@ungap/custom-elements"></script>
<a is="custom-anchor" href="https://google.com">Custom google anchor</a>
The downside of extending built-ins is that there is no shadowDOM that allows style encapsulation. If you need that, instead go with an autonomous custom element and use a native anchor tag (or even your extended built-in HTMLAnchorElement) internally:
const styles = `
a { color: red; }
a:hover { background-color: yellow; }
`;
class CustomAnchor extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = styles;
this.anchor = document.createElement('a');
this.shadowRoot.append(style, this.anchor);
}
static get observedAttributes() { return ['link', 'text']; }
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal === newVal) return; // nothing changed
switch (attr) {
case 'link':
if (newVal) this.anchor.href = newVal;
else this.anchor.removeAttribute('href');
break;
case 'text':
this.anchor.textContent = newVal ?? '';
break;
default:
}
}
}
customElements.define('custom-anchor', CustomAnchor);
<script src="//unpkg.com/@ungap/custom-elements"></script>
<custom-anchor link="https://google.com" text="Google Search"></custom-anchor>
If you prefer (e.g. for screenreaders and SEO-reasons) to have the link text as the element's content, throw in a default <slot>
:
const styles = `
a { color: red; }
a:hover { background-color: yellow; }
`;
class CustomAnchor extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const style = document.createElement('style');
style.textContent = styles;
this.anchor = document.createElement('a');
const slot = document.createElement('slot');
this.anchor.appendChild(slot);
this.shadowRoot.append(style, this.anchor);
}
static get observedAttributes() { return ['link']; }
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal === newVal) return; // nothing changed
switch (attr) {
case 'link':
if (newVal) this.anchor.href = newVal;
else this.anchor.removeAttribute('href');
break;
default:
}
}
}
customElements.define('custom-anchor', CustomAnchor);
<script src="//unpkg.com/@ungap/custom-elements"></script>
<custom-anchor link="https://google.com">Google Search</custom-anchor>