Re: [WICG/webcomponents] [element-internals] How to get internals in base class and subclass, without leaking it to public (Issue #962)

As I'm using this in the real world, I find that element "internals" are just too difficult to keep internal. Especially when trying to use with (imo idiomatic) JavaScript patterns like class-factory mixins. So I simply settle with public properties.

Example:

```js
function WithShadow(Base) {
 return class WithShadow extends Base {
  shadowMode = "open"

  constructor(...args) {
   super(...args)

   this.internals ??= this.attachInternals()

   if (!this.internals.shadowRoot) this.attachShadow({mode: this.shadowMode})
  }
 }
}
```

Will someone decide to mess with `element.internals`? Who knows. If they break something, I just won't care.

This is still pretty bad, because it means this mixin _**is compatible only with other mixins or classes that use `this.internals`**_ as a convention. Any classes that store internals on some other property will cause a runtime error.

A `protected` feature in JavaScript is nowhere near being in the picture, and may never be at this pace, so that's not something we can simply tell people to wait for. And even if it existed, the same issue exists: not all classes will be standardized to use the same property, so the runtime error can still happen when using the same pattern with a protected field.

# A better solution?

I'm starting to think that a standardized callback for internals would be a better way to expose internals and keep them encapsulated, as described to some extent in

- https://github.com/WICG/webcomponents/issues/758

It works great because then there's a standard named member that all custom element classes can rely on with which they can receive internals without leaking to public.

With a callback, there is no conflict of property naming, each class can use a `#private` field without issue, like so:

```js
class CoolElement extends HTMLElement {
  #internals
  
  internalsCallback(internals) {
    this.#internals = internals
    this.#doSomethingWithInternals()
  }
  
  #doSomethingWithInternals() { console.log(this.#internals) }
}

class CoolElementWithMoreFeatures extends HTMLElement {
  #stuff
  
  internalsCallback(internals) {
    super.internalsCallback(internals)

    this.#stuff = internals
    this.#doSomethingWithStuff()
  }
  
  #doSomethingWithStuff() { console.log(this.#stuff) }
}

class CoolElementWithEvenMoreFeatures extends HTMLElement {
  #things
  
  internalsCallback(internals) {
    super.internalsCallback(internals)

    this.#things = internals
    this.#doSomethingWithThings()
  }
  
  #doSomethingWithThings() { console.log(this.#things) }
}

const el = new CoolElementWithEvenMoreFeatures()

el.#internals // error
el.#stuff // error
el.#things // error

// either this is allowed:
el.internalsCallback(new MyMockInternals())

// or maybe the browser enforces a special invariant that the method cannot be called after construction:
el.internalsCallback(something) // runtime error
```

How can that invariant idea be enforced? It would be easy to enforce with `document.createElement()` because it can easily run logic after construction to delete the `internalsCallback` method. But with usage of `new`, the engine would need a special non-standard way to run logic after construction to ensure that the method is deleted after `new MyElement` is finished, or some other special non-standard JS behavior (not sure if that's possible across browsers).

When decorators reach stage 4 (looks like they have a very good chance of doing so now, but browser implementations are going really slow in this area) then another idea for enforcing the invariant is to require usage of a decorator, because decorators can always add logic that runs _**after the user's contructor**_.

Example:

```js
@customElements.withInternals
class MyEl extends HTMLElement {
  internalsCallback(internals) { ... }
}
```

where `customElements.withInternals` is a decorator provided natively by the browser's `customElements` API, and where `internalsCallback` will be called during construction only if the decorator is used, and where the decorator has the final say in finishing the construction so it can delete `internalsCallback` (and if for some reason it cannot be deleted, throws an error).

With `new.target`, the decorator can ensure that in a hiearchy of such decorated classes, only the outermost class performs the finalization:

```js
@customElements.withInternals
class MyEl extends HTMLElement {
  internalsCallback(internals) { ... }
}

@customElements.withInternals
class BetterEl extends HTMLElement {
  internalsCallback(internals) { ... }
}

new BetterEl() // internalsCallback is deleted only after the `BetterEl` constructor, not after `MyEl`.
```

Could such an idea be viable?

Another idea is maybe there's a way to do it with a decorator that injects internals directly to a method:

```js
class MyEl extends HTMLElement {
  @customElements.withInternals
  myArbitrarilyNamedInternalsHandler(internals) { ... }
}
```


-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/962#issuecomment-1813310788
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/962/1813310788@github.com>

Received on Wednesday, 15 November 2023 21:47:31 UTC