Skip to content

Commit

Permalink
feat(transact): support nested JSON txs (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
becomingbabyman authored Jan 13, 2021
1 parent b391ab5 commit 86a4ac2
Show file tree
Hide file tree
Showing 11 changed files with 543 additions and 68 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ node_modules/
public/js
out/
dist/
.clj-kondo/
.lsp/
.history/

/target
/checkouts
Expand Down
86 changes: 83 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const config = {
// and lets you lookup entities by their unique attributes.
schema: {
todo: {
project: { type: 'ref' },
project: { type: 'ref', cardinality: 'one' },
name: { unique: 'identity' }
}
},
Expand All @@ -73,10 +73,25 @@ const config = {
// It's a transaction that runs on component mount.
// Use it to hydrate your app.
initialData: [
{ project: { id: -1, name: 'Do it', owner: -2 } },
{ project: { id: -1, name: 'Do it', user: -2 } },
{ todo: { project: -1, name: 'Make it' } },
{ user: { id: -2, name: 'Arpegius' } }
]

// Or relationships can be specified implicitly with nested JSON
initialData: [
{
todo: {
name: 'Make it',
project: {
name: 'Do it',
user: {
name: 'Arpegius'
}
}
}
}
]
}

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

// And most importantly you can traverse arbitrarily deep relationships.
sameTodo.get('project', 'owner', 'name') // => 'Arpegius'
sameTodo.get('project', 'user', 'name') // => 'Arpegius'
```

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

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

### Arrays & Nested JSON

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.

```js
const config = {
schema: {
company: {
numbers: { type: 'ref', cardinality: 'many' },
projects: { type: 'ref', cardinality: 'many' },
}
}
}

transact([
{ project: { id: -1, name: 'a' } },
{
company: {
numbers: [1, 2, 3],
projects: [
{ project: { id: -1 } },
{ project: { name: 'b' } },
]
}
}
])

// Index into arrays
company.get('numbers', 1, 'value') // => 2
company.get('projects', 0, 'ref', 'name') // => 'a'
// Get the automatically assigned order
// Order starts at 1 and increments by 1
company.get('numbers', 0, 'order') // => 1
company.get('projects', 0, 'order') // => 1
company.get('projects', 1, 'order') // => 2
// Map over individual attributes
company.get('numbers', 'value') // => [1, 2, 3]
company.get('projects', 'ref', 'name') // => ['a', 'b']
```

The `entity.get` API is flexible and supports indexing into arrays as well as automatically mapping over individual attributes.

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`.

```js
transact([
{
id: company.get('numbers', 2, 'id'),
order: (company.get('numbers', 0, 'order')
+ company.get('numbers', 1, 'order')) / 2
}
])

company.get('numbers', 'value') // => [1 3 2]
```

If you need to transact complex JSON like arrays of arrays then you're better off serializing it to a string first.

```js
// NOT supported
transact([{ company: { matrix: [[1, 2, 3], [4, 5, 6]] } }])

// Better
transact([{ company: { matrix: JSON.stringify([[1, 2, 3], [4, 5, 6]]) } }])
JSON.parse(company.get('matrix'))
```

## Performance

Expand Down
69 changes: 69 additions & 0 deletions js/array-example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'
const { HomebaseProvider, useTransact, useEntity } = window.homebase.react

const config = {
schema: {
store: {
items: { type: 'ref', cardinality: 'many' }
},
item: {
date: { type: 'ref', cardinality: 'one' }
}
},
initialData: [{
store: {
identity: 'store 1',
items: [
{ item: { name: 'item 1' } },
{ item: { name: 'item 2' } },
{ item: { name: 'item 3' } },
{ item: { name: 'item 4' } },
{ item: { name: 'item 5', date: { year: 2021, month: 1, day: 3 } } },
]
}
}]
}

export const App = () => (
<HomebaseProvider config={config}>
<Items/>
</HomebaseProvider>
)

const Items = () => {
const [store] = useEntity({ identity: 'store 1' })
const [transact] = useTransact()

let newI = null
const onDragOver = React.useCallback(e => {
e.preventDefault()
newI = parseInt(e.target.dataset.index)
})

const reorder = React.useCallback((id, orderMin, orderMax) => {
const order = (orderMin + orderMax) / 2.0
transact([{'homebase.array': {id, order}}])
}, [transact])

return (
<div>
{store.get('items').map((item, i) => (
<div
key={item.get('ref', 'id')}
style={{ cursor: 'move' }}
data-index={i}
draggable
onDragOver={onDragOver}
onDragEnd={e => reorder(
item.get('id'),
newI > 0 && store.get('items', newI - 1, 'order') || 0,
store.get('items', newI, 'order'),
)}
>
{item.get('ref', 'name')} &nbsp;
<small>{item.get('ref', 'date', 'year')}</small>
</div>
))}
</div>
)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "A graph database for React.",
"version": "0.0.0-development",
"license": "MIT",
"homepage": "https://github.com/homebaseio/homebase-react",
"homepage": "https://homebase.io",
"main": "./dist/js/homebase.react.js",
"private": false,
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[[devcards "0.2.7"]
[datascript "1.0.1"]
[reagent "1.0.0-alpha2"]
[inflections "0.13.2"]
[camel-snake-kebab "0.4.2"]]

:dev-http {3000 "public"}
Expand Down
21 changes: 21 additions & 0 deletions src/example/array.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
(ns example.array
(:require
[devcards.core :as dc]
[homebase.react]
["../js_gen/array-example" :as react-example])
(:require-macros
[devcards.core :refer [defcard-rg defcard-doc]]
[dev.macros :refer [inline-resource]]))

(defcard-rg array-example
[react-example/App])

(def code-snippet
(clojure.string/replace-first
(inline-resource "js/array-example.jsx")
"const { HomebaseProvider, useTransact, useEntity } = window.homebase.react"
"import { HomebaseProvider, useTransact, useEntity } from 'homebase-react'"))
(defcard-doc
"[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/js/array-example.jsx)"
(str "```javascript\n" code-snippet "\n```"))

1 change: 1 addition & 0 deletions src/example/core.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
[cljsjs.react.dom]
[reagent.core]
[devcards.core :as dc]
[example.array]
[example.counter]
[example.todo]
[example.todo-firebase]))
Expand Down
Loading

0 comments on commit 86a4ac2

Please sign in to comment.