dop251 / goja

ECMAScript/JavaScript engine in pure Go
MIT License
5.63k stars 378 forks source link

go<->js interop: map forEach #605

Closed p8a closed 2 months ago

p8a commented 2 months ago

I have an object model defined in Go which I want to pass to JS rules to be verified but I cannot figure out, for example, how to get the string or array length of the Go objects from JS.

This is the Go code:

s.vm.Set("model", &Model{
        Single:   "single",
        Multiple: []string{"one", "two", "three"},
})
s.vm.Set("log", fmt.Println)
...
s.vm.RunProgram(rule)

First attempt was to use JS length property which fails (I'm assuming Go strings are not mapped to actual JS strings):

log(model.single.length);

results in

TypeError: Cannot read property 'length' of undefined at rules/test.js:1:18(3)

Second attempt was to write a Go function which will calculate the length - however that function gets undefined as the argument instead of the actual value.

Updated Go code:

s.vm.Set("model", &Model{
        Single:   "single",
        Multiple: []string{"one", "two", "three"},
})
s.vm.Set("log", fmt.Println)
s.vm.Set("length", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) == 1 {
    v := call.Argument(0)
    fmt.Println("go arg:", v.Export())
}

return s.vm.ToValue(len(call.Arguments))
})

Updated JS code:

log("js:", length(model.single));
log("js:", length(model.multiple));

Output:

go arg: <nil>
js: 1
go arg: <nil>
js: 1

What is the best way to make the more complex Go structures available to the JavaScript code while still being able to use length, forEach, map, etc ?

I'm assuming manually exporting the Go structure into a similar goja model using goja types (string, array) would work but the model is rather large.

dop251 commented 2 months ago

The property name will be model.Single, not model.single, unless you use UncapFieldNameMapper.

p8a commented 2 months ago

Thanks, UncapFieldNameMapper fixes both situations (I was using TagFieldNameMapper).

p8a commented 2 months ago

I have a related question: what is the best, most efficient way to expose a Go map to JS ? If I do it directly forEach does not work - although it does work for arrays:

Go code:

s.vm.Set("arrayTest", []string{
        "one", "two", "three",
})
s.vm.Set("mapTest", map[string][]string{
"one": {"a", "b"},
"two": {"c", "d"},
})

JavaScript code:

arrayTest.forEach((v) => log("array:", v));

mapTest.forEach((v, k, m) => {
    log("map:", k);
})

Output:

array: one
array: two
array: three
rules/foreach.js TypeError: Object has no member 'forEach' at rules/foreach.js:8:16(8)

Manually constructing a Map object in JS using Object.entries works:

const foo = new Map(Object.entries(mapTest));
foo.forEach((v, k, m) => {
    log("js map:", k, v);
})

Output:

js map: one [a b]
js map: two [c d]

Thanks

dop251 commented 2 months ago

Please make sure you read the documentation for the ToValue() method. It should answer all your questions.

p8a commented 2 months ago

I did, just wanted to confirm my understanding is correct: to get a "true" Map (one that has foreach, etc) I have to initialize it either in JavaScript (my example from the last comment) or in Go - there is no "auto-magic" which wraps Go maps in JavaScript Map