If everything in JavaScript es from the prototype, I would imagine there is some way to create web ponents without using class. I never found any article about it, I don't know if it's because it's a bad practice or because it's not possible.
If everything in JavaScript es from the prototype, I would imagine there is some way to create web ponents without using class. I never found any article about it, I don't know if it's because it's a bad practice or because it's not possible.
Share Improve this question edited Jul 17, 2022 at 16:33 Boruto asked Jul 17, 2022 at 16:29 BorutoBoruto 871 silver badge5 bronze badges 17- 1 To create a web ponent, you usually use class, but I want to know how it can be created using just functions – Boruto Commented Jul 17, 2022 at 16:36
- 1 look on the right side, no class used, that's what your be dealing with.. no reason not to not use a class – Lawrence Cherone Commented Jul 17, 2022 at 16:56
- 2 ... this is a mon phenomena in javascript development. Most javascript programmers (and I'm deliberately not including Typescript programmers) are much more fortable with functional style but the people who actually develop javascript implement javascript in C/C++. Therefore, every time they try to improve the language they create features from the point of view of an OO programmer - which generally tend to not match the way most javascript programmers think... – slebetman Commented Jul 17, 2022 at 18:20
- 1 ... However, there are excellent frameworks out there written from the point of view of javascript programmers. Check out vue.js and svelte for examples of really good ponent systems that work the way javascript programmers think: using simple functions instead of doing stuff the OO-way. You may even want to look at React.js and it's functional ponent API. – slebetman Commented Jul 17, 2022 at 18:22
- 2 @slebetman ... The usage of classes is not a criteria which defines an OO language. The usage of functions, especially when mentioning contexts like react/vue/svelte does not make one use a "… functional style …", but a programming style based on functions. Functional starts with the usage of higher order functions. Why am'I nitpicking? Because precise wording matters, especially when it es to ments like above, where the argumentation is close to "we/ours versus theirs". And whatever your preferred JS programming style is, it most probably fulfills more criteria of OOP than of FP. – Peter Seliger Commented Jan 29, 2024 at 20:52
2 Answers
Reset to default 11Yes, it can be done without using the class
syntax. The trick is to use Reflect.construct
to substitute the call to super
. Here's a small example snippet, containing a factory (createCustomElementFactory
) to create the 'class' you need for registering a custom element using customElements.define
.
To play with a (way more evolved) module for this, see/fork this Stackblitz project.
Worked out the idea into a web ponent factory on Github.
const myCustomElementCreator = createCustomElementFactory();
const connectedCallback = function() {
const isCustom = this.getAttribute(`is`);
if (!isCustom) {
this.style.display = `block`;
this.style.padding = `2px 5px`;
this.style.border = `1px solid #999`;
}
this.insertAdjacentHTML(`afterbegin`, `<div>I am <i><${
this.tagName.toLowerCase()}${
isCustom ? ` is="${isCustom}"` : ``}></i></div>`);
console.log(`We just connected the ${
isCustom ? `custom` : `autonomous`} element '${
this.tagName.toLowerCase()}${isCustom ? `[is="${isCustom}"]` : ``}'`);
};
const myBrandNewElemClass = myCustomElementCreator({ connectedCallback } );
const myBrandNewParagraphClass = myCustomElementCreator({
connectedCallback,
forElem: HTMLParagraphElement });
// register 2 elements
customElements.define( "my-brand-new-element", myBrandNewElemClass );
customElements.define(
"my-brand-new-paragraph",
myBrandNewParagraphClass,
{ extends: `p` } );
document.body.insertAdjacentHTML(
`beforeend`,
`<my-brand-new-element>Hello world!</my-brand-new-element>`
);
document.body.insertAdjacentHTML(
`beforeend`,
`<p is="my-brand-new-paragraph">Hello paragraph!</p>`
);
function createCustomElementFactory() {
const paramsPlaceholder = ({
get all() {
return {
connectedCallback,
disconnectedCallback,
adoptedCallback,
attributeChangedCallback,
observedAttributes,
forElem } = {}
}
});
function CustomElementConstructorFactory(params = paramsPlaceholder.all) {
const elemProto = params.forElem?.prototype instanceof HTMLElement
? params.forElem : HTMLElement;
function CustomElementConstructor() {
if (elemProto !== HTMLElement) {
self = Reflect.construct( params.forElem, [], CustomElementConstructor );
return self;
}
return Reflect.construct( HTMLElement, [], CustomElementConstructor );
}
CustomElementConstructor.prototype = elemProto.prototype;
CustomElementConstructor.observedAttributes = params.observedAttributes;
Object.entries( params ).forEach( ([name, cb]) => {
if (cb && cb instanceof Function) {
CustomElementConstructor.prototype[name] = cb; }
} );
return CustomElementConstructor;
}
return (params = paramsPlaceholder.all) => CustomElementConstructorFactory(params);
}
Based on @Koolinc answer, I was able to extend this to make it work with Dart:
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart';
class WebComponent<T extends HTMLElement> {
late T element;
String get extendsType => 'HTMLElement';
void connectedCallback() {}
void disconnectedCallback() {}
void adoptedCallback() {}
void attributeChangedCallback(
String name,
String? oldValue,
String? newValue,
) {}
Iterable<String> get observedAttributes => [];
R getRoot<R extends JSObject>() {
final hasShadow = element.shadowRoot != null;
return (hasShadow ? element.shadowRoot! : element) as R;
}
bool get isCustom {
return element.getAttribute('is') != null;
}
static void define(
String tag,
WebComponent Function() create, {
String? extendsTag,
}) {
final obj = _factory(create);
if (extendsTag != null) {
window.customElements.define(
tag,
obj,
ElementDefinitionOptions(extends_: extendsTag),
);
} else {
window.customElements.define(tag, obj);
}
}
}
@JS('Reflect.construct')
external JSAny _reflectConstruct(
JSObject target,
JSAny args,
JSFunction constructor,
);
final _instances = <HTMLElement, WebComponent>{};
JSFunction _factory(WebComponent Function() create) {
final base = create();
final elemProto = globalContext[base.extendsType] as JSObject;
late JSAny obj;
JSAny constructor() {
final args = <String>[].jsify()!;
final self = _reflectConstruct(elemProto, args, obj as JSFunction);
final el = self as HTMLElement;
_instances.putIfAbsent(el, () => create()..element = el);
return self;
}
obj = constructor.toJS;
obj = obj as JSObject;
final observedAttributes = base.observedAttributes;
obj['prototype'] = elemProto['prototype'];
obj['observedAttributes'] = observedAttributes.toList().jsify()!;
final prototype = obj['prototype'] as JSObject;
prototype['connectedCallback'] = (HTMLElement instance) {
_instances[instance]?.connectedCallback();
}.toJSCaptureThis;
prototype['disconnectedCallback'] = (HTMLElement instance) {
_instances[instance]?.disconnectedCallback();
_instances.remove(instance);
}.toJSCaptureThis;
prototype['adoptedCallback'] = (HTMLElement instance) {
_instances[instance]?.adoptedCallback();
}.toJSCaptureThis;
prototype['attributeChangedCallback'] = (
HTMLElement instance,
String name,
String? oldName,
String? newName,
) {
_instances[instance]?.attributeChangedCallback(name, oldName, newName);
}.toJSCaptureThis;
return obj as JSFunction;
}