Skip to content

Commit

Permalink
#863 - Add @tomic-react docs and collectionPage hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Mar 28, 2024
1 parent 616412a commit 8f7f2ce
Show file tree
Hide file tree
Showing 15 changed files with 488 additions and 83 deletions.
5 changes: 5 additions & 0 deletions browser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ This changelog covers all three packages, as they are (for now) updated as a who
- `resource.removeClasses()`
- `resource.addClasses()`

# @tomic/react

- Added `useCollectionPage` hook.
- Fix bug where `useCollection` would fetch the collection twice on mount.

### @tomic/cli

- [#837](https://github.com/atomicdata-dev/atomic-server/issues/837) Fix timestamp is mapped to string instead of number.
Expand Down
4 changes: 2 additions & 2 deletions browser/lib/src/websockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ export async function authenticate(

// Maybe this should happen after the authentication is confirmed?
fetchAll &&
this.resources.forEach(r => {
store.resources.forEach(r => {
if (r.isUnauthorized() || r.loading) {
this.fetchResourceFromServer(r.getSubject());
store.fetchResourceFromServer(r.subject);
}
});
}
Expand Down
1 change: 1 addition & 0 deletions browser/react/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export function useCanWrite(

if (resource.new) {
setCanWrite(true);
setMsg(undefined);

return;
}
Expand Down
1 change: 1 addition & 0 deletions browser/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export * from './useMarkdown.js';
export * from './useServerSearch.js';
export * from './useCollection.js';
export * from './useMemberFromCollection.js';
export * from './useCollectionPage.js';
export * from '@tomic/lib';
8 changes: 8 additions & 0 deletions browser/react/src/useCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export function useCollection(
queryFilter: QueryFilter,
pageSize?: number,
): UseCollectionResult {
const [firstRun, setFirstRun] = useState(true);

const store = useStore();
const [server] = useServerURL();

Expand All @@ -56,6 +58,12 @@ export function useCollection(
}, []);

useEffect(() => {
if (firstRun) {
setFirstRun(false);

return;
}

const newCollection = buildCollection(
store,
server,
Expand Down
12 changes: 12 additions & 0 deletions browser/react/src/useCollectionPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Collection } from '@tomic/lib';
import { useEffect, useState } from 'react';

export function useCollectionPage(collection: Collection, page: number) {
const [items, setItems] = useState<string[]>([]);

useEffect(() => {
collection.getMembersOnPage(page).then(setItems);
}, [collection, page]);

return items;
}
6 changes: 5 additions & 1 deletion docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@
- [@tomic/react](usecases/react.md)
- [useStore](react/useStore.md)
- [useResource](react/useResource.md)
- [useValue & friends](react/useValue.md)
- [useValue](react/useValue.md)
- [useCollection](react/useCollection.md)
- [useServerSearch](react/useServerSearch.md)
- [useCurrentAgent](react/useCurrentAgent.md)
- [useCanWrite](react/useCanWrite.md)
- [Examples](react/examples.md)
- [@tomic/svelte](svelte.md)
- [JS CLI](js-cli.md)
- [Rust](rust-lib.md)
Expand Down
92 changes: 92 additions & 0 deletions docs/src/react/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Examples

## Realtime Todo app

In this example, we create a basic to-do app that persists on the server and updates in real-time when anyone makes changes.
If you were to make this in vanilla react without any kind of persistence it would probably look almost the same.
The main difference is the use of the `useArray` and `useBoolean` hooks instead of `useState`.

```jsx
import { useArray, useBoolean, useResource } from '@tomic/react';
import { useState } from 'react';

export const TodoList = () => {
const store = useStore();
const checklist = useResource<Checklist>('https://my-server/checklist/1');

const [todos, setTodos] = useArray(checklist, todoApp.properties.todos, {
commit: true,
});

const [inputValue, setInputValue] = useState('');

const removeTodo = (subject: string) => {
setTodos(todos.filter(todo => todo !== subject));
};

const addTodo = async () => {
const newTodo = await store.newResource({
isA: todoApp.classes.todoItem,
parent: checklist.subject,
propVals: {
[core.properties.name]: inputValue,
[todoApp.properties.done]: false,
},
});

await newTodo.save();

setTodos([...todos, newTodo.subject]);
setInputValue('');
};

return (
<div>
<ul>
{todos.map(subject => (
<li key={subject}>
<Todo subject={subject} onDelete={removeTodo} />
</li>
))}
</ul>
<input
type='text'
placeholder='Add a new todo...'
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
<button onClick={addTodo}>Add</button>
</div>
);
};

interface TodoProps {
subject: string;
onDelete: (subject: string) => void;
}

const Todo = ({ subject, onDelete }: TodoProps) => {
const resource = useResource<Todo>(subject);
const [done, setDone] = useBoolean(resource, todoApp.properties.done, {
commit: true,
});

const deleteTodo = () => {
resource.destroy();
onDelete(subject);
};

return (
<span>
<input
type='checkbox'
checked={done}
onChange={e => setDone(e.target.checked)}
/>
{resource.title}
<button onClick={deleteTodo}>Delete</button>
</span>
);
};
```

40 changes: 40 additions & 0 deletions docs/src/react/useCanWrite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# useCanWrite

`useCanWrite` is a hook that can be used to check if an agent has write access to a certain resource.

Normally you would just use `await resource.canWrite()` but since this is an async function, using it in react can be annoying.

The `useCanWrite` hook works practically the same as the `canWrite` method on `Resource`.

```jsx
import { useCanWrite, useResource, useString } from '@tomic/react';

const ResourceDescription = () => {
const resource = useResource('https://my-server.com/my-resource');
const [description, setDescription] = useString(resource, core.properties.description);
const [canWrite] = useCanWrite(resource);

if (canWrite) {
return (
<textarea onChange={e => setDescription(e.target.value)}>{description}</textarea>
<button onClick={() => resource.save()}>Save</button>
)
}

return <p>{description}</p>;
};
```

## Reference

### Parameters

- `resource: Resource` - The resource to check write access for.
- `agent?: Agent` - Optional different agent to check write access for. Defaults to the current agent.

### Returns

Returns a tuple with the following fields:

- `canWrite: boolean` - Whether the agent can write to the resource.
- `msg: string` - An error message if the agent cannot write to the resource.
149 changes: 149 additions & 0 deletions docs/src/react/useCollection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# useCollection

The useCollection hook is used to fetch a [Collection](../js-lib/collection.md).
It returns the collection together with a function to invalidate and re-fetch it.

```typescript
// Create a collection of all agents on the drive.
const { collection ,invalidateCollection } = useCollection({
property: core.properties.isA,
value: core.classes.Agent
});
```

## Reference

### Parameters

- **query**: QueryFilter - The query used to build the collection
- **pageSize**: number - The max number of items per page

### Returns

Returns an object containing the following items:

- **collection**: [Collection](../js-lib/collection.md) - The collection.
- **invalidateCollection**: `function` - A function to invalidate and re-fetch the collection.

## QueryFilter

A QueryFilter is an object with the following properties:

| Name | Type | Description |
| --- | --- | --- |
| property | `string` | The subject of the property you want to filter by. |
| value | `string` | The value of the property you want to filter by. |
| sort_by | `string` | The subject of the property you want to sort by. By default collections are sorted by subject |
| sort_desc | `boolean` | If true, the collection will be sorted in descending order. (Default: false) |

## Additional Hooks

Working with collections in React can be a bit tedious because most methods of `Collection` are asynchronous.
Luckily, we made some extra hooks to help with the most common patterns.

### useCollectionPage

The `useCollectionPage` hook makes it easy to create paginated views. It takes a collection and a page number and returns the items on that page.

```jsx
import {
core,
useCollection,
useCollectionPage,
useResource,
} from '@tomic/react';
import { useState } from 'react';

interface PaginatedChildrenProps {
subject: string;
}

export const PaginatedChildren = ({ subject }: PaginatedChildrenProps) => {
const [currentPage, setCurrentPage] = useState(0);

const { collection } = useCollection({
property: core.properties.parent,
value: subject,
});

const items = useCollectionPage(collection, currentPage);

return (
<div>
<button onClick={() => setCurrentPage(p => Math.max(0, p - 1))}>
Prev
</button>
<button
onClick={() =>
setCurrentPage(p => Math.min(p + 1, collection.totalPages - 1))
}
>
Next
</button>
{items.map(item => (
<Item key={item} subject={item} />
))}
</div>
);
};

const Item = ({ subject }: { subject: string }) => {
const resource = useResource(subject);

return <div>{resource.title}</div>;
};
```

### UseMemberOfCollection

Building virtualized lists is always difficult when working with unfamiliar data structures, especially when the data is paginated.
The `UseMemberOfCollection` hook makes it easy.

It takes a collection and index and returns the resource at that index.

In this example, we use the [`react-window`](https://github.com/bvaughn/react-window?tab=readme-ov-file) library to render a virtualized list of comments.

```jsx
import { useCallback } from 'react';
import { FixedSizeList } from 'react-window';
import Autosizer from 'react-virtualized-auto-sizer';
import { useCollection, useMemberOfCollection } from '@tomic/react';
import { myOntology, type Comment } from './ontologies/myOntology';

const ListView = () => {
// We create a collection of all comments.
const { collection } = useCollection({
property: core.properties.isA,
value: myOntology.classes.comment,
});

// We have to define the CommentComponent inside the ListView component because it needs access to the collection.
// Normally you'd pass it as a prop but that is not possible due to how react-window works.
const CommentComp = useCallback(({index}: {index: number}) => {
// Get the resource at the specified index.
const comment = useMemberOfCollection<Comment>(collection, index);

return (
<div>
<UserInline subject={comment.props.writtenBy}>
<p>{comment.props.description}</p>
</div>
);
}, [collection]);

return (
<Autosizer>
{({width, height}) => (
<FixedSizeList
height={height}
itemCount={collection.totalMembers}
itemSize={50}
width={width}
>
{CommentComp}
</FixedSizeList>
)}
</Autosizer>
);
}
```
23 changes: 23 additions & 0 deletions docs/src/react/useCurrentAgent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# useCurrentAgent

`useCurrentAgent` is a convenient hook that returns the current agent set in the store.
It also allows you to change the agent.

It also updates whenever the agent changes.

```ts
const [agent, setAgent] = useCurrentAgent();
```

## Reference

### Parameters

none

### Returns

Returns a tuple with the following fields:

- `agent: Agent` - The current agent set on the store.
- `setAgent: (agent: Agent) => void` - A function to set the current agent on the store.
Loading

0 comments on commit 8f7f2ce

Please sign in to comment.