Skip to content

Commit 86a4ac2

Browse files
authored
feat(transact): support nested JSON txs (#48)
1 parent b391ab5 commit 86a4ac2

11 files changed

Lines changed: 543 additions & 68 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ node_modules/
22
public/js
33
out/
44
dist/
5+
.clj-kondo/
6+
.lsp/
7+
.history/
58

69
/target
710
/checkouts

README.md

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const config = {
6464
// and lets you lookup entities by their unique attributes.
6565
schema: {
6666
todo: {
67-
project: { type: 'ref' },
67+
project: { type: 'ref', cardinality: 'one' },
6868
name: { unique: 'identity' }
6969
}
7070
},
@@ -73,10 +73,25 @@ const config = {
7373
// It's a transaction that runs on component mount.
7474
// Use it to hydrate your app.
7575
initialData: [
76-
{ project: { id: -1, name: 'Do it', owner: -2 } },
76+
{ project: { id: -1, name: 'Do it', user: -2 } },
7777
{ todo: { project: -1, name: 'Make it' } },
7878
{ user: { id: -2, name: 'Arpegius' } }
7979
]
80+
81+
// Or relationships can be specified implicitly with nested JSON
82+
initialData: [
83+
{
84+
todo: {
85+
name: 'Make it',
86+
project: {
87+
name: 'Do it',
88+
user: {
89+
name: 'Arpegius'
90+
}
91+
}
92+
}
93+
}
94+
]
8095
}
8196

8297
const RootComponent = () => (
@@ -101,7 +116,7 @@ const [sameTodo] = useEntity({ todo: { name: 'Make it' } })
101116
sameTodo.get('id') // => 2
102117

103118
// And most importantly you can traverse arbitrarily deep relationships.
104-
sameTodo.get('project', 'owner', 'name') // => 'Arpegius'
119+
sameTodo.get('project', 'user', 'name') // => 'Arpegius'
105120
```
106121

107122
### `useTransact`
@@ -170,7 +185,72 @@ This hook returns the current database client with some helpful functions for sy
170185

171186
Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend.
172187

188+
### Arrays & Nested JSON
189+
190+
Arrays and arbitrary JSON are partially supported for convenience. However in most cases its better to avoid arrays. Using a query and then sorting by an attribute is simpler and more flexible. This is because arrays add extra overhead to keep track of order.
191+
192+
```js
193+
const config = {
194+
schema: {
195+
company: {
196+
numbers: { type: 'ref', cardinality: 'many' },
197+
projects: { type: 'ref', cardinality: 'many' },
198+
}
199+
}
200+
}
201+
202+
transact([
203+
{ project: { id: -1, name: 'a' } },
204+
{
205+
company: {
206+
numbers: [1, 2, 3],
207+
projects: [
208+
{ project: { id: -1 } },
209+
{ project: { name: 'b' } },
210+
]
211+
}
212+
}
213+
])
173214

215+
// Index into arrays
216+
company.get('numbers', 1, 'value') // => 2
217+
company.get('projects', 0, 'ref', 'name') // => 'a'
218+
// Get the automatically assigned order
219+
// Order starts at 1 and increments by 1
220+
company.get('numbers', 0, 'order') // => 1
221+
company.get('projects', 0, 'order') // => 1
222+
company.get('projects', 1, 'order') // => 2
223+
// Map over individual attributes
224+
company.get('numbers', 'value') // => [1, 2, 3]
225+
company.get('projects', 'ref', 'name') // => ['a', 'b']
226+
```
227+
228+
The `entity.get` API is flexible and supports indexing into arrays as well as automatically mapping over individual attributes.
229+
230+
Array items are automatically assigned an `order` and either a `value` or a `ref` depending on if item in the array is an entity or not. To reorder an array item change its `order`.
231+
232+
```js
233+
transact([
234+
{
235+
id: company.get('numbers', 2, 'id'),
236+
order: (company.get('numbers', 0, 'order')
237+
+ company.get('numbers', 1, 'order')) / 2
238+
}
239+
])
240+
241+
company.get('numbers', 'value') // => [1 3 2]
242+
```
243+
244+
If you need to transact complex JSON like arrays of arrays then you're better off serializing it to a string first.
245+
246+
```js
247+
// NOT supported
248+
transact([{ company: { matrix: [[1, 2, 3], [4, 5, 6]] } }])
249+
250+
// Better
251+
transact([{ company: { matrix: JSON.stringify([[1, 2, 3], [4, 5, 6]]) } }])
252+
JSON.parse(company.get('matrix'))
253+
```
174254

175255
## Performance
176256

js/array-example.jsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react'
2+
const { HomebaseProvider, useTransact, useEntity } = window.homebase.react
3+
4+
const config = {
5+
schema: {
6+
store: {
7+
items: { type: 'ref', cardinality: 'many' }
8+
},
9+
item: {
10+
date: { type: 'ref', cardinality: 'one' }
11+
}
12+
},
13+
initialData: [{
14+
store: {
15+
identity: 'store 1',
16+
items: [
17+
{ item: { name: 'item 1' } },
18+
{ item: { name: 'item 2' } },
19+
{ item: { name: 'item 3' } },
20+
{ item: { name: 'item 4' } },
21+
{ item: { name: 'item 5', date: { year: 2021, month: 1, day: 3 } } },
22+
]
23+
}
24+
}]
25+
}
26+
27+
export const App = () => (
28+
<HomebaseProvider config={config}>
29+
<Items/>
30+
</HomebaseProvider>
31+
)
32+
33+
const Items = () => {
34+
const [store] = useEntity({ identity: 'store 1' })
35+
const [transact] = useTransact()
36+
37+
let newI = null
38+
const onDragOver = React.useCallback(e => {
39+
e.preventDefault()
40+
newI = parseInt(e.target.dataset.index)
41+
})
42+
43+
const reorder = React.useCallback((id, orderMin, orderMax) => {
44+
const order = (orderMin + orderMax) / 2.0
45+
transact([{'homebase.array': {id, order}}])
46+
}, [transact])
47+
48+
return (
49+
<div>
50+
{store.get('items').map((item, i) => (
51+
<div
52+
key={item.get('ref', 'id')}
53+
style={{ cursor: 'move' }}
54+
data-index={i}
55+
draggable
56+
onDragOver={onDragOver}
57+
onDragEnd={e => reorder(
58+
item.get('id'),
59+
newI > 0 && store.get('items', newI - 1, 'order') || 0,
60+
store.get('items', newI, 'order'),
61+
)}
62+
>
63+
{item.get('ref', 'name')} &nbsp;
64+
<small>{item.get('ref', 'date', 'year')}</small>
65+
</div>
66+
))}
67+
</div>
68+
)
69+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A graph database for React.",
44
"version": "0.0.0-development",
55
"license": "MIT",
6-
"homepage": "https://github.com/homebaseio/homebase-react",
6+
"homepage": "https://homebase.io",
77
"main": "./dist/js/homebase.react.js",
88
"private": false,
99
"scripts": {

shadow-cljs.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[[devcards "0.2.7"]
77
[datascript "1.0.1"]
88
[reagent "1.0.0-alpha2"]
9+
[inflections "0.13.2"]
910
[camel-snake-kebab "0.4.2"]]
1011

1112
:dev-http {3000 "public"}

src/example/array.cljs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
(ns example.array
2+
(:require
3+
[devcards.core :as dc]
4+
[homebase.react]
5+
["../js_gen/array-example" :as react-example])
6+
(:require-macros
7+
[devcards.core :refer [defcard-rg defcard-doc]]
8+
[dev.macros :refer [inline-resource]]))
9+
10+
(defcard-rg array-example
11+
[react-example/App])
12+
13+
(def code-snippet
14+
(clojure.string/replace-first
15+
(inline-resource "js/array-example.jsx")
16+
"const { HomebaseProvider, useTransact, useEntity } = window.homebase.react"
17+
"import { HomebaseProvider, useTransact, useEntity } from 'homebase-react'"))
18+
(defcard-doc
19+
"[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/js/array-example.jsx)"
20+
(str "```javascript\n" code-snippet "\n```"))
21+

src/example/core.cljs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[cljsjs.react.dom]
77
[reagent.core]
88
[devcards.core :as dc]
9+
[example.array]
910
[example.counter]
1011
[example.todo]
1112
[example.todo-firebase]))

0 commit comments

Comments
 (0)