streamlitでjavascriptからpythonへデータを受け渡したかったが情報がなかったので作れるようなテンプレートを作ってみた


streamlitで現在の座標を取得しようと思ったのですが、軽く調べた感じでは[少し複雑なコードしか出てきませんでした。](https://discuss.streamlit.io/t/web-geolocation-api-to-get-users-location/9493)
ただjavascriptを実行してstreamlitから取り出したいだけなのにbokehいらないと思ったので模索してみたらstreamlitコンポーネントを作る方法にいきつきました。


```python
import streamlit as st
from bokeh.models.widgets import Button
from bokeh.models import CustomJS
from streamlit_bokeh_events import streamlit_bokeh_events

loc_button = Button(label="Get Location")
loc_button.js_on_event("button_click", CustomJS(code="""
    navigator.geolocation.getCurrentPosition(
        (loc) => {
            document.dispatchEvent(new CustomEvent("GET_LOCATION", {detail: {lat: loc.coords.latitude, lon: loc.coords.longitude}}))
        }
    )
    """))
result = streamlit_bokeh_events(
    loc_button,
    events="GET_LOCATION",
    key="get_location",
    refresh_on_update=False,
    override_height=75,
    debounce_time=0)
```

streamlit=>javascriptのパターンはあってもjavascript=>streamlitのパターンがなかったので大変でした。

できあがったリポジトリがこちらです。

https://github.com/misogihagi/streamlit-current-location

pypiにも登録しました。

```
pip install streamlit streamlit_current_location
```

## 例

src.tsに受け渡したいデータをStreamlit.setComponentValue()で囲んでStreamlit.setComponentReady()するスクリプトを書き、`npm run build`でstreamlitが読み込められるようなコンポーネントを作ります。
そこからは通常のstreamlitコンポーネントと同じように扱います。
例えばWeb Speech APIを使って色の音声認識をするにはsrc.tsに以下のスクリプトを書きます。

```typescript:src.ts
const grammar =
  "#JSGF V1.0; grammar colors; public <color> = aqua | azure | beige | bisque | black | blue | brown | chocolate | coral | crimson | cyan | fuchsia | ghostwhite | gold | goldenrod | gray | green | indigo | ivory | khaki | lavender | lime | linen | magenta | maroon | moccasin | navy | olive | orange | orchid | peru | pink | plum | purple | red | salmon | sienna | silver | snow | tan | teal | thistle | tomato | turquoise | violet | white | yellow ;";
const recognition = new SpeechRecognition();
const speechRecognitionList = new SpeechGrammarList();
speechRecognitionList.addFromString(grammar, 1);
recognition.grammars = speechRecognitionList;
recognition.continuous = false;
recognition.lang = "en-US";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

window.onload = () => {
  recognition.start();
  recognition.onresult = (event) => {
    const color = event.results[0][0].transcript;
    Streamlit.setComponentValue(coordsToObject(color));
  };

  Streamlit.setComponentReady();
};
```



## 解説

### python部分

streamlitに直接読み出される部分です。
特になにもしないので最小限のライブラリ読み出して終わりです。
dist以下に本処理が書かれたファイルを置きます。

```python:python部分
import os

import streamlit.components.v1 as components

parent_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(parent_dir, "dist")


def current_position():
    component = components.declare_component("current_position", path=build_dir)
    value = component(name="current_position")
    return value
```

### index.html

こちらも特に何もしません。
スクリプトの名前はindex.jsにしておきました。

```html:dist/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Streamlit Component</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Streamlit Component" />
    <script src="index.js"></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
  </body>
</html>
```

### dist/index.js(src.ts)

本処理の部分です。
streamlitはjsonにできる純粋なオブジェクトじゃないとだめなのでオブジェクトにする関数を作っています。
domが読み込まれたらnavigator.geolocation.getCurrentPositionで現在の座標をとってきてStreamlit.setComponentValueでstreamlitに伝えるだけの簡単なスクリプトです。
テンプレートの方ではわざわざビルド用の設定が入っていましたが今はesbuildがありますので
`esbuild src.ts --bundle --minify --sourcemap --outfile=dist/index.js`で十分です。
`--watch`オプションをつけてあげればデバッグも簡単です。
現代フロントエンドバンザイ！


```typescript:src.ts
import { Streamlit } from "streamlit-component-lib";

const coordsToObject = ({
  accuracy,
  altitude,
  altitudeAccuracy,
  heading,
  latitude,
  longitude,
  speed,
}: GeolocationCoordinates) => ({
  accuracy,
  altitude,
  altitudeAccuracy,
  heading,
  latitude,
  longitude,
  speed,
});

window.onload = () => {
  navigator.geolocation.getCurrentPosition(({ coords }) => {
    Streamlit.setComponentValue(coordsToObject(coords));
  }, console.log);

  Streamlit.setComponentReady();
};

```