Motivation
Private members / methods are a standard part of JavaScript when working with classes and so should be supported when referenced via a render template, e.g.
export default class QuickLinks extends HTMLElement {
connectedCallback() {
// ...
this.#title = 'My Dropdown';
}
#selectOption(event: Event) {
const selectedLink = this.links.find(
(link) => link.route === (event.target as HTMLSelectElement).value,
);
window.location.href = selectedLink.route;
}
render() {
return (
<div>
<h1>{this.#title}</h1>
<select name="quick-links-dropdown" onchange={this.#selectOption}>
{/* ... *}
</select>
</div>
);
}
}
customElements.define("x-quick-links", QuickLinks);
Technical Design
The main issue with this inside templates, is that they'll bind to current element, so the <p> or <select> tag, NOT the custom element host, where title and selectOption are actually bound to. Now this isn't an issue for public members / methods, because WCC's JSX compiler will support binding this references used in templates to the hosting custom element.
For example, this
<button onclick={this.increment}>Increment (+)</button>
Would get turned into
<button onclick="this.getRootNode().host.increment(event)">Increment (+)</button>
BUT, this.getRootNode().host is only a reference to the DOM element, not the underlying class. Such that if we try and use a private method
<select name="quick-links-dropdown" onchange={this.#selectOption}>
{/* ... */}
</select>
We will get an error about access, which makes sense
The same is true for private members, for example this
<p>Name: {this.#name}</p>
Will yield an error from the compiler
TypeError [Error]: Cannot read properties of undefined (reading 'name')
Additional Context
So it seems in general we'll need to rethink our binding approach if we want to support private methods / members, and figure out a way to really bind this to the custom element instance instead of just the associated DOM node.
Events are maybe easier, since in theory we could just convert any event handlers to addEventListeners
<select name="quick-links-dropdown" onchange={this.#selectOption}>
{/* ... */}
</select>
connectedCallback() {
const select = this.shadowRoot.querySelector('select');
select.addEventListener('change', this.#selectOption);
}
Would just a little bit of extra work / refactoring, but should be doable.
Also saw an option using Symbols
// Define a unique key hidden inside your component module scope
const privateClickKey = Symbol('privateClick');
class SymbolInlineElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Bind the symbol method to lock down "this"
this[privateClickKey] = (event) => {
console.log('Handled securely via symbol token. Target:', event.target);
console.log('Class context:', this.tagName);
};
}
connectedCallback() {
// Expose the key safely to the inner layout string via a reference
this.shadowRoot.innerHTML = `
<button onclick="this.getRootNode().host[Symbol.for('privateClickKey')]?.(event)">
Click Me
</button>
`;
// Map a global or instance token lookup if required
this[Symbol.for('privateClickKey')] = this[privateClickKey];
}
}
customElements.define('symbol-inline-element', SymbolInlineElement);
Another option using custom elements registry
class MyParentComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<div>
<!-- Deeply nested native element -->
<button id="myBtn">Click Me</button>
</div>
);
}
connectedCallback() {
const btn = this.shadowRoot.querySelector('#myBtn');
btn.addEventListener('click', function() {
// 1. Get the ShadowRoot's host element (<my-parent-component>)
const hostElement = this.getRootNode().host;
// 2. Direct approach: Read the constructor property directly from the instance
const componentClassDirect = hostElement.constructor;
console.log('Class reference via constructor:', componentClassDirect);
// 3. Alternative registry approach: Lookup via tag name
const tagName = hostElement.tagName.toLowerCase();
const componentClassRegistry = customElements.get(tagName);
console.log('Class reference via registry:', componentClassRegistry);
});
}
}
customElements.define('my-parent-component', MyParentComponent);
Motivation
Private members / methods are a standard part of JavaScript when working with classes and so should be supported when referenced via a render template, e.g.
Technical Design
The main issue with
thisinside templates, is that they'll bind to current element, so the<p>or<select>tag, NOT the custom element host, wheretitleandselectOptionare actually bound to. Now this isn't an issue for public members / methods, because WCC's JSX compiler will support bindingthisreferences used in templates to the hosting custom element.For example, this
Would get turned into
BUT,
this.getRootNode().hostis only a reference to the DOM element, not the underlying class. Such that if we try and use a private methodWe will get an error about access, which makes sense
The same is true for private members, for example this
Will yield an error from the compiler
Additional Context
So it seems in general we'll need to rethink our binding approach if we want to support private methods / members, and figure out a way to really bind
thisto the custom element instance instead of just the associated DOM node.Events are maybe easier, since in theory we could just convert any event handlers to
addEventListenersWould just a little bit of extra work / refactoring, but should be doable.
Also saw an option using Symbols
Another option using custom elements registry