Skip to content

feat(transact): support nested JSON txs #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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