diff --git a/js-lib/README.md b/js-lib/README.md
index 2ebe83f..469dbe7 100644
--- a/js-lib/README.md
+++ b/js-lib/README.md
@@ -1,18 +1,46 @@
-// const trame = new Trame({ iframe });
-// await trame.connect({ application: 'trame' });
+# Trame iframe client library for plain JS
+This library aims to simplify interaction between a trame application living inside an iframe and its iframe parent.
+This work is inspired by the [official trame-client js lib](https://github.com/Kitware/trame-client/tree/master/js-lib)
-// State handing
-trame.state.set("a", 5);
-console.log(trame.state.get("b"));
-trame.state.update({
- a: 1,
- b: 2,
-});
+## Examples
+- [Vite](./examples/vite/)
+
+## Usage
+First you need to grab the iframe that contains your trame application.
+```js
+import ClientCommunicator from "@kitware/trame-iframe-client";
+
+const iframe = document.getElementById("trame_app");
+const iframe_url = "http://localhost:3000";
-// Method call on Python
-const result = await trame.trigger("name", [arg_0, arg_1], { kwarg_0: 1, kwarg_1: 2 });
+const trame = new ClientCommunicator(iframe, iframe_url);
-// TODO - state watching
-trame.state.watch(["a"], (a) => {
- console.log(`a changed to ${a}`);
+// set
+trame.state.set("a", 2);
+trame.state.set('b', 3);
+trame.state.update({
+ a: 2.5,
+ b: 3.5,
+ c: 4.5,
})
+
+// get
+console.log(trame.state.get("c"));
+console.log(trame.state.get('a'));
+
+
+// simple api for state change
+trame.state.watch(
+ ["a", "b", "c"],
+ (a, b, c) => {
+ console.log(`a(${a}) or b(${b}) or c(${c}) have changed`);
+ }
+);
+
+// -----------------------------------
+// Method execution API
+// -----------------------------------
+
+// method execution on Python side
+trame.trigger("name", ['arg_0', 'arg_1'], { kwarg_0: 1, kwarg_1: 2 });
+```
diff --git a/js-lib/examples/vite/README.md b/js-lib/examples/vite/README.md
new file mode 100644
index 0000000..e5787f0
--- /dev/null
+++ b/js-lib/examples/vite/README.md
@@ -0,0 +1,25 @@
+# Vite project
+
+This example use npm package to illustrate how to use the trame iframe client.
+
+## Trame setup
+
+```bash
+python3 -m venv .venv
+source .venv/bin/activate
+pip install trame trame-iframe
+```
+
+## Build the client
+
+```bash
+cd client
+npm i
+npm run build
+```
+
+## Running example
+
+```bash
+python ./server.py --port 3000 --server
+```
diff --git a/js-lib/examples/vite/client/.gitignore b/js-lib/examples/vite/client/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/js-lib/examples/vite/client/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/js-lib/examples/vite/client/counter.js b/js-lib/examples/vite/client/counter.js
new file mode 100644
index 0000000..f94ba09
--- /dev/null
+++ b/js-lib/examples/vite/client/counter.js
@@ -0,0 +1,9 @@
+export function setupCounter(element, trame) {
+ trame.state.onReady(() => {
+ trame.state.watch(["count"], (count) => {
+ console.log(`count is ${count}`);
+ element.innerHTML = `count is ${count}`;
+ });
+ });
+ element.addEventListener("click", () => trame.trigger("add"));
+}
diff --git a/js-lib/examples/vite/client/index.html b/js-lib/examples/vite/client/index.html
new file mode 100644
index 0000000..ddc7e32
--- /dev/null
+++ b/js-lib/examples/vite/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/js-lib/examples/vite/client/main.js b/js-lib/examples/vite/client/main.js
new file mode 100644
index 0000000..dc27295
--- /dev/null
+++ b/js-lib/examples/vite/client/main.js
@@ -0,0 +1,29 @@
+import ClientCommunicator from "@kitware/trame-iframe-client";
+import "./style.css";
+import { setupCounter } from "./counter.js";
+
+document.querySelector("#app").innerHTML = `
+
+
Hello Trame !
+
+
+
+
+
+
+
+`;
+
+const url = "http://localhost:3000";
+const iframe = document.getElementById("trame_app");
+
+iframe.addEventListener("load", () => {
+ const trame = new ClientCommunicator(iframe, url);
+ setupCounter(document.querySelector("#counter"), trame);
+ document
+ .querySelector("#play")
+ .addEventListener("click", () => trame.trigger("toggle_play"));
+ document
+ .querySelector("#subtract")
+ .addEventListener("click", () => trame.trigger("subtract"));
+});
diff --git a/js-lib/examples/vite/client/package.json b/js-lib/examples/vite/client/package.json
new file mode 100644
index 0000000..892060d
--- /dev/null
+++ b/js-lib/examples/vite/client/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "trame",
+ "version": "0.0.0",
+ "type": "module",
+ "main": "main.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^5.2.0"
+ },
+ "dependencies": {
+ "@kitware/trame-iframe-client": "/home/jules/projects/kitware/trame/repos/trame-iframe/js-lib/dist/trame-iframe.mjs"
+ }
+}
diff --git a/js-lib/examples/vite/client/style.css b/js-lib/examples/vite/client/style.css
new file mode 100644
index 0000000..30aa814
--- /dev/null
+++ b/js-lib/examples/vite/client/style.css
@@ -0,0 +1,96 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+#app {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.vanilla:hover {
+ filter: drop-shadow(0 0 2em #f7df1eaa);
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/js-lib/examples/vite/server.py b/js-lib/examples/vite/server.py
new file mode 100644
index 0000000..701f017
--- /dev/null
+++ b/js-lib/examples/vite/server.py
@@ -0,0 +1,48 @@
+import asyncio
+from trame.app import get_server
+from trame.widgets import iframe
+from trame.ui.html import DivLayout
+
+server = get_server()
+state, ctrl = server.state, server.controller
+
+state.count = 1
+state.play = False
+
+
+@state.change("count")
+def count_change(count, **_):
+ print(f"count={count}")
+
+
+@ctrl.trigger("add")
+def add_to_count():
+ state.count += 1
+
+
+@ctrl.trigger("subtract")
+def subtract_to_count():
+ state.count -= 1
+
+
+@ctrl.trigger("toggle_play")
+def toggle_play():
+ state.play = not state.play
+
+
+async def animate(**kwargs):
+ while True:
+ await asyncio.sleep(0.5)
+ if state.play:
+ with state:
+ state.count += 1
+
+
+ctrl.on_server_ready.add_task(animate)
+
+with DivLayout(server) as layout:
+ comm = iframe.Communicator(
+ target_origin="http://localhost:2222", enable_rpc=True
+ )
+
+server.start()