konsoletyper / teavm

Compiles Java bytecode to JavaScript, WebAssembly and C
https://teavm.org
Apache License 2.0
2.55k stars 260 forks source link

Support for javascript classes inside @JSBody tags #909

Open shannah opened 2 months ago

shannah commented 2 months ago

The following example prints warning messages to the console on compile, but the app works. I think it is related to an attempt to parse the Javascript class.

package ca.weblite.teavm.webcomponent;

import ca.weblite.teavm.delegate.WebComponentDelegate;
import ca.weblite.teavm.factory.PojoFactory;
import ca.weblite.teavm.factory.WebComponentFactory;
import ca.weblite.teavm.factory.WebComponentDelegateFactory;
import ca.weblite.teavm.jso.HTMLElement;
import org.teavm.jso.JSBody;

import java.util.HashMap;
import java.util.Map;

public class WebComponentRegistry {
    private final Map<String, WebComponentFactory> components = new HashMap<>();

    private PojoFactory pojoFactory;

    private static WebComponentRegistry instance;

    public static WebComponentRegistry getInstance() {
        if (instance == null) {
            instance = new WebComponentRegistry();
        }
        return instance;
    }

    public void setPojoFactory(PojoFactory pojoFactory) {
        this.pojoFactory = pojoFactory;
    }

    public void register(String name, WebComponentFactory factory) {
        components.put(name, factory);
        registerNative(name, new WebComponentDelegateFactory() {
            @Override
            public WebComponentDelegate create(String elementType) {
                WebComponent component = factory.create();
                return new WebComponentDelegate() {
                    @Override
                    public void constructorCallback(HTMLElement self) {
                        component.constructorCallback(self);
                    }

                    @Override
                    public void connectedCallback(HTMLElement self) {
                        component.connectedCallback(self);
                    }

                    @Override
                    public void disconnectedCallback(HTMLElement self) {
                        component.disconnectedCallback(self);
                    }

                    @Override
                    public void adoptedCallback(HTMLElement self) {
                        component.adoptedCallback(self);
                    }

                    @Override
                    public void attributeChangedCallback(HTMLElement self, String name, String oldValue, String newValue) {
                        component.attributeChangedCallback(self, name, oldValue, newValue);
                    }
                };
            }
        }, factory.create().getObservedProperties());
    }

    public void register(String name, Class<? extends WebComponent> componentClass) {
        register(name, () -> {
            try {
                if (pojoFactory != null) {
                    return pojoFactory.create(componentClass);
                }
                return componentClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
    }

    public WebComponent create(String name) {
        return components.get(name).create();
    }

    @JSBody(params = {"elementName", "factory", "observedAttributes"}, script = """
            class MyCustomElement extends HTMLElement {
                #delegate;
                static get observedAttributes() {
                    return observedAttributes;
                }
                constructor() {
                    super();
                    this.#delegate = factory.create(elementName);
                    this.#delegate.constructorCallback(this);
                }

                connectedCallback() {
                    this.#delegate.connectedCallback(this);
                }

                disconnectedCallback() {
                    this.#delegate.disconnectedCallback(this);
                }

                adoptedCallback() {
                    this.#delegate.adoptedCallback(this);
                }

                attributeChangedCallback(name, oldValue, newValue) {
                    this.#delegate.attributeChangedCallback(this, name, oldValue, newValue);
                }

            }
            customElements.define(elementName, MyCustomElement);

            """)
    private native static void registerNative(
            String elementName,
            WebComponentDelegateFactory factory,
            String[] observedAttributes
    );
}

The warnings I receive are:

WARNING: Error in @JSBody script line 0, char 33: missing ; before statement
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 0, char 41: missing ; before statement
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 0, char 53: missing ; before statement
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 0, char 54: missing ; before statement
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 1, char 5: illegal character: #
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 1, char 5: missing } in compound statement
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
WARNING: Error in @JSBody script line 1, char 5: missing } after function body
    at ca.weblite.teavm.webcomponent.WebComponentRegistry.registerNative
konsoletyper commented 2 months ago

This is because TeaVM uses Rhino to parse JS. Rhino does not fully conform ES2015, not even close. Writing own JS parser is a lot of work, patching Rhino is impossible due to licensing issues. Anyway, for me it seems strange to have large pieces of code inside @JSBody. Would not just subclassing HTMLElement in Java side enough? AFAIR, there are only few issues preventing that: properly define class's prototype via Reflection.construct and ability to take class as a value (or rather passing Class to native JS methods). What do you think?

shannah commented 2 months ago

Would not just subclassing HTMLElement in Java side enough?

I'm creating custom elements by following the structure described in this MDN doc.

It is actually working really nicely. Just these warning are unsettling, so I thought I'd post them.

I first attempted to translate these structure into the old function/prototype style classes, but wasn't able to get it to work.

What kinds of things are likely to break when warnings like this occur?