japgolly / scalajs-react

Facebook's React on Scala.JS
https://japgolly.github.io/scalajs-react/
Apache License 2.0
1.65k stars 232 forks source link

Methods on component accessible to JS #837

Open toddburnside opened 3 years ago

toddburnside commented 3 years ago

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 commented 3 years ago

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 :grinning:).

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

toddburnside commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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