Hello,
I have POCed the change here https://github.com/Jasonfran/wails/tree/support_for_struct_and_nilable_parameters_in_function_bindings and want some feedback.
To me this seems like the most logical way to handle function bindings. It means we don't have to depend on more libraries just to decode a map of values. You can pass in complex structs and have the parameter effortlessly unmarshalled for you.
Is there a design decision as to why you went the map[string]interface{} route rather than just unmarshalling to a struct? Or is there something I've missed?
It works like a dream from what I've tested of it and I think it's a nicer API for users too.
To expand on this further I think it could be useful to then generate typescript interfaces for these struct parameters.
Please let me know what you think!
Thanks
Hi @Jasonfran - thanks for this! Will check it out today. We are always up for more improvements! There probably was a design decision at the time but that would have been over 2 years ago now. I'll get some feedback to you but yes, I'm surprised we aren't already doing this.
Thanks! 馃憤
@Jasonfran I just created a test project to test structs and nilables using the latest branch:
package main
import (
"github.com/leaanthony/mewn"
"github.com/wailsapp/wails"
)
func basic() *Person {
return &Person{
Name: "John Doe",
Address: Address{
Street: "Nowheresville",
},
}
}
type Person struct {
Name string
Address Address
Parner *Partner
}
type Address struct {
Street string
}
type Partner struct {
Person *Person
}
func main() {
js := mewn.String("./frontend/build/main.js")
css := mewn.String("./frontend/build/main.css")
app := wails.CreateApp(&wails.AppConfig{
Width: 1024,
Height: 768,
Title: "structtest",
JS: js,
CSS: css,
Colour: "#131313",
})
app.Bind(basic)
app.Run()
}
and if I print the result in JS, the output is:
{Name: "John Doe", Address: {Street: "Nowheresville"}, Parner: null}
This works for both structs and pointers to structs.
What are you proposing we do differently?
Cheers.
Ah, I think I may have misunderstood. You're talking about input values not return values. Do you have an example where this is an issue and we can work to that? Thanks!
Yes input parameters. I don't have an example at hand but just give your basic function a Person parameter and then build an object in JavaScript that matches that and try to pass it as a parameter.
The first thing I did when trying out Wails was having a data model in JavaScript/Typescript and seeing how Wails maps that to a Go model as the only examples for Wails used primitive types as parameters, not structs. I found out (through trying it and reading the docs) that by default it is unmarshalled to a map and so that would never work.
Do you agree that this would be a more natural and user friendly way to pass complex types from JS to Go?
Yes for sure. I guess the stance we've always taken is that your state should live in Go and your frontend should be simply a view of that data. I can see where getting state back is beneficial and so this approach seems good to me. I've long thought about a means for doing this via a synced state store, but I think that may be non-trivial and end up being a bit of a hack.
The hard part about this approach is ensuring the structures are correct. Ideally you could generate js/ts versions of your bound objects to ensure consistency but again, this becomes non-trivial.
Yeah I agree with that. But I'm also not a fan of having functions with lots of parameters. For example, if I want to search some data I could create a function like this:
func Search(firstname string, lastname string, ageLowerBound int, ageUpperBound int, addressLine1 string, addressLine2 string, postCode string) []Person {
}
There's nothing really wrong with that but I prefer small parameter lists and passing in structs which hold the information where beneficial. Since the function signature stays the same and you can expand it in a backwards compatible way:
type FilterParameters struct {
Firstname string
Lastname string
AgeLowerBound int
AgeUpperBound int
AddressLine1 string
AddressLine2 string
PostCode string
}
func Search(filterParams FilterParameters) []Person {
}
I've also updated and simplified my code to support slice parameters too. So previously something like this wasn't possible:
func DeletePeople(ids []int) {
}
Obviously there's ways around this by just having a function that deletes a single person and calling that multiple times. But for ease of use and a nice API I think this is beneficial.
Clearly there's many different approaches for developing applications with Wails. In my head Wails is like a client and server, like a single page application in a web browser. So client and server state exist separately and pass data back and forth as necessary.
Whereas many people might take the approach that Wails controls everything and maintains page state. For example, ticking a checkbox will pass that info back to Go, generate a new view model and then pass that back to React.
Anyway... If you're happy with this feature then it'd have to be done differently to how I POCed it as it's not backwards compatible for anyone that currently depends on the map[string]interface{} behaviour. I can work on developing it in a backwards compatible way. I envisage something like app.SmartBind().
This is something I think should be default in V2. Changing the API is ok so long as you can provide warning and an auto-migrate path. Perhaps we should think more about what the current Bind method should be renamed to...BindInterface?
I'm good to do the migration part. I think this is a good step forward and opens the door to something else I want to do.
Would supporting GRPC as an alternative RPC system work ?
Could use https://github.com/grpc/grpc-web for the frontend and https://github.com/improbable-eng/grpc-web/tree/master/go/grpcwebproxy for backend.
Then users could spec all their RPC structs in proto files and generate the go and ts sources.
Could probably support something like
frontend
import * as Wails from '@wailsapp/runtime';
import * as grpcWeb from 'grpc-web';
import {EchoServiceClient} from './echo_grpc_web_pb';
import {EchoRequest, EchoResponse} from './echo_pb';
const echoService = new EchoServiceClient(Wails.GRPC_ENDPOINT, null, null);
const request = new EchoRequest();
request.setMessage('Hello World!');
const call = echoService.echo(request, {'custom-header-1': 'value1'},
(err: grpcWeb.Error, response: EchoResponse) => {
console.log(response.getMessage());
});
call.on('status', (status: grpcWeb.Status) => {
// ...
});
backend
app.BindGrpc(&echoServer{})
Or could even apply the same idea to any http/websocket based RPC technology
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
@Jasonfran - are you still up for this? I've written a project parser for v2 and wanted to bring this up again.
Hey, yeah for sure.
Am looking at this with respect to sync stores. I just made a PR and realise that auto data transformation is probably something we want to support. It'd work in the same way as calls. I want to make sure it supports nilable types so will be looking at this shortly 馃憤
I've had a look at this and it appears pretty awesome! The difference in my implementation for stores and your branch is the fact you've unified the decoding which means it's missing type conversion. Example: sending a number from the front end will always turn up as a float64, but if the parameter is an int then it'll fail. Did I miss the type conversion in your code?
These are the methods I'm talking about: https://github.com/wailsapp/wails/search?q=convert&unscoped_q=convert
I'll get stores working right (I'm very close) then push it up. Between us, i'm sure we can get a solution into the main calls.
In my branch there's an example of the conversion here:
decodedVal := reflect.New(typ).Interface()
err = json.Unmarshal(rawMsg, decodedVal)
So in this function typ is the parameter type for a parameter in the bound function.
I new up that parameter type. So in the case of something like int then decodedVal is an *int, or *int is an **int
Pass that into json.Unmarshal which knows how to handle every type anyway. It automatically translates null to the zero value of that type. So 0 in the case of int or nil in the case of *int. And for any other valid values it's what you'd expect.
It then goes through this,
if decodedVal == nil {
result = reflect.Zero(typ)
} else {
result = reflect.ValueOf(decodedVal).Elem()
}
which I just realised isn't actually needed. Because if you pass null as the parameter value for an int then decodedVal is an *int pointing at the zero value 0. Or if it's an *int then decodedVal is an **int pointing at a nil *int. So you can just do result = reflect.ValueOf(decodedVal).Elem()
Hope that makes sense? It's basically just delegating any json to Go type conversion to the json package since it does all that for us. We only have to make sure we new up the correct type for it to unmarshal into.
I totally missed that nuance! That's really interesting. I'll see if this also simplifies the sync store code. Thanks for your time in responding. I'm up for using this in IPC code too.
@Jasonfran - some feedback. This technique works really well in the sync stores. I'll look at porting it to the call handlers 馃憤
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Stop it bot
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Behave. I'm getting to it