Skip to content

Commit 9f40624

Browse files
VeskeRGregHolmes
authored andcommitted
Add LiveObjects LiveMap example
1 parent 8256185 commit 9f40624

15 files changed

+2214
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_PUBLIC_ABLY_KEY=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Working with LiveMap from LiveObjects
2+
3+
This folder contains the code for LiveMap (Typescript) - a demo of how you can leverage [Ably LiveObjects](https://ably.com/docs/liveobjects) to create, subscribe to, and update a LiveMap object synchronized across clients in realtime.
4+
5+
## Getting started
6+
7+
1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found:
8+
9+
```sh
10+
git clone [email protected]:ably/docs.git
11+
```
12+
13+
2. Change directory:
14+
15+
```sh
16+
cd /examples/liveobjects-live-map/javascript/
17+
```
18+
19+
3. Rename the environment file:
20+
21+
```sh
22+
mv .env.example .env.local
23+
```
24+
25+
4. In `.env.local` update the value of `VITE_PUBLIC_ABLY_KEY` to be your Ably API key.
26+
27+
5. Install dependencies:
28+
29+
```sh
30+
yarn install
31+
```
32+
33+
6. Run the server:
34+
35+
```sh
36+
yarn run dev
37+
```
38+
39+
7. Try it out by opening two tabs to [http://localhost:5173/](http://localhost:5173/) with your browser to see the result.
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link href="https://fonts.googleapis.com/css?family=Inter" rel="stylesheet" />
7+
<link rel="stylesheet" href="src/styles.css" />
8+
<title>LiveMap example</title>
9+
</head>
10+
<body class="font-inter">
11+
<div class="flex justify-center items-center min-h-screen p-4">
12+
<div class="w-1/2 h-1/2 flex flex-col space-y-4">
13+
<h2 id="title" class="text-xl font-bold text-center mb-4">Realtime Task Board</h2>
14+
<div class="flex space-x-2">
15+
<input
16+
id="task-input"
17+
placeholder="Enter task"
18+
class="flex-grow px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
19+
type="text"
20+
value=""
21+
/>
22+
<button id="add-task" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
23+
Add
24+
</button>
25+
26+
<button id="remove-tasks" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
27+
Remove all
28+
</button>
29+
</div>
30+
31+
<div class="h-full border rounded-lg overflow-y-auto bg-white shadow-lg">
32+
<div id="tasks" class="p-4 space-y-4"></div>
33+
</div>
34+
</div>
35+
</div>
36+
<script type="module" src="src/script.ts"></script>
37+
</body>
38+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "liveobjects-live-map",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"devDependencies": {
12+
"autoprefixer": "^10.4.20",
13+
"dotenv": "^16.4.5",
14+
"postcss": "^8.4.47",
15+
"tailwindcss": "^3.4.13",
16+
"typescript": "^5.6.3"
17+
},
18+
"dependencies": {
19+
"ably": "file:./ably-2.5.0-liveobjects.tgz",
20+
"nanoid": "^5.0.7",
21+
"vite": "^5.4.2"
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { LiveMap } from 'ably';
2+
3+
export type Tasks = LiveMap<{ [key: string]: string }>;
4+
5+
declare global {
6+
export interface LiveObjectsTypes {
7+
root: {
8+
tasks: Tasks;
9+
};
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { DefaultRoot, LiveMap, Realtime } from 'ably';
2+
import LiveObjects from 'ably/liveobjects';
3+
import { nanoid } from 'nanoid';
4+
import { Tasks } from './ably.config';
5+
import './styles.css';
6+
7+
const client = new Realtime({
8+
clientId: nanoid(),
9+
key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
10+
environment: 'sandbox',
11+
plugins: { LiveObjects },
12+
});
13+
14+
const urlParams = new URLSearchParams(window.location.search);
15+
16+
const channelName = urlParams.get('name') || 'liveobjects-live-map';
17+
const channel = client.channels.get(channelName, { modes: ['STATE_PUBLISH', 'STATE_SUBSCRIBE'] });
18+
19+
const taskInput = document.getElementById('task-input') as HTMLInputElement;
20+
const addTaskButton = document.getElementById('add-task');
21+
const tasksDiv = document.getElementById('tasks');
22+
const removeAllTasksDiv = document.getElementById('remove-tasks');
23+
24+
async function main() {
25+
await channel.attach();
26+
27+
const liveObjects = channel.liveObjects;
28+
const root = await liveObjects.getRoot();
29+
30+
await initTasks(root);
31+
addEventListenersToButtons(root);
32+
}
33+
34+
async function initTasks(root: LiveMap<DefaultRoot>) {
35+
// subscribe to root to get notified when tasks object gets changed on the root.
36+
// for example, when we clear all tasks
37+
root.subscribe(({ update }) => {
38+
if (update.tasks === 'updated') {
39+
subscribeToTasksUpdates(root.get('tasks'));
40+
}
41+
});
42+
43+
if (root.get('tasks')) {
44+
subscribeToTasksUpdates(root.get('tasks'));
45+
return;
46+
}
47+
48+
await root.set('tasks', await channel.liveObjects.createMap());
49+
}
50+
51+
function subscribeToTasksUpdates(tasks: Tasks) {
52+
tasksDiv.innerHTML = '';
53+
54+
tasks.subscribe(({ update }) => {
55+
Object.entries(update).forEach(async ([taskId, change]) => {
56+
switch (change) {
57+
case 'updated':
58+
tasksOnUpdated(taskId, tasks);
59+
break;
60+
case 'removed':
61+
tasksOnRemoved(taskId);
62+
break;
63+
}
64+
});
65+
});
66+
67+
for (const [taskId] of tasks.entries()) {
68+
createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
69+
}
70+
}
71+
72+
function tasksOnUpdated(taskId: string, tasks: Tasks) {
73+
const taskSpan = document.querySelector(`.task[data-task-id="${taskId}"] > span`);
74+
if (taskSpan) {
75+
taskSpan.innerHTML = tasks.get(taskId);
76+
} else {
77+
createTaskDiv({ id: taskId, title: tasks.get(taskId) }, tasks);
78+
}
79+
}
80+
81+
function tasksOnRemoved(taskId: string) {
82+
document.querySelector(`.task[data-task-id="${taskId}"]`)?.remove();
83+
}
84+
85+
function createTaskDiv(task: { id: string; title: string }, tasks: Tasks) {
86+
const { id, title } = task;
87+
88+
const parser = new DOMParser();
89+
const taskDiv = parser.parseFromString(
90+
`<div class="flex justify-between items-center rounded space-x-4 task" data-task-id="${id}">
91+
<span class="flex-grow">${title}</span>
92+
<button class="bg-blue-500 hover:bg-blue-600 text-white px-2 py-1 rounded update-task">Edit</button>
93+
<button class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded remove-task">Remove</button>
94+
</div>`,
95+
'text/html',
96+
).body.firstChild as HTMLElement;
97+
98+
tasksDiv.appendChild(taskDiv);
99+
100+
taskDiv.querySelector('.update-task').addEventListener('click', async () => {
101+
const newTitle = prompt('New title for a task:');
102+
if (!newTitle) {
103+
return;
104+
}
105+
await tasks.set(id, newTitle);
106+
});
107+
taskDiv.querySelector('.remove-task').addEventListener('click', async () => {
108+
await tasks.remove(id);
109+
});
110+
}
111+
112+
function addEventListenersToButtons(root: LiveMap<DefaultRoot>) {
113+
addTaskButton.addEventListener('click', async () => {
114+
const taskTitle = taskInput.value.trim();
115+
if (!taskTitle) {
116+
return;
117+
}
118+
119+
const taskId = nanoid();
120+
taskInput.value = '';
121+
await root.get('tasks').set(taskId, taskTitle);
122+
});
123+
124+
removeAllTasksDiv.addEventListener('click', async () => {
125+
await root.set('tasks', await channel.liveObjects.createMap());
126+
});
127+
}
128+
129+
main()
130+
.then()
131+
.catch((e) => console.error(e));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('tailwindcss').Config} */
2+
module.exports = {
3+
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
4+
theme: {
5+
extend: {},
6+
},
7+
plugins: [],
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"compilerOptions": {
3+
"lib": [
4+
"dom",
5+
"es2015"
6+
],
7+
"outDir": "./lib/cjs/",
8+
"sourceMap": true,
9+
"declaration": true,
10+
"noImplicitAny": true,
11+
"module": "commonjs",
12+
"target": "es2017",
13+
"allowJs": true,
14+
"moduleResolution": "node",
15+
"esModuleInterop": true
16+
},
17+
"include": [
18+
"src/**/*.ts*"
19+
],
20+
"exclude": [
21+
"node_modules",
22+
"dist",
23+
"lib"
24+
]
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
interface ImportMetaEnv {
2+
readonly VITE_PUBLIC_ABLY_KEY: string;
3+
// Add other environment variables here if needed
4+
}
5+
6+
interface ImportMeta {
7+
readonly env: ImportMetaEnv;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from 'vite';
2+
3+
export default defineConfig({
4+
css: {
5+
postcss: {
6+
plugins: [require('tailwindcss'), require('autoprefixer')],
7+
},
8+
},
9+
});

0 commit comments

Comments
 (0)