-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Spaces Component Locking example
- Loading branch information
1 parent
8c619a5
commit ef141a1
Showing
26 changed files
with
4,553 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
VITE_PUBLIC_ABLY_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Locking components within collaborative applications | ||
|
||
This folder contains the code for the component locking (Typescript) - a demo of how you can leverage [Ably Spaces](https://github.com/ably/spaces) to lock components within a form or web page. | ||
|
||
## Getting started | ||
|
||
1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found: | ||
|
||
```sh | ||
git clone [email protected]:ably/docs.git | ||
``` | ||
|
||
2. Change directory: | ||
|
||
```sh | ||
cd /examples/spaces-component-locking/javascript/ | ||
``` | ||
|
||
3. Rename the environment file: | ||
|
||
```sh | ||
mv .env.example .env.local | ||
``` | ||
|
||
4. In `.env.local` update the value of `VITE_PUBLIC_ABLY_KEY` to be your Ably API key. | ||
|
||
5. Install dependencies: | ||
|
||
```sh | ||
yarn install | ||
``` | ||
|
||
6. Run the server: | ||
|
||
```sh | ||
yarn run dev | ||
``` | ||
|
||
7. Try it out by opening two tabs to [http://localhost:5173/](http://localhost:5173/) with your browser to see the result. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Component locking</title> | ||
<link rel="stylesheet" href="src/styles.css" /> | ||
<link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'> | ||
</head> | ||
<body style="font-family: 'Inter';font-size: 22px;"> | ||
<div id="component-locking" class="container"> | ||
<div id="inner" class="inner"></div> | ||
</div> | ||
<script type="module" src="src/script.ts"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "js-component-locking", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "tsc && vite build", | ||
"preview": "vite preview" | ||
}, | ||
"devDependencies": { | ||
"dotenv": "^16.4.5", | ||
"typescript": "^5.5.4", | ||
"@faker-js/faker": "^9.2.0" | ||
}, | ||
"dependencies": { | ||
"@ably/spaces": "^0.4.0", | ||
"ably": "^2.3.1", | ||
"nanoid": "^5.0.7", | ||
"vite": "^5.4.2" | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
examples/spaces-component-locking/javascript/src/LockedField.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const createLockedFieldSvg = (className: string): SVGElement => { | ||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | ||
svg.setAttribute('width', '1em'); | ||
svg.setAttribute('height', '1em'); | ||
svg.setAttribute('viewBox', '0 0 16 16'); | ||
svg.setAttribute('fill', 'none'); | ||
svg.setAttribute('class', className); | ||
|
||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | ||
path.setAttribute( | ||
'd', | ||
'M12.0003 5.8334H11.3337V4.50007C11.3337 2.66008 9.84032 1.16675 8.00033 1.16675C6.16033 1.16675 4.66699 2.66008 4.66699 4.50007V5.8334H4.00033C3.26699 5.8334 2.66699 6.43339 2.66699 7.16672V13.8334C2.66699 14.5667 3.26699 15.1667 4.00033 15.1667H12.0003C12.7337 15.1667 13.3337 14.5667 13.3337 13.8334V7.16672C13.3337 6.43339 12.7337 5.8334 12.0003 5.8334ZM8.00033 11.8334C7.26699 11.8334 6.66699 11.2334 6.66699 10.5C6.66699 9.76671 7.26699 9.16671 8.00033 9.16671C8.73366 9.16671 9.33366 9.76671 9.33366 10.5C9.33366 11.2334 8.73366 11.8334 8.00033 11.8334ZM6.00033 5.8334V4.50007C6.00033 3.39341 6.89366 2.50008 8.00033 2.50008C9.10699 2.50008 10.0003 3.39341 10.0003 4.50007V5.8334H6.00033Z', | ||
); | ||
path.setAttribute('fill', 'currentColor'); | ||
|
||
svg.appendChild(path); | ||
return svg; | ||
}; |
184 changes: 184 additions & 0 deletions
184
examples/spaces-component-locking/javascript/src/script.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import Spaces, { Space, Lock, type SpaceMember } from '@ably/spaces'; | ||
import { Realtime } from 'ably'; | ||
import { nanoid } from 'nanoid'; | ||
import { createLockedFieldSvg } from './LockedField'; | ||
import { faker } from '@faker-js/faker'; | ||
|
||
type Member = Omit<SpaceMember, 'profileData'> & { | ||
profileData: { memberColor: string; memberName: string }; | ||
}; | ||
|
||
interface Entry { | ||
label: string; | ||
name: string; | ||
} | ||
let space: Space; | ||
const entries: Entry[] = [ | ||
{ label: 'Entry 1', name: 'entry1' }, | ||
{ label: 'Entry 2', name: 'entry2' }, | ||
{ label: 'Entry 3', name: 'entry3' }, | ||
]; | ||
|
||
const client = new Realtime({ | ||
clientId: nanoid(), | ||
key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string, | ||
}); | ||
|
||
connect(); | ||
|
||
async function connect() { | ||
buildForm(); | ||
|
||
const spaces = new Spaces(client); | ||
space = await spaces.get('component-locking'); | ||
|
||
// /** 💡 Enter the space as soon as it's available 💡 */ | ||
await space.enter({ | ||
memberName: faker.person.fullName(), | ||
memberColor: faker.color.rgb({ format: 'hex', casing: 'lower' }), | ||
}); | ||
|
||
const locks = await space.locks.getAll(); | ||
|
||
if (locks.length > 0) { | ||
locks.map(async (lock) => { | ||
await updateComponent(lock); | ||
}); | ||
} | ||
|
||
// /** 💡 Subscribe to all component updates 💡 */ | ||
space.locks.subscribe('update', async (componentUpdate) => { | ||
// /** 💡 Update form on each components update 💡 */ | ||
console.log(componentUpdate); | ||
await updateComponent(componentUpdate); | ||
}); | ||
} | ||
|
||
async function updateComponent(componentUpdate: Lock) { | ||
const self = await space.members.getSelf(); | ||
const locked = componentUpdate?.status === 'locked'; | ||
|
||
const inputElement = document.getElementById(componentUpdate.id) as HTMLInputElement; | ||
const parentDiv = inputElement.closest('.input-cell-container'); | ||
const inputContainer = parentDiv?.querySelector('.input-container'); | ||
|
||
if (locked) { | ||
const lockHolder = componentUpdate.member as Member; | ||
const lockedByYou = locked && lockHolder?.connectionId === self?.connectionId; | ||
const readOnly = Boolean(lockHolder && !lockedByYou); | ||
|
||
const memberColor = lockHolder?.profileData.memberColor; | ||
const memberName = lockedByYou ? 'You' : lockHolder?.profileData.memberName; | ||
|
||
const lockedDiv = document.createElement('div'); | ||
lockedDiv.className = 'lock'; | ||
lockedDiv.id = 'lock'; | ||
lockedDiv.innerHTML = `${memberName} ${lockedByYou ? '' : createLockedFieldSvg('text-base').outerHTML}`; | ||
lockedDiv.style.setProperty('--member-bg-color', memberColor); | ||
inputContainer?.appendChild(lockedDiv); | ||
|
||
inputElement.style.setProperty('--member-bg-color', memberColor); | ||
inputElement.setAttribute('data-locked', 'true'); | ||
|
||
if (lockHolder) { | ||
inputElement.classList.remove('regular-cell'); | ||
inputElement.classList.add('active-cell'); | ||
} else { | ||
inputElement.classList.add('locked'); | ||
} | ||
|
||
if (readOnly) { | ||
inputElement.classList.remove('full-access'); | ||
inputElement.classList.add('read-only'); | ||
} | ||
} else { | ||
const lockedDiv = inputContainer?.querySelector('.lock'); | ||
|
||
if (lockedDiv) { | ||
inputContainer?.removeChild(lockedDiv); | ||
} | ||
inputElement.removeAttribute('data-locked'); | ||
inputElement.classList.remove('locked', 'read-only', 'active-cell'); | ||
inputElement.classList.add('regular-cell', 'full-access'); | ||
inputElement.style.removeProperty('--member-bg-color'); | ||
} | ||
} | ||
|
||
const handleFocus = async (event: FocusEvent) => { | ||
const focusedElement = event.target as HTMLInputElement; | ||
const currentlyLockedElement = document.querySelector('[data-locked="true"]') as HTMLInputElement; | ||
|
||
if (currentlyLockedElement) { | ||
await space?.locks.release(currentlyLockedElement.name); | ||
} | ||
|
||
if (focusedElement.getAttribute('data-locked') === 'true') { | ||
return; | ||
} | ||
|
||
await space?.locks.acquire(focusedElement.name); | ||
}; | ||
|
||
const handleBlur = async (event: FocusEvent) => { | ||
const focusedElement = event.target as HTMLInputElement; | ||
|
||
if (focusedElement.getAttribute('data-locked') !== 'true') { | ||
return; | ||
} | ||
|
||
if (event.relatedTarget) { | ||
space?.locks.release(focusedElement.name); | ||
} | ||
}; | ||
|
||
function buildForm() { | ||
const innerContainer = document.getElementById('inner'); | ||
|
||
entries.map((entry) => { | ||
const inputCellContainer = document.createElement('div'); | ||
inputCellContainer.id = 'input-cell-container'; | ||
inputCellContainer.className = 'input-cell-container'; | ||
inputCellContainer.style.setProperty('--member-bg-color', '#AC8600'); | ||
const entryLabel = document.createElement('label'); | ||
entryLabel.className = 'label'; | ||
entryLabel.setAttribute('for', entry.name); | ||
entryLabel.textContent = entry.label; | ||
|
||
const inputContainer = document.createElement('div'); | ||
inputContainer.className = 'input-container'; | ||
inputContainer.id = 'input-container'; | ||
|
||
const formInput = document.createElement('input'); | ||
formInput.id = entry.name; | ||
formInput.className = `input regular-cell full-access`; | ||
formInput.placeholder = 'Click to lock and edit me'; | ||
formInput.name = entry.name; | ||
formInput.onfocus = (event) => { | ||
handleFocus(event); | ||
}; | ||
formInput.onblur = (event) => { | ||
handleBlur(event); | ||
}; | ||
|
||
innerContainer?.appendChild(inputCellContainer); | ||
inputCellContainer.appendChild(entryLabel); | ||
inputCellContainer?.appendChild(inputContainer); | ||
inputContainer?.appendChild(formInput); | ||
|
||
const channel = client.channels.get(`component-locking-${entry.name}`); | ||
|
||
channel.subscribe('update', (message) => { | ||
const input = document.getElementById(entry.name) as HTMLInputElement; | ||
if (input) { | ||
input.value = message.data.value; | ||
} | ||
}); | ||
|
||
formInput.addEventListener('input', (event) => { | ||
const target = event.target as HTMLInputElement; | ||
channel.publish('update', { | ||
value: target.value, | ||
}); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.