Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constructor arguments in custom elements #605

Closed
olanod opened this issue Nov 10, 2016 · 12 comments
Closed

Constructor arguments in custom elements #605

olanod opened this issue Nov 10, 2016 · 12 comments

Comments

@olanod
Copy link

olanod commented Nov 10, 2016

Dependency injection is a very well established pattern that greatly improves test-ability of a piece of software. Is common for components to use external code in the form of a dependency, like an API client for example which developers want to mock during tests. In a world of modules we shouldn't rely on global variables.
This dependencies could be retrieved by the browser from a user defined function that returns an array(or any iterable?) of objects or primitives that are passed as constructor arguments when the custom element is instantiated.

class MyComponent extends HTMLElement {
    constructor(dep1, dep2) {
        super()
        this.foo = dep1
        this.bar = dep2
    }
}

customElements.define('my-component', MyComponent, { dependencies: () => [new Foo, new Bar] })

An alternative could be a function that allows users to instantiate elements manually.

customElements.define('my-component', MyComponent, { construct: MyComponent => new MyComponent(foo, bar) }) 
@annevk
Copy link
Collaborator

annevk commented Nov 10, 2016

I think you'll have to elaborate on the processing model.

@treshugart
Copy link

treshugart commented Nov 10, 2016

How does this work with the document.createElement() method or the declarative (HTML) API? Those API points implicitly suggest that there's no way to pass constructor arguments. I see there's a dependencies option, but I'm not convinced that's much better than newing them in the constructor since many people seem to be pre-defining the element which precludes the consumer from specifying.

@domenic
Copy link
Collaborator

domenic commented Nov 10, 2016

If you just want to provide default constructor arguments, I'd suggest you do that when defining the constructor. You could even use expressions to look them up in a DI container, e.g. (dep1 = diContainer.get('dep1', this), dep2 = diContainer.get('dep2', this)).

@rniwa
Copy link
Collaborator

rniwa commented Nov 10, 2016

Yeah, I don't think we want to bake a particular way of doing dependency injection into the platform.

@olanod
Copy link
Author

olanod commented Nov 11, 2016

@treshugart Yeah I know, it's a well know fact that document.createElement() or the declarative API need a class with 0 arguments constructor and call super always because, how do you pass arguments to your constructor form the HTML? What I propose is a way to tell the browser that my custom element class has a constructor that needs some arguments and that it can get those arguments by calling the function that the user provided, then the browser will use that to correctly create the instance of the class... So yes it would be some standard way of doing DI now that you guys mention it, but still is it that bad or crazy?
A patched document.createElement could be doing something like this:

const originalCreateElement = HTMLDocument.prototype.createElement
HTMLDocument.prototype.createElement = ( elementName, options ) => {
    const constructor = customElements.get( elementName )
    if ( constructor ) {
        // get arguments somewhere with a public API with a creative name
        let args = customElements.getRegisteredArgumentsForConstructor( constructor )
        // or from a well known symbol in the constructor? 
        // let args = constructor[elementDependenciesSymbol]
        return new constructor(...args)
    else {
         return originalCreateElement.call(this, elementName, options)
    }
}

@rniwa
Copy link
Collaborator

rniwa commented Nov 11, 2016

We certainly don't want to use the second argument of createElement to do this because it's already used for customized builtin in the current spec, people have plans to use it for defining attributes, children, etc...

On the other hand, it might be that we can just expose that second argument in the constructor. That'd let authors do whatever they want to do besides dependency injection like configuring elements for one way or another.

One thing to keep in mind is that there's no equivalent mechanism to do this in HTML because we only have elements and attributes in HTML, and attributes are added after the element is constructed.

@WebReflection
Copy link

WebReflection commented Nov 11, 2016

@rniwa current createElement accepts as second argument an object in order to be indeed extended as the Web needs through any sort of property. document.createElement('custom-element', {arguments:[1,2,3]}) would be as natural implementation as it reads, IMO.

@rniwa
Copy link
Collaborator

rniwa commented Nov 11, 2016

That's fine. There's no harm in the constructor to see that object in the argument.

@wessberg
Copy link

I too really want to see this through. As others have said, the ElementCreationOptions dictionary already exists and may be extended as the requirements change.

In regards to DI-patterns, following @domenic's suggestion of providing default constructor arguments directly from the element constructor means that the Custom Element classes must depend on the service container, which, besides being a bit verbose and slightly annoying in practice, also may lead to circular dependencies (depending on the implementation) if the Custom Element itself is also injected as a service.

@ibhi
Copy link

ibhi commented Jun 8, 2017

I found a kinda dirty workaround of wrapping the custom elements class in a constructor function(I would like to call it a component factory) where I can do DI(Here I have used Angular DI) and capturing the constructor function arguments in closure and passing them to the inner class(custom elements class)

Here is a simple sudo code of what I mean

// I do DI of the dependencies here
function MyComponent(http, bar) {
    return class extends HTMLElement {
        constructor() {
            super()
            this.http = http
            this.bar = bar
    }
   }
}
// We can use typescript decorators as well
MyComponent.parameters = [[new Inject(Http)], [new Inject(Bar)]];
const injector = ReflectiveInjector.resolveAndCreate([
     Http,
     Bar,
     {
           provide: MyComponent,
           useFactory: (http, bar) => new MyComponent(http, bar),
           deps: [Http, Bar]
     }
]);

customElements.define('my-component', injector.get(MyComponent));

For a working version of the above code, refer https://github.com/ibhi/webcomponent-with-di/blob/master/src/users.component.js

@annevk annevk added the v2 label Sep 4, 2017
@domenic
Copy link
Collaborator

domenic commented Oct 13, 2017

Let's close this. There are plentiful workarounds, and upon re-reading, the proposal seems to be "provide the UA with a no-argument function it can call to create elements even if the constructor takes mandatory arguments". However, I think it's much simpler and fits better with the platform to flip that around: make the constructor the no-argument function the platform calls, and if you want to have a mandatory-arguments factory, you can e.g. use static methods on your custom element class.

@domenic domenic closed this as completed Oct 13, 2017
@denis-migdal
Copy link

denis-migdal commented Oct 7, 2023

As it wasn't said here :

  • If you want to have constructor arguments, they all need to have a default value, and instead of using createElement() you can :
let elem = new (customElements.get('my-custom') )("foo", "faa", "fuu")
  • If your element is in HTML, just use the html attribute in the connectedCallback() to initialize your element.

  • If you want to bind arguments to a tag name :

class MyCustom extends HTMLElement {
     constructor(foo = null) {   }
}

function bindCstr(custom, ...args) {
             class X extends custom {
                constructor() {
                    super(...args)
                }
             }

             return X;
       }

customElements.define(name, bindCstr(MyCustom, 'foo') );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants