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

Methods on component accessible to JS #837

Open
toddburnside opened this issue Dec 21, 2020 · 8 comments
Open

Methods on component accessible to JS #837

toddburnside opened this issue Dec 21, 2020 · 8 comments
Labels

Comments

@toddburnside
Copy link
Contributor

Hi,

We have created a facade for ag-grid-react using ScalablyTyped. The basic functionality is working fine, but the grid allows for things like custom cell renderers and cell editors to be specified as react components. I am able to pass in components using either a function as described in INTEROP.md, and with component.cmapCtorProps[…](…).toJsComponent.raw.

However, the grid requires some extra “lifecycle” functions to be available on some of the components, such as getValue(): js.Any, which the grid uses to get the new value after editing is completed. I was hoping that if I put the functions in the Backend of the component, they would be visible to the grid. But, no matter what I try, I get this helpful message in the console: “ag-Grid: Framework component is missing the method getValue()".

Is there a way to make extra functions in a component visible to the grid?

A jsx example of what I'm talking about is: https://github.com/ag-grid/ag-grid-react-example/blob/master/src-examples/richGridDeclarativeExample/NameCellEditor.jsx In the jsx, they just define the lifecycle methods right alongside the render method.

Thanks!

@japgolly
Copy link
Owner

Hey! Yes, the way to do that is to create a standard Scala.JS facade that includes getValue() etc, and then call .addFacade when you're building up your component. It's described in the interop page but I see now that there's no example (hey PR welcome btw 😀).

Here's an example in the unit tests that should help:

@toddburnside
Copy link
Contributor Author

Great! Thanks for the quick response. I figured there had to be a way. :) Thanks for the awesome library. Once I get it working in my code, I'll create an example for the docs and submit a PR.

@japgolly
Copy link
Owner

No worries at all.

Once I get it working in my code, I'll create an example for the docs and submit a PR.

Woah thank you very much. I like to throw those statements out there just in case but I never really expect a response. It's very much appreciated so thanks a lot!

@toddburnside
Copy link
Contributor Author

I created PR #838 for the addFacade example.

I see how .addFacade makes it possible to access the facade methods of a JS defined component from Scala, but I can't see how it can be used to make extra methods on a Scala defined component available from JS.

In this case we need to pass a Scala component to a JS component (ag-grid-react in this case), that expects to be able to call certain methods on our component.

We do have components where we have methods defined in the backend of a Scala component that we call from elsewhere, but that requires something like aladinRef.get.flatMapCB(_.backend.gotoRaDec(xxx)), which needs to explicitly reference the backend. Is this extra level of indirection what makes it so that these methods are not visible from JS - at least not where they are expected to be?

@rpiaggio
Copy link
Collaborator

rpiaggio commented Jan 8, 2021

Hi there! I've been trying to solve this issue too.

Just for reference: we need to create a component like this one, which has a getValue method that ag-grid needs to access: https://gist.github.com/maxkoretskyi/fb8c630b86a772622bf8ac55dd623b76 (this is so that we use a custom editor in ag-grid).

Passing component.toJsComponent doesn't work, since that wraps our Scala component in a JS functional component which can't have additional methods.

Since we need to pass a JS class component, I tried passing component.raw which would seem to be what we want, but this crashes at runtime with a TypeError when ag-grid tries to create our component since this is undefined in https://github.com/japgolly/scalajs-react/blob/master/core/src/main/scala/japgolly/scalajs/react/component/builder/ViaReactComponent.scala#L258.

And that's as far as I got for now, I haven't figured out what this is supposed to be in this context... @japgolly can you think of a way to work around this? We would like to access the JS class component, or alternatively be able to wrap the Scala component in a JS class component.

@japgolly
Copy link
Owner

Just a word of warning. I use JS components from within Scala often but I've never really called Scala components from JS. I've made all the raw JS representations accessible via .raw but I haven't considered usage any more advanced than that.

@toddburnside Yeah, all the custom Scala methods go in the backend so to access from JS you'd have to access the backend first. If you want to execute a Callback from JS I have no idea. It's a value class over Trampoline[A] so probably the Trampoline[A] is all you'd see from JS, in which case maybe we should expose a JS method called runNow() that you could call from JS and matches Callback? If React JS will actually stick to JS classes for components and not change again, we should probably support doing the same in Scala too, like Slinky does and without the "backend" concept. In which case when using it from JS you wouldn't need to call .backend first. Problem is I have no income, working at a loss this year so I don't see myself sitting down for a two weeks (?) to implement this so unless I get funding or someone contributes it it won't happen for a while (sorry). Going via .backend or providing your own JS wrapper to avoid users doing so would be the short-term solution to making it feel 100% like JS.

@rpiaggio Sorry man I don't quite understand what you mean. That JS component is calling this because it's accessing it's a class-level field set in the constructor. I don't see where Scala <-> JS comes in to it. To simulate that in Scala you could have private val textInput = ... in your backend and you could also create methods like getValue() in your backend too, but like above, to use it from JS you'd have to call .backend first or create your own JS wrapper :(

@toddburnside
Copy link
Contributor Author

Thanks for the response @japgolly . At the moment we are looking into an option that does not require the above. If that doesn't work out, I may try the JS wrapper for the Scala component.

@japgolly
Copy link
Owner

japgolly commented Feb 9, 2021

I believe I've come up with a solution to this.

  • For starters, users must annotate members in their backend with @JSExport. Eg:

    class Backend($: BackendScope[Unit, State]) {
    
      @JSExport
      def welcome1() = "why hello there!"
    
      @JSExport
      def welcome2 = "hey mate!"
    
      @JSExport
      var v = 123
    
      @JSExport
      val field = "i'm a field"
  • We could make scalajs-react add code to component constructors (either for all components with backends, or maybe if a directive is specified in Scala) so that when a component and backend is first created, the JS uses runtime reflection to automatically generate forwarders on itself to the exposed methods in the backend.

    Say m is a raw instance of a Scala component with a backend, m.backend gives you the backend class and Object.getOwnPropertyNames(m.backend.__proto__) returns a superset of all exposed methods. We can then iterate over them, filter out conflicts, and generate forwarders. Each forwarder would depend on the member type (fn, fn(), var, val, anything else?).

    Screenshot_2021-02-09_13-01-34

  • Extra points: maybe these should be added to m.__proto__ once rather than every instance of m? It's probably more efficient and might be more idiomatic for the JS world (?). Will have to investigate the nuances and details of how JS and prototypes work.

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

No branches or pull requests

3 participants