Skip to content

Commit

Permalink
Add Spaces Component Locking example
Browse files Browse the repository at this point in the history
  • Loading branch information
GregHolmes committed Dec 16, 2024
1 parent 8c619a5 commit ef141a1
Show file tree
Hide file tree
Showing 26 changed files with 4,553 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/spaces-component-locking/javascript/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_PUBLIC_ABLY_KEY=
36 changes: 36 additions & 0 deletions examples/spaces-component-locking/javascript/.gitignore
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
39 changes: 39 additions & 0 deletions examples/spaces-component-locking/javascript/README.md
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.
16 changes: 16 additions & 0 deletions examples/spaces-component-locking/javascript/index.html
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>
22 changes: 22 additions & 0 deletions examples/spaces-component-locking/javascript/package.json
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 examples/spaces-component-locking/javascript/src/LockedField.ts
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 examples/spaces-component-locking/javascript/src/script.ts
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,
});
});
});
}
Loading

0 comments on commit ef141a1

Please sign in to comment.