├── .babelrc ├── .eslintrc.json ├── .github ├── pull_request_template.md └── workflows │ ├── cd.yml │ ├── ci.yml │ ├── gh-branch-pages.yml │ └── gh-pages.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── prepare-commit-msg ├── .npmignore ├── .prettierrc ├── .releaserc.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── deps.edn ├── doc ├── README.md ├── cljdoc.edn ├── examples.md ├── performance.md └── tooling_debugging.md ├── docs ├── 0100|Overview.md ├── 0200|Quick_Start.md ├── 0300|Tutorial.md ├── 0350|---.md ├── 0400|API.md ├── 0650|---.md ├── 0675|Debugging.md ├── 0700|Performance.md ├── 0800|Examples.md └── 0900|Recipes.md ├── examples ├── README.md ├── counter │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ └── index.js │ └── yarn.lock ├── reagent │ ├── counter │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ └── index.html │ │ ├── shadow-cljs.edn │ │ ├── src │ │ │ └── main │ │ │ │ └── counter.cljs │ │ └── yarn.lock │ └── todo │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ └── index.html │ │ ├── shadow-cljs.edn │ │ ├── src │ │ └── main │ │ │ └── todo.cljs │ │ └── yarn.lock ├── roam │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── craco.config.js │ ├── package.json │ ├── public │ │ ├── asset-manifest.json │ │ ├── edn │ │ │ └── hn.edn │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ └── convert_roam_edn │ │ │ ├── README.md │ │ │ ├── convert.clj │ │ │ └── datasets │ │ │ └── hn.edn │ ├── src │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── Block.js │ │ │ ├── CodeBlock.js │ │ │ ├── CodeBlockStyle.js │ │ │ ├── RoamMarkdown.js │ │ │ └── ScrollToTop.js │ │ ├── index.css │ │ ├── index.js │ │ ├── pages │ │ │ ├── Blocks.js │ │ │ └── PageUid.js │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ ├── tailwind.config.js │ └── yarn.lock ├── todo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ └── index.js │ └── yarn.lock └── typescript-firebase-todo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.tsx │ └── index.tsx │ ├── tsconfig.json │ └── yarn.lock ├── jest.config.js ├── package.json ├── pom.xml ├── public ├── css │ └── firebaseui.css ├── images │ ├── datalog_console.png │ ├── enable_chrome_formatters_1.png │ ├── enable_chrome_formatters_2.png │ ├── enable_chrome_formatters_3.png │ └── logo-blk.png └── index.html ├── shadow-cljs.edn ├── src ├── dev │ └── homebase │ │ └── dev │ │ ├── example │ │ ├── core.cljs │ │ ├── js │ │ │ ├── array.jsx │ │ │ ├── counter.jsx │ │ │ ├── todo-firebase.jsx │ │ │ └── todo.jsx │ │ ├── js_compiled │ │ │ ├── array.js │ │ │ ├── counter.js │ │ │ ├── todo-firebase.js │ │ │ └── todo.js │ │ ├── react │ │ │ ├── array.cljs │ │ │ ├── counter.cljs │ │ │ ├── todo.cljs │ │ │ └── todo_firebase.cljs │ │ ├── reagent.cljs │ │ └── reagent │ │ │ ├── counter.cljs │ │ │ └── todo.cljs │ │ └── macros.clj ├── main │ └── homebase │ │ ├── cache.cljs │ │ ├── js.cljs │ │ ├── react.cljs │ │ ├── reagent.cljs │ │ └── util.cljs └── test │ └── homebase │ ├── benchmarks.test.js │ ├── js_test.cljs │ ├── react.test.js │ ├── reagent_test.cljs │ └── test_polyfills.cljs ├── types ├── index.d.ts └── index.test-d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-modules-commonjs" 7 | ] 8 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "root": true, 4 | "parser": "babel-eslint", 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:jsx-a11y/recommended", 11 | "plugin:react-hooks/recommended", 12 | "airbnb", 13 | "prettier", 14 | "plugin:jest/all", 15 | "plugin:jest-dom/recommended" 16 | ], 17 | "env": { 18 | "browser": true, 19 | "commonjs": true, 20 | "es6": true, 21 | "node": true 22 | }, 23 | "parserOptions": { 24 | "ecmaVersion": 2018, 25 | "sourceType": "module", 26 | "ecmaFeatures": { 27 | "jsx": true 28 | } 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | } 34 | }, 35 | "rules": { 36 | "semi": 0, 37 | "react/jsx-filename-extension": 0, 38 | "react/react-in-jsx-scope": 0, 39 | "react/prop-types": 0, 40 | "implicit-arrow-linebreak": 0, 41 | "object-curly-newline": 0, 42 | "react/no-unescaped-entities": 0, 43 | "react/jsx-one-expression-per-line": 0, 44 | "prettier/prettier": ["error"], 45 | "jsx-a11y/anchor-is-valid": 0 46 | }, 47 | "plugins": ["prettier"] 48 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## PR Description 2 | 3 | 4 | ## PR Checklist 5 | 6 | ### Testing 7 | - [ ] added relevant test coverage 8 | - [ ] no tests needed 9 | 10 | ### Docs 11 | - [ ] added relevant docs 12 | - preview them at https://homebase.io/docs/homebase-react/{BRANCH_NAME}/overview 13 | - [ ] updated relevant sections in the README.md 14 | - [ ] updated relevant docstrings in index.d.ts 15 | - [ ] no docs needed 16 | 17 | ### Typescript 18 | - [ ] added or edited relevant Typescript type declarations 19 | - [ ] no type declaration updates needed 20 | 21 | ## Merging 22 | For maintainers. 23 | 24 | To merge, select "Squash and Merge". Then: 25 | 1. Make sure the top commit message follows [Angular Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines), 26 | 2. Delete all other commit messages in the description, but keep any lines designating [co-authors](https://docs.github.com/en/free-pro-team@latest/github/committing-changes-to-your-project/creating-a-commit-with-multiple-authors) so contributors will retain credit for their contributions. 27 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | if: "!contains(github.event.head_commit.message, 'skip cd')" 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '12' 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: 1.8 19 | - uses: DeLaGuardo/setup-clojure@3.2 20 | with: 21 | cli: 1.10.1.693 22 | - run: yarn install --frozen-lockfile 23 | - run: yarn test 24 | - run: yarn semantic-release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | strategy: 9 | matrix: 10 | os: [ubuntu-18.04] 11 | fail-fast: false 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '12' 19 | 20 | - uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | 24 | - uses: DeLaGuardo/setup-clojure@3.2 25 | with: 26 | cli: 1.10.1.693 27 | 28 | - name: Get yarn cache directory path 29 | id: yarn-cache-dir-path 30 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 31 | 32 | - name: Cache yarn packages 33 | uses: actions/cache@v2 34 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 35 | with: 36 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 37 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-yarn- 40 | 41 | - name: Cache maven packages 42 | uses: actions/cache@v2 43 | with: 44 | path: ~/.m2 45 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 46 | restore-keys: ${{ runner.os }}-m2 47 | 48 | - name: Cache shadow-cljs 49 | uses: actions/cache@v2 50 | with: 51 | path: .shadow-cljs 52 | key: ${{ runner.os }}-shadow-cljs-${{ hashFiles('**/pom.xml') }} 53 | restore-keys: ${{ runner.os }}-shadow-cljs 54 | 55 | - run: yarn install --frozen-lockfile 56 | 57 | - run: yarn shadow-cljs release npm 58 | - run: yarn bundle-ts 59 | - run: yarn shadow-cljs compile test 60 | 61 | - run: node out/node-tests.js 62 | - run: yarn jest src/* 63 | - run: yarn tsd 64 | -------------------------------------------------------------------------------- /.github/workflows/gh-branch-pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish Branch Examples 2 | 3 | on: push 4 | 5 | jobs: 6 | publish-examples: 7 | name: Publish Examples 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '12' 15 | 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: 1.8 19 | 20 | - uses: DeLaGuardo/setup-clojure@3.2 21 | with: 22 | cli: 1.10.1.693 23 | 24 | - name: Get yarn cache directory path 25 | id: yarn-cache-dir-path 26 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 27 | 28 | - name: Cache yarn packages 29 | uses: actions/cache@v2 30 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 31 | with: 32 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 33 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-yarn- 36 | 37 | - name: Cache maven packages 38 | uses: actions/cache@v2 39 | with: 40 | path: ~/.m2 41 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 42 | restore-keys: ${{ runner.os }}-m2 43 | 44 | - name: Cache shadow-cljs 45 | uses: actions/cache@v2 46 | with: 47 | path: .shadow-cljs 48 | key: ${{ runner.os }}-shadow-cljs-${{ hashFiles('**/pom.xml') }} 49 | restore-keys: ${{ runner.os }}-shadow-cljs 50 | 51 | - run: yarn install --frozen-lockfile 52 | 53 | - run: yarn shadow-cljs release dev 54 | 55 | - name: Extract branch name 56 | shell: bash 57 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 58 | id: extract_branch 59 | 60 | - name: Publish to GitHub Pages 🚀 61 | uses: JamesIves/github-pages-deploy-action@4.1.0 62 | with: 63 | branch: gh-pages 64 | folder: public 65 | target-folder: branches/${{ steps.extract_branch.outputs.branch }} 66 | 67 | - name: Slack Notification 68 | uses: rtCamp/action-slack-notify@v2 69 | env: 70 | SLACK_CHANNEL: proj-dev-homebase-react 71 | SLACK_COLOR: ${{ job.status }} # or a specific color like 'green' or '#ff00ff' 72 | SLACK_ICON: https://github.com/homebaseio.png?size=200 73 | SLACK_MESSAGE: "- :github: Branch: \n- :card_file_box: devcards: https://homebaseio.github.io/homebase-react/branches/${{ steps.extract_branch.outputs.branch }}/index.html" 74 | SLACK_TITLE: "Published ${{ steps.extract_branch.outputs.branch }} to GitHub Pages :rocket:" 75 | SLACK_USERNAME: Homebase 76 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 77 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish Examples 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | publish-examples: 9 | name: Publish Examples 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: '12' 17 | 18 | - uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | 22 | - uses: DeLaGuardo/setup-clojure@3.2 23 | with: 24 | cli: 1.10.1.693 25 | 26 | - name: Get yarn cache directory path 27 | id: yarn-cache-dir-path 28 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)" 29 | 30 | - name: Cache yarn packages 31 | uses: actions/cache@v2 32 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Cache maven packages 40 | uses: actions/cache@v2 41 | with: 42 | path: ~/.m2 43 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 44 | restore-keys: ${{ runner.os }}-m2 45 | 46 | - name: Cache shadow-cljs 47 | uses: actions/cache@v2 48 | with: 49 | path: .shadow-cljs 50 | key: ${{ runner.os }}-shadow-cljs-${{ hashFiles('**/pom.xml') }} 51 | restore-keys: ${{ runner.os }}-shadow-cljs 52 | 53 | - run: yarn install --frozen-lockfile 54 | 55 | - run: yarn shadow-cljs release dev 56 | 57 | - name: Publish to GitHub Pages 🚀 58 | uses: JamesIves/github-pages-deploy-action@4.1.0 59 | with: 60 | branch: gh-pages 61 | folder: public 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | out/ 4 | dist/ 5 | .clj-kondo/ 6 | .lsp/ 7 | .history/ 8 | 9 | /target 10 | /checkouts 11 | /src/gen 12 | /src/js_gen/tests 13 | 14 | package-lock.json 15 | report.html 16 | pom.xml.asc 17 | *.iml 18 | *.jar 19 | *.log 20 | *.orig 21 | .shadow-cljs 22 | .idea 23 | .lein-* 24 | .nrepl-* 25 | .DS_Store 26 | .calva 27 | 28 | .hgignore 29 | .hg/ 30 | .vscode 31 | .cpcache -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | yarn commitlint -e "$(git rev-parse --git-dir)/COMMIT_EDITMSG" -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname $0)/_/husky.sh" 3 | 4 | NOCOLOR='\033[0m' 5 | PURPLE='\033[1;35m' 6 | exec < /dev/tty && yarn git-cz --hook || 7 | echo "${PURPLE}FIX: Try upgrading git, --absolute-git-dir was introduced in v2.13${NOCOLOR}" && true -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .husky/ 3 | .releaserc.json 4 | commitlint.config.js 5 | 6 | node_modules/ 7 | public/js 8 | out/ 9 | 10 | /target 11 | /checkouts 12 | /src/gen 13 | /src/js_gen/tests 14 | 15 | package-lock.json 16 | report.html 17 | pom.xml 18 | pom.xml.asc 19 | *.iml 20 | *.jar 21 | *.log 22 | *.orig 23 | .shadow-cljs 24 | .idea 25 | .lein-* 26 | .nrepl-* 27 | .DS_Store 28 | .calva 29 | 30 | .hgignore 31 | .hg/ 32 | 33 | examples/ 34 | types/ 35 | src/ 36 | public/ 37 | js/ 38 | docs/ 39 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 100 8 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/changelog", 6 | "@semantic-release/npm", 7 | "@semantic-release/github" 8 | ] 9 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | ## Developing homebase-react 4 | 5 | If you consider contributing changes to homebase-react – thank you! 6 | Please review these guidelines when filing a pull request: 7 | 8 | - Commits follow the [Angular commit convention](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) 9 | - 2 spaces indentation 10 | - Features and bug fixes should be covered by test cases 11 | 12 | ## Creating releases 13 | 14 | homebase-react uses [semantic-release](https://github.com/semantic-release/semantic-release) 15 | to release new versions automatically on merge to master. 16 | 17 | - Commits of type `fix` will trigger bugfix releases, think `0.0.1` 18 | - Commits of type `feat` will trigger feature releases, think `0.1.0` 19 | - Commits with `BREAKING CHANGE` in body or footer will trigger breaking releases, think `1.0.0` 20 | 21 | All other commit types will trigger no new release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Caboodle Information Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Homebase React 4 | 5 | [![CI](https://github.com/homebaseio/homebase-react/workflows/CI/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACI) 6 | [![CD](https://github.com/homebaseio/homebase-react/workflows/CD/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACD) 7 | [![License](https://img.shields.io/github/license/homebaseio/homebase-react.svg)](LICENSE) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/homebase__io?label=Follow&style=social)](https://twitter.com/homebase__io) 9 | 10 | *The React state management library for write-heavy applications* 11 | 12 | Supported languages, frameworks and DBs: 13 | 14 | - JS + React + Datascript ([jump](#javascript--react)) 15 | - CLJS + Reagent + Datascript ([jump](#clojurescript--reagent)) 16 | - *Datahike support coming soon* 17 | 18 | ## What and Why 19 | 20 | As data and our need to annotate and organize it grows, so does our need for supporting state in *write-heavy* applications. 21 | 22 | To solve this problem, modern write-heavy applications such as Superhuman, Roam Research, and Facebook Messenger built their own embedded data layers to enable these more sophisticated user experiences. 23 | 24 | Homebase-react enables developers to access the same embedded datalog database as Roam Research through React hooks. You no longer have to build out a team or learn specialized tools like Clojure in order to build a delightful write-heavy application. 25 | 26 | 27 | ## Testimonials 28 | > Homebase is executing on the vision of data usage, portability, and management we had when building Firebase. We never got there. I'm excited! 29 | > 30 | > —James Tamplin, Founder/CEO of Firebase 31 | 32 | > Datalog is the future of end-user programming, personal productivity software, p2p software, etc. 33 | > 34 | > —Chet Corcos, Founding Engineer of Notion 35 | 36 | # Javascript + React 37 | 38 | Start by installing `homebase-react`. 39 | 40 | [![NPM Version](https://img.shields.io/npm/v/homebase-react)](https://www.npmjs.com/package/homebase-react) 41 | [![Bundle Size](https://img.shields.io/bundlephobia/minzip/homebase-react)](https://www.npmjs.com/package/homebase-react) 42 | 43 | ```bash 44 | # NPM 45 | npm install homebase-react --save 46 | 47 | # Yarn 48 | yarn add homebase-react 49 | ``` 50 | 51 | Optionally install the `datalog-console` [chrome extension](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb?hl=en) to inspect the `homebase-react` DB in your browser. 52 | 53 | ⭐️ 📖 **[Read the JS docs](https://homebase.io/docs/homebase-react)** ⚛️ ⭐️ 54 | 55 | ```js 56 | import { useCallback } from 'react' 57 | import { HomebaseProvider, useEntity, useTransact } from 'homebase-react' 58 | 59 | const RootComponent = () => ( 60 | // Create a DB and set some starting data 61 | 62 | 63 | 64 | ) 65 | 66 | const App = () => { 67 | // Get entity id = 1 68 | const [counter] = useEntity(1) 69 | const [transact] = useTransact() 70 | 71 | return ( 72 | 81 | ) 82 | } 83 | ``` 84 | 85 | [Live demo](https://homebaseio.github.io/homebase-react/#!/homebase.dev.example.counter) 86 | 87 | # ClojureScript + Reagent 88 | 89 | Start by adding `homebase-react`. 90 | 91 | [![Clojars Project](https://img.shields.io/clojars/v/io.homebase/homebase-react.svg)](https://clojars.org/io.homebase/homebase-react) 92 | 93 | Optionally add `datalog-console` and its corresponding [chrome extension](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb?hl=en) to inspect the DB in your browser. 94 | 95 | [![Clojars Project](https://img.shields.io/clojars/v/io.homebase/datalog-console.svg)](https://clojars.org/io.homebase/datalog-console) 96 | 97 | ⭐️ 📖 **[Read the CLJS docs](https://cljdoc.org/d/io.homebase/homebase-react/CURRENT)** ƛ ⭐️ 98 | 99 | ```clojure 100 | (ns homebase.dev.example.reagent.counter 101 | (:require 102 | [datascript.core :as d] 103 | [homebase.reagent :as hbr] 104 | [datalog-console.integrations.datascript :as datalog-console])) 105 | 106 | (def db-conn (d/create-conn {})) 107 | (d/transact! db-conn [[:db/add 1 :count 0]]) ; Transact some starting data. 108 | (hbr/connect! db-conn) ; Connect homebase.reagent to the database. 109 | (datalog-console/enable! {:conn db-conn}) ; Also connect the datalog-console extension for better debugging. 110 | 111 | (defn counter [] 112 | (let [[entity] (hbr/entity db-conn 1)] ; Get a homebase.reagent/Entity. Note the use of db-conn and not @db-conn, this makes it reactive. 113 | (fn [] 114 | [:div 115 | "Count: " (:count @entity) ; Deref the entity just like a reagent/atom. 116 | [:div 117 | [:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @entity))]])} ; Use d/transact! just like normal. 118 | "Increment"]]]))) 119 | ``` 120 | 121 | [Live demo](https://homebaseio.github.io/homebase-react/index.html#!/homebase.dev.example.reagent) 122 | 123 | ## Roadmap 124 | 125 | 1. ~~Improve developer tools: custom chrome formatters, DB admin console extension~~ 126 | 2. ~~Rewrite React ↔ Homebase cache~~ 127 | 1. ~~Support async DB access (for Datahike)~~ 128 | 2. ~~Reactive query planning (better perf on pages with lots of live reads)~~ 129 | 3. Swap [Datascript](https://github.com/tonsky/datascript) out for [Datahike](https://github.com/replikativ/datahike) 130 | 1. Immutability 131 | 2. History / Change Tracking 132 | 4. Persist to IndexedDB 133 | 5. [Local-first](https://www.inkandswitch.com/local-first.html) conflict resolution for offline caching and sync between multiple devices 134 | 135 | ## Limitations 136 | 137 | Homebase React is currently not a good choice for read-heavy applications (e.g. Twitter, ecommerce). We plan to support these query patterns with our [platform](http://homebase.io) eventually. 138 | 139 | ## Alternatives 140 | 141 | There isn't much in the way of React friendly datalog DB based state management for Javascript, but there's at least one alternative if you're a Clojure dev. 142 | 143 | - If you prefer `d/pull` over `d/entity` take a look at [Posh](https://github.com/denistakeda/posh). It supports less of the `d/q` API, but provides more tools for tuning performance. 144 | 145 | ## Development 146 | 147 | ```bash 148 | yarn install 149 | yarn dev 150 | ``` 151 | 152 | Open http://localhost:3000 153 | 154 | ## Test 155 | 156 | ```bash 157 | yarn install 158 | yarn test 159 | ``` 160 | 161 | ## Contributing 162 | 163 | Welcome and thank you! Writing docs, patches and features are all incredibly helpful and appreciated. 164 | 165 | We only ask that you provide test coverage for code changes, and conform to our [commit guidelines](CONTRIBUTING.md). 166 | 167 | ## Authors 168 | 169 | - Chris Smothers ([@csmothers](https://twitter.com/csmothers)) – [Homebase](https://www.homebase.io/) 170 | - JB Rubinovitz ([@rubinovitz](https://twitter.com/rubinovitz)) – [Homebase](https://www.homebase.io/) 171 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps {datascript/datascript {:mvn/version "1.0.7"} 3 | reagent/reagent {:mvn/version "1.0.0-alpha2"} 4 | inflections/inflections {:mvn/version "0.13.2"} 5 | io.homebase/datalog-console {:mvn/version "0.2.2"} 6 | nano-id/nano-id {:mvn/version "1.0.0"} 7 | camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}} 8 | :aliases {:dev {:extra-paths ["src/dev" "src/test"] 9 | :extra-deps {thheller/shadow-cljs {:mvn/version "2.11.25"} 10 | devcards/devcards {:mvn/version "0.2.7"} 11 | binaryage/devtools {:mvn/version "1.0.2"}}} 12 | :jar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.0.216"}} 13 | :exec-fn hf.depstar/jar 14 | :exec-args {:jar "homebase-react.jar" :sync-pom true}} 15 | :install {:replace-deps {slipset/deps-deploy {:mvn/version "0.1.5"}} 16 | :exec-fn deps-deploy.deps-deploy/deploy 17 | :exec-args {:installer :local :artifact "homebase-react.jar"}} 18 | :deploy {:replace-deps {slipset/deps-deploy {:mvn/version "0.1.5"}} 19 | :exec-fn deps-deploy.deps-deploy/deploy 20 | :exec-args {:installer :remote :artifact "homebase-react.jar"}}}} 21 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Homebase React 2 | 3 | [![CI](https://github.com/homebaseio/homebase-react/workflows/CI/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACI) 4 | [![CD](https://github.com/homebaseio/homebase-react/workflows/CD/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACD) 5 | [![License](https://img.shields.io/github/license/homebaseio/homebase-react.svg)](LICENSE) 6 | [![GitHub Repo stars](https://img.shields.io/github/stars/homebaseio/homebase-react?style=social)](https://github.com/homebaseio/homebase-react) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/homebase__io?label=Follow&style=social)](https://twitter.com/homebase__io) 8 | 9 | > Use a datalog DB to manage react application state. 10 | 11 | Supported languages, frameworks and DBs: 12 | 13 | - JS + React + Datascript 14 | - CLJS + Reagent + Datascript 15 | - *Datahike support coming soon* 16 | 17 | # Javascript + React 18 | 19 | This is the Clojure docs site. Go here for [JS + React docs](https://homebase.io/docs/homebase-react). 20 | 21 | # ClojureScript + Reagent 22 | 23 | Start by adding `homebase-react`. 24 | 25 | [![Clojars Project](https://img.shields.io/clojars/v/io.homebase/homebase-react.svg)](https://clojars.org/io.homebase/homebase-react) 26 | 27 | Optionally add `datalog-console` and its corresponding [chrome extension](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb?hl=en) to inspect the DB in your browser. 28 | 29 | [![Clojars Project](https://img.shields.io/clojars/v/io.homebase/datalog-console.svg)](https://clojars.org/io.homebase/datalog-console) 30 | 31 | ## Quick Start 32 | 33 | ```clojure 34 | (ns homebase.dev.example.reagent.counter 35 | (:require 36 | [datascript.core :as d] 37 | [homebase.reagent :as hbr] 38 | [datalog-console.integrations.datascript :as datalog-console])) 39 | 40 | (def db-conn (d/create-conn {})) 41 | (d/transact! db-conn [[:db/add 1 :count 0]]) ; Transact some starting data. 42 | (hbr/connect! db-conn) ; Connect homebase.reagent to the database. 43 | (datalog-console/enable! {:conn db-conn}) ; Also connect the datalog-console extension for better debugging. 44 | 45 | (defn counter [] 46 | (let [[entity] (hbr/entity db-conn 1)] ; Get a homebase.reagent/Entity. Note the use of db-conn and not @db-conn, this makes it reactive. 47 | (fn [] 48 | [:div 49 | "Count: " (:count @entity) ; Deref the entity just like a reagent/atom. 50 | [:div 51 | [:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @entity))]])} ; Use d/transact! just like normal. 52 | "Increment"]]]))) 53 | ``` 54 | 55 | [Live demo](https://homebaseio.github.io/homebase-react/index.html#!/homebase.dev.example.reagent) 56 | 57 | ## API 58 | 59 | - [homebase.reagent](https://cljdoc.org/d/io.homebase/homebase-react/CURRENT/api/homebase.reagent) 60 | 61 | ## Performance 62 | 63 | Our reactive query functions like `hbr/entity` and `hbr/q` will only trigger re-renders when their results change. 64 | 65 | In the case of `hbr/entity` we track which attributes get consumed `(:attr @entity)` and only dispatch updates when those attributes are transacted. 66 | 67 | `hbr/q` queries rerun on every transaction. If the result is different we re-render. We're looking into differential datalog and incremental view maintenance, but for typical datasets of tens of thousands of datoms the current performance is great. DOM updates tend to be much more costly, so just rerunning the queries still performs well as long as we don't repaint the DOM. 68 | 69 | ## Alternatives 70 | 71 | - If you prefer `d/pull` over `d/entity` take a look at [Posh](https://github.com/denistakeda/posh). It supports less of the `d/q` API, but provides more tools for tuning performance. -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree 2 | [["Introduction" {:file "doc/README.md"}] 3 | ["Examples" {:file "doc/examples.md"}] 4 | ["Misc" {} 5 | ["Tooling & Debugging" {:file "doc/tooling_debugging.md"}] 6 | ["Performance" {:file "doc/performance.md"}] 7 | ["Contribution" {:file "CONTRIBUTING.md"}]]]} -------------------------------------------------------------------------------- /doc/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Visit the [live demos](https://homebaseio.github.io/homebase-react/index.html#!/homebase.dev.example.reagent) devcards site to see some examples. -------------------------------------------------------------------------------- /doc/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Homebase React tracks the attributes consumed in each component via `homebase.reagent/Entity` and scopes those attributes to their respective `hbr/entity` reagent atom. Re-renders are only triggered when an attribute changes. 4 | 5 | The default caching reduces unnecessary re-renders and virtual DOM thrashing a lot. That said, it is still possible to trigger more re-renders than you might want. 6 | 7 | ## Smart Prop Drilling 8 | 9 | One top level `hbr/entity` + prop drilling the entity it returns will cause all children to re-render on any change to the parent or their siblings. 10 | 11 | To fix this we recommend passing ids to children, not whole entities. Instead get the entity in the child with `(hbr/entity db-conn id)`. This creates a new scope for each child so they are not affected by changes in the state of the parent or sibling components. 12 | 13 | ### Good Prop Drilling 14 | 15 | ```clojure 16 | (defn friend [id] 17 | (let [[user] (hbr/entity db-conn id)] 18 | (fn [] 19 | [:div (:user/name @user)]))) 20 | 21 | (defn friends [user-id] 22 | (let [[user] (hbr/entity db-conn user-id)] 23 | (fn [user-id] 24 | [:div 25 | (for [u (:user/friends @user)] 26 | [friend (:db/id u)])]))) 27 | ``` 28 | 29 | ### Bad Prop Drilling 30 | 31 | ```clojure 32 | (defn friend [user] 33 | [:div (:user/name @user)]) 34 | 35 | (defn friends [user-id] 36 | (let [[user] (hbr/entity db-conn user-id)] 37 | (fn [user-id] 38 | [:div 39 | (for [u (:user/friends @user)] 40 | [friend u])]))) 41 | ``` 42 | 43 | ## Query performance 44 | 45 | `hbr/q` queries rerun on every transaction. If the result is different we re-render. We're looking into differential datalog and incremental view maintenance, but for typical datasets of tens of thousands of datoms the current performance is great. DOM updates tend to be much more costly, so rerunning the queries still performs well as long as we don't repaint the DOM. 46 | 47 | If you are seeing UI slowdowns consider virtualizing large lists and only rendering DOM nodes that fit on the screen. -------------------------------------------------------------------------------- /doc/tooling_debugging.md: -------------------------------------------------------------------------------- 1 | # Tooling & Debugging 2 | 3 | We've built a few tools to make debugging a bit more convenient. 4 | 5 | ## Custom chrome formatters 6 | If you develop with [Chrome](https://www.google.com/chrome/) or a Chromium browser like Brave or Edge you'll get significantly more meaningful logs for entities `(js/console.log an-entity)` due to our use of custom chrome :formatters. These custom formatters allow us to perform lazy database queries to fetch all of an entity's attributes, including references to other entities and all reverse references to the current entity. They let you access your entire data graph from the console, with any logged out entity as an entry point. 7 | 8 | **To enable custom chrome formatters** 9 | 10 | **1.** Add **[binaryage/cljs-devtools](https://github.com/binaryage/cljs-devtools)** to your app. Our entity formatters implement protocols defined in cljs-devtools and need cljs-devtools to work. Plus, if you've never used cljs-devtools you're in for a treat. 11 | 12 | **2.** Open the preferences panel in chrome devtools by clicking the cog. 13 | 14 | ![image of chrome devtools preferences button](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_1.png?raw=true) 15 | 16 | **3.** Toggle `Enabled custom formatters` on. 17 | 18 | ![image of chrome devtools custom formatters toggle](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_2.png?raw=true) 19 | 20 | **4.** Keep the chrome console open and refresh the page. Any logged out entities should now have the custom formatting. 21 | 22 | ![image of custom entity chrome console logs](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_3.png?raw=true) 23 | 24 | **Live demo:** open the console while on the [todo example](https://homebaseio.github.io/homebase-react/#!/homebase.dev.example.todo) page. 25 | 26 | **Remember**: for custom formatters to work `(js/console.log an-entity)` must be called *after* you open the chrome console. Anything logged out before you open the console will not have custom formatting applied because chrome processes those logs in the background. 27 | 28 | ## Datalog Console Extension 29 | 30 | We also integrate with the [Datalog Console](https://github.com/homebaseio/datalog-console) extension. 31 | 32 | ![image of datalog console extension](https://github.com/homebaseio/homebase-react/blob/master/public/images/datalog_console.png?raw=true) 33 | 34 | It's still in an early stage of development, but we seek to expose all common DB administration capabilities here and let you connect to any Datalog database that implements the console's interface. 35 | 36 | ### Using the Datalog Console 37 | 38 | 1. [Add the extension to Chrome](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb) 39 | 2. Vist a page built with homebase-react [like this one](https://cljdoc.org/d/io.homebase/homebase-react/CURRENT/api/homebase.reagent), open the inspector, click the `Datalog DB` tab, and click `Load database` to try it out 40 | -------------------------------------------------------------------------------- /docs/0100|Overview.md: -------------------------------------------------------------------------------- 1 | ## Homebase React 2 | 3 | [![CI](https://github.com/homebaseio/homebase-react/workflows/CI/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACI) 4 | [![CD](https://github.com/homebaseio/homebase-react/workflows/CD/badge.svg)](https://github.com/homebaseio/homebase-react/actions?query=workflow%3ACD) 5 | [![License](https://img.shields.io/github/license/homebaseio/homebase-react.svg)](LICENSE) 6 | [![GitHub Repo stars](https://img.shields.io/github/stars/homebaseio/homebase-react?style=social)](https://github.com/homebaseio/homebase-react) 7 | [![Twitter Follow](https://img.shields.io/twitter/follow/homebase__io?label=Follow&style=social)](https://twitter.com/homebase__io) 8 | 9 | Supported languages, frameworks and DBs: 10 | 11 | - JS + React + Datascript 12 | - CLJS + Reagent + Datascript 13 | - *Datahike support coming soon* 14 | 15 | # ClojureScript + Reagent 16 | 17 | This is the Javascript docs site. Go here for [CLJS + Reagent docs](https://cljdoc.org/d/io.homebase/homebase-react/CURRENT). 18 | 19 | # Javascript + React 20 | 21 | Start by installing `homebase-react` [![NPM Version](https://img.shields.io/npm/v/homebase-react)](https://www.npmjs.com/package/homebase-react) 22 | [![Bundle Size](https://img.shields.io/bundlephobia/minzip/homebase-react)](https://www.npmjs.com/package/homebase-react) 23 | 24 | ```bash 25 | # NPM 26 | npm install homebase-react --save 27 | 28 | # Yarn 29 | yarn add homebase-react 30 | ``` 31 | 32 | Optionally install the `datalog-console` [chrome extension](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb?hl=en) to inspect the `homebase-react` DB in your browser. 33 | 34 | ## Quick Start 35 | 36 | ```js 37 | import { useCallback } from 'react' 38 | import { HomebaseProvider, useEntity, useTransact } from 'homebase-react' 39 | 40 | const RootComponent = () => ( 41 | // Create a DB and set some starting data 42 | 43 | 44 | 45 | ) 46 | 47 | const App = () => { 48 | // Get entity id = 1 49 | const [counter] = useEntity(1) 50 | const [transact] = useTransact() 51 | 52 | return ( 53 | 62 | ) 63 | } 64 | ``` 65 | 66 | [Live demo](https://homebaseio.github.io/homebase-react/#!/homebase.dev.example.counter) 67 | 68 | ## What and Why 69 | 70 | As data and our need to annotate and organize it grows, so does our need for supporting state in *write-heavy* applications. 71 | 72 | To solve this problem, modern write-heavy applications such as Superhuman, Roam Research, and Facebook Messenger built their own embedded data layers to enable these more sophisticated user experiences. 73 | 74 | Homebase React enables developers to access the same embedded datalog database as Roam Research through React hooks. You no longer have to build out a team or learn specialized tools like Clojure in order to build a delightful write-heavy application. 75 | 76 | ## Testimonials 77 | > Homebase is executing on the vision of data usage, portability, and management we had when building Firebase. We never got there. I'm excited! 78 | > 79 | > —James Tamplin, Founder/CEO of Firebase 80 | 81 | > Datalog is the future of end-user programming, personal productivity software, p2p software, etc. 82 | > 83 | > —Chet Corcos, Founding Engineer of Notion 84 | -------------------------------------------------------------------------------- /docs/0200|Quick_Start.md: -------------------------------------------------------------------------------- 1 | > We recommend everyone start by [enabling custom chrome formatters](/docs/homebase-react/main/debugging#custom-chrome-formatters) for a much better debugging experience. 2 | 3 | ![image of custom entity chrome console logs](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_3.png?raw=true) 4 | 5 | Ok. Let's get going. 6 | 7 | Homebase React creates a local relational database for your React app. 8 | 9 | Adding `HomebaseProvider` automatically creates the database. 10 | 11 | ```js 12 | import { HomebaseProvider } from 'homebase-react' 13 | 14 | const config = { initialData: [{ counter: { id: 1, count: 0 }}] } 15 | 16 | const RootComponent = () => ( 17 | 18 | 19 | 20 | ) 21 | ``` 22 | 23 | Read from and write to that database via hooks. 24 | 25 | ```js 26 | import { useCallback } from 'react' 27 | import { useEntity, useTransact } from 'homebase-react' 28 | 29 | const App = () => { 30 | const [counter] = useEntity(1) 31 | const [transact] = useTransact() 32 | 33 | const handleClick = useCallback(() => { 34 | transact([{ counter: { 35 | id: 1, count: counter.get('count') + 1 36 | } }]) 37 | }, [counter, transact]) 38 | 39 | return ( 40 | 44 | ) 45 | } 46 | ``` 47 | 48 | For a step by step guide take a look at the [tutorial](/docs/homebase-react/main/tutorial). 49 | 50 | Check out the [API docs](/docs/homebase-react/main/api) to learn about our other hooks like [`useQuery`](/docs/homebase-react/main/api#usequery) and [`useClient`](/docs/homebase-react/main/api#useclient). -------------------------------------------------------------------------------- /docs/0350|---.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/docs/0350|---.md -------------------------------------------------------------------------------- /docs/0400|API.md: -------------------------------------------------------------------------------- 1 | > We recommend everyone start by [enabling custom chrome formatters](/docs/homebase-react/main/debugging#custom-chrome-formatters) for a much better debugging experience. 2 | 3 | ![image of custom entity chrome console logs](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_3.png?raw=true) 4 | 5 | ## `HomebaseProvider` 6 | 7 | The HomebaseProvider wraps your React app and makes a relational database accessible to all of your components. Configure it with `lookupHelpers` and `initialData`. 8 | 9 | ```js 10 | import { HomebaseProvider, useEntity, useTransact, useQuery } from 'homebase-react' 11 | 12 | const config = { 13 | // Lookup helpers are a way to simplify relational queries at query time. 14 | // The helpers currently supported are: 15 | // `type: 'ref'` which is a relationship and 16 | // `unique: 'identity` which enforces a uniqueness constraint 17 | // and lets you lookup entities by their unique attributes. 18 | lookupHelpers: { 19 | todo: { 20 | project: { type: 'ref', cardinality: 'one' }, 21 | name: { unique: 'identity' } 22 | } 23 | }, 24 | 25 | // Set `debug` to true in order to access the _recentlyTouchedAttributes attribute on your entities 26 | // _recentlyTouchedAttributes shows any cached attributes for a given entity 27 | // This is helpful for approximating that entity's schema and values 28 | debug: true, 29 | 30 | // Initial data is what it sounds like. 31 | // It's a transaction that runs on component mount. 32 | // Use it to hydrate your app. 33 | initialData: [ 34 | { project: { id: -1, name: 'Do it', user: -2 } }, 35 | { todo: { project: -1, name: 'Make it' } }, 36 | { user: { id: -2, name: 'Arpegius' } } 37 | ] 38 | 39 | // Or relationships can be specified implicitly with nested JSON 40 | initialData: [ 41 | { 42 | todo: { 43 | name: 'Make it', 44 | project: { 45 | name: 'Do it', 46 | user: { 47 | name: 'Arpegius' 48 | } 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | 55 | const RootComponent = () => ( 56 | 57 | 58 | 59 | ) 60 | ``` 61 | 62 | ## `useEntity` and `entity.get` 63 | 64 | Entities are the building blocks of the Homebase data model. They are like JSON objects with bonus features. In particular **you can traverse arbitrarily deep relationships without actually denormalizing and nesting your data**. 65 | 66 | ```js 67 | // You can get an entity by its id and get attributes off of it. 68 | const [todo] = useEntity(2) 69 | todo.get('id') // => 2 70 | todo.get('name') // => 'Make it' 71 | 72 | // Entities with unique attributes can also be retrieved by those attributes. 73 | const [sameTodo] = useEntity({ todo: { name: 'Make it' } }) 74 | sameTodo.get('id') // => 2 75 | 76 | // And most importantly you can traverse arbitrarily deep relationships. 77 | sameTodo.get('project', 'user', 'name') // => 'Arpegius' 78 | ``` 79 | 80 | ## `useTransact` 81 | 82 | Transactions let you create, update and delete multiple entities simultaneously. All changes will reactively update any components that depend on the changed data. 83 | 84 | ```js 85 | const [transact] = useTransact() 86 | 87 | // A transaction is an array of nested objects and or arrays. 88 | // Leaving the id blank will create a new entity. 89 | transact([{ todo: { name: 'New Todo', project: 1 } }]) 90 | 91 | // Setting the id to a negative number is a temp id which 92 | // allows multiple entities to be related to each other on creation. 93 | transact([ 94 | { project: { id: -123, name: 'New Project' } }, 95 | { todo: { project: -123, name: 'New Todo' } }, 96 | ]) 97 | 98 | // Update an entity by including its id. 99 | // NOTE: that only the included attributes will be updated. 100 | transact([{ project: { id: 1, name: 'Changed Project Title' } }]) 101 | 102 | // To remove an attribute you have to explicitly set it to null. 103 | transact([{ project: { id: 1, name: null } }]) 104 | 105 | // To delete an entire entity use retractEntity and its id 106 | transact([['retractEntity', 1]]) 107 | ``` 108 | 109 | ## `useQuery` 110 | 111 | Use queries to return an array of entities that meet a given criteria. Our query API is powered by Datalog, but exposed as JSON similar to a JS SQL driver or MongoDB. Datalog is similar to SQL and is incredibly powerful. However, only a subset of features are currently available in JSON. 112 | 113 | We will prioritize features based on community feedback so please open an issue if there's something you need. In the meantime you can further filter results with JS `filter()` and `sort()`. 114 | 115 | ```js 116 | // Finds all todos with a name 117 | const [todos] = useQuery({ 118 | $find: 'todo', 119 | $where: { todo: { name: '$any' } } 120 | }) 121 | 122 | // Returns an array of todo entities 123 | todos 124 | .sort((todo1, todo2) => todo1.get('name') > todo2.get('name') ? 1 : -1) 125 | .map(todo => todo.get('name')) 126 | ``` 127 | 128 | ## `useClient` 129 | 130 | This hook returns the current database client with some helpful functions for syncing data with a backend. 131 | 132 | - `client.dbToString()` serializes the whole db including the lookupHelpers to a string. 133 | - `client.dbFromString('a serialized db string')` replaces the current db. 134 | - `client.dbToDatoms()` returns an array of all the facts aka datoms saved in the db. 135 | - Datoms are the smallest unit of data in the database, like a key value pair but better. 136 | - Datoms are arrays of `[entityId, attribute, value, transactionId, isAddedBoolean]`. 137 | - `client.addTransactListener((changedDatoms) => ...)` adds a listener function to all transactions. 138 | - Use this to save data to your backend. 139 | - `client.removeTransactListener()` removes the transaction listener. 140 | - Please note that only 1 listener can be added per useClient scope. 141 | - `client.transactSilently([{item: {name: ...}}])` like `transact()` only it will not trigger any listeners. 142 | - Use this to sync data from your backend into the client. 143 | - `client.entity(id or { thing: { attr: 'unique value' } })` like `useEntity`, but **returns a promise**. Get an entity in a callback or other places where a React hook does not make sense. 144 | - The entity returned by this function **will NOT live update the parent React component** when its data changes. If you want reactive updates we recommend using `useEntity`. 145 | - `client.query({ $find: 'thing', $where: { thing: { name: '$any' } } })` like `useQuery`, but **returns a promise**. Perform a query in a callback or other places where a React hook does not make sense. 146 | - The entities returned by this function **will NOT live update the parent React component** when their data changes. If you want reactive updates we recommend using `useQuery`. 147 | 148 | Check out the [Firebase example](https://homebaseio.github.io/homebase-react/#!/example.todo_firebase) for a demonstration of how you might integrate a backend. 149 | 150 | ## Arrays & Nested JSON 151 | 152 | 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. 153 | 154 | ```js 155 | const config = { 156 | lookupHelpers: { 157 | company: { 158 | numbers: { type: 'ref', cardinality: 'many' }, 159 | projects: { type: 'ref', cardinality: 'many' }, 160 | } 161 | } 162 | } 163 | 164 | transact([ 165 | { project: { id: -1, name: 'a' } }, 166 | { 167 | company: { 168 | numbers: [1, 2, 3], 169 | projects: [ 170 | { project: { id: -1 } }, 171 | { project: { name: 'b' } }, 172 | ] 173 | } 174 | } 175 | ]) 176 | 177 | // Index into arrays 178 | company.get('numbers', 1, 'value') // => 2 179 | company.get('projects', 0, 'ref', 'name') // => 'a' 180 | // Get the automatically assigned order 181 | // Order starts at 1 and increments by 1 182 | company.get('numbers', 0, 'order') // => 1 183 | company.get('projects', 0, 'order') // => 1 184 | company.get('projects', 1, 'order') // => 2 185 | // Map over individual attributes 186 | company.get('numbers', 'value') // => [1, 2, 3] 187 | company.get('projects', 'ref', 'name') // => ['a', 'b'] 188 | ``` 189 | 190 | The `entity.get` API is flexible and supports indexing into arrays as well as automatically mapping over individual attributes. 191 | 192 | 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`. 193 | 194 | ```js 195 | transact([ 196 | { 197 | id: company.get('numbers', 2, 'id'), 198 | order: (company.get('numbers', 0, 'order') 199 | + company.get('numbers', 1, 'order')) / 2 200 | } 201 | ]) 202 | 203 | company.get('numbers', 'value') // => [1 3 2] 204 | ``` 205 | 206 | If you need to transact complex JSON like arrays of arrays then you're better off serializing it to a string first. 207 | 208 | ```js 209 | // NOT supported 210 | transact([{ company: { matrix: [[1, 2, 3], [4, 5, 6]] } }]) 211 | 212 | // Better 213 | transact([{ company: { matrix: JSON.stringify([[1, 2, 3], [4, 5, 6]]) } }]) 214 | JSON.parse(company.get('matrix')) 215 | ``` 216 | 217 | For more information check out the [JSON Derived Relationships blog post](https://homebase.io/blog/homebase-react-0.5.0-json-derived-relationships) 218 | -------------------------------------------------------------------------------- /docs/0650|---.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/docs/0650|---.md -------------------------------------------------------------------------------- /docs/0675|Debugging.md: -------------------------------------------------------------------------------- 1 | Homebase React uses ClojureScript and its corresponding data format EDN internally. We then compile all of that to Javascript using the Google Closure Compiler (closure not clojure) to get as small a bundle as possible. Then we provide APIs (react hooks) that accept JSON and do all the conversion to EDN and back again behind the scenes. 2 | 3 | EDN and Clojure provide far more safety and extensibility than JSON and Javascript; Clojure being immutable by default and EDN being extensible. This lets us build and support features that would be unwieldy in JSON and JS. 4 | 5 | However, the tradeoffs are: 6 | 7 | 1. A larger bundle size. Some of the Clojure runtime cannot be compiled away even though the closure compiler is really aggressive. 8 | 2. Clojure error messages sometimes leak into JS land. We try to annotate the ones we know about so they make sense to JS devs, but it's far from perfect and if you see something weird please create an issue. 9 | 3. Our code is released already minified. We do this because most people do not develop with the google closure compiler and other build tools are not nearly as effective at optimizing this code. This makes debugging homebase-react while developing a bit harder since the code is not very readable, but we think the tradeoff is worth it to provide a smaller bundle size. And to compensate we try to build enough supporting dev tooling so you never need to read the compiled source. 10 | 4. Confusing console logs. EDN data looks different from JSON and to add to that, homebase-react mostly outputs entities, which are lazy data types and not very helpful when logged out with the default console formatting. See custom chrome formatters below for a vastly improved logging experience. 11 | 12 | ### Custom chrome formatters 13 | If you develop with [Chrome](https://www.google.com/chrome/) or a Chromium browser like Brave or Edge you'll get significantly more meaningful logs for entities `console.log(anEntity)` due to our use of custom chrome :formatters. These custom formatters allow us to perform lazy database queries to fetch all of an entity's attributes, including references to other entities and all reverse references to the current entity. They let you access your entire data graph from the console, with any logged out entity as an entry point. 14 | 15 | **To enable custom chrome formatters** 16 | 17 | **1.** Open the preferences panel in chrome devtools by clicking the cog. 18 | 19 | ![image of chrome devtools preferences button](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_1.png?raw=true) 20 | 21 | **2.** Toggle `Enabled custom formatters` on. 22 | 23 | ![image of chrome devtools custom formatters toggle](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_2.png?raw=true) 24 | 25 | **3.** Keep the chrome console open and refresh the page. Any logged out entities should now have the custom formatting. 26 | 27 | ![image of custom entity chrome console logs](https://github.com/homebaseio/homebase-react/blob/master/public/images/enable_chrome_formatters_3.png?raw=true) 28 | 29 | **Live demo:** open the console while on the [todo example](https://homebaseio.github.io/homebase-react/#!/homebase.dev.example.todo) page. 30 | 31 | **Remember**: for custom formatters to work `console.log(anEntity)` must be called *after* you open the chrome console. Anything logged out before you open the console will not have custom formatting applied because chrome processes those logs in the background. 32 | 33 | ### Datalog Console Extension 34 | 35 | We also integrate with the [Datalog Console](https://github.com/homebaseio/datalog-console) extension. 36 | 37 | ![image of datalog console extension](https://github.com/homebaseio/homebase-react/blob/master/public/images/datalog_console.png?raw=true) 38 | 39 | It's still in an early stage of development, but we seek to expose all common DB administration capabilities here and let you connect to any Datalog database that implements the console's interface. 40 | 41 | #### Using the Datalog Console 42 | 43 | 1. [Add the extension to Chrome](https://chrome.google.com/webstore/detail/datalog-console/cfgbajnnabfanfdkhpdhndegpmepnlmb) 44 | 2. Visit a page built with homebase-react [like this one](https://homebaseio.github.io/homebase-react/#!/homebase.dev.example.todo), open the inspector, click the `Datalog DB` tab, and click `Load database` to try it out 45 | 46 | ### DEPRECATED `_recentlyTouchedAttributes` 47 | 48 | *Use [custom chrome formatters](#custom-chrome-formatters) instead.* 49 | 50 | If you set `debug` to `true` in your configuration, you will be able to access the `_recentlyTouchedAttributes` attribute on entities. `_recentlyTouchedAttributes` will show any cached attributes for a given entity. This is helpful for approximating that entity's schema and values. 51 | 52 | ```js 53 | 54 | 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/0700|Performance.md: -------------------------------------------------------------------------------- 1 | Homebase React tracks the attributes consumed in each component via the `entity.get` function and scopes those attributes to their respective `useEntity` or `useQuery` hook. Re-renders are only triggered when an attribute changes. 2 | 3 | The default caching reduces unnecessary re-renders and virtual DOM thrashing a lot. That said, it is still possible to trigger more re-renders than you might want. 4 | 5 | ## Smart Prop Drilling 6 | 7 | One top level `useQuery` + prop drilling the entities it returns will cause all children to re-render on any change to the parent or their siblings. 8 | 9 | To fix this we recommend passing ids to children, not whole entities. Instead get the entity in the child with `useEntity(id)`. This creates a new scope for each child so they are not affected by changes in the state of the parent or sibling components. 10 | 11 | ### Good Prop Drilling 12 | 13 | ```js 14 | const TodoList = () => { 15 | const [todos] = useQuery({ 16 | $find: 'todo', 17 | $where: { todo: { name: '$any' } } 18 | }) 19 | return (todos.map(t => )) 20 | } 21 | 22 | const Todo = React.memo(({ id }) => { 23 | const [todo] = useEntity(id) 24 | // ... 25 | }) 26 | ``` 27 | 28 | ### Bad Prop Drilling 29 | 30 | ```js 31 | const TodoList = () => { 32 | const [todos] = useQuery({ 33 | $find: 'todo', 34 | $where: { todo: { name: '$any' } } 35 | }) 36 | return (todos.map(t => )) 37 | } 38 | 39 | const Todo = React.memo(({ todo }) => { 40 | // ... 41 | }) 42 | ``` 43 | 44 | ## React Hooks Performance 45 | 46 | If you're looking for more optimizations check out the [React docs on optimizing hooks](https://reactjs.org/docs/hooks-faq.html#performance-optimizations). -------------------------------------------------------------------------------- /docs/0800|Examples.md: -------------------------------------------------------------------------------- 1 | Want to see homebase-react in action? Take a look at the examples. 2 | 3 | - [Live Examples](https://homebaseio.github.io/homebase-react/) 4 | - [Examples Repo](https://github.com/homebaseio/homebase-react/tree/master/examples) -------------------------------------------------------------------------------- /docs/0900|Recipes.md: -------------------------------------------------------------------------------- 1 | ## Syncing to Firebase 2 | 3 | The example below shows a recipe for keeping Homebase React in sync with Firebase. `client.addTransactListener(callback)` lets you listen to every local transaction and send those updates to Firebase. We also need a way to sync Firebase with Homebase React. In this example we create a namespace on Firebase for each user based on their firebase uid and listen to all changes in that namespace. client.` transactSilently(tx)` allows us save changes received from Firebase without triggering our transactListener function and sending those changes back to Firebase endlessly. 4 | 5 | ```js 6 | import { useClient, useEntity } from 'homebase-react'; 7 | import firebase from 'firebase/app'; 8 | import debounce from 'lodash/debounce'; 9 | import React from 'react'; 10 | 11 | const SyncToFirebase = () => { 12 | const [client] = useClient() 13 | const [currentUser] = useEntity({ identity: 'currentUser' }) 14 | const userId = currentUser.get('uid') 15 | const transactListener = React.useCallback( 16 | (changedDatoms) => { 17 | const cardinalityManyAttrs = new Set([]) // E.g. ':project/todos' or ':user/friends' 18 | const localOnlyAttrs = new Set([]) // E.g. ':current-user/uid' these are attributes you don't 19 | // want to save to Firebase, but also don't want to have to call `client.transactSilently()` everytime you change them. 20 | 21 | // Find the datoms that were changed more than once 22 | const numDatomChanges = changedDatoms.reduce( 23 | (acc, [id, attr]) => ({ ...acc, [id + attr]: (acc[id + attr] || 0) + 1 }), 24 | {}, 25 | ) 26 | // Only send one change to firebase per datom 27 | const datomsForFirebase = changedDatoms.filter( 28 | // eslint-disable-next-line no-unused-vars 29 | ([id, attr, _, __, isAdded]) => !(!isAdded && numDatomChanges[id + attr] > 1), 30 | ) 31 | datomsForFirebase.forEach(([id, attr, v, tx, isAdded]) => { 32 | if (!localOnlyAttrs.has(attr)) { 33 | const ref = firebase.database().ref( 34 | // This example uses firebase realtime database with the following rules. 35 | // { 36 | // "rules": { 37 | // "users": { 38 | // "$uid": { 39 | // ".read": "$uid === auth.uid", 40 | // ".write": "$uid === auth.uid" 41 | // } 42 | // } 43 | // } 44 | // } 45 | // Every user has a unique namespace with full read/write permission. 46 | // For single page apps like this we can write the raw datoms to this namespace. 47 | // Here we are generating a unique key for every datom. 48 | `users/${userId}/entities/${id}|${attr.replace('/', '|')}|${ 49 | // add the value to the key of cardinality many datoms since they are only unique when their value is included 50 | cardinalityManyAttrs.has(attr) ? v : '' 51 | }`, 52 | ) 53 | // eslint-disable-next-line no-unused-expressions 54 | isAdded ? ref.set([id, attr, v, tx, isAdded]) : ref.remove() 55 | } 56 | }) 57 | }, 58 | [userId], 59 | ) 60 | 61 | React.useEffect(() => { 62 | const softTransact = (tx) => { 63 | try{ 64 | client.transactSilently(tx) 65 | } catch (er) { 66 | tx.forEach((txPart) => { 67 | try { 68 | client.transactSilent([txPart]) 69 | } catch (err) { 70 | // eslint-disable-next-line no-console 71 | console.warn(err, txPart) 72 | } 73 | }) 74 | } 75 | } 76 | // Homebase -> Firebase 77 | client.addTransactListener(transactListener) 78 | // Firebase -> Homebase 79 | const ref = firebase.database().ref(`users/${userId}/entities`) 80 | let txQueue = [] 81 | const debouncedTransactQueue = debounce(() => { 82 | softTransact(txQueue) 83 | txQueue = [] 84 | }, 300) 85 | const onAdd = (ds) => { 86 | txQueue.push(['add', ...ds.val()]) 87 | debouncedTransactQueue() 88 | } 89 | const onRetract = (ds) => { 90 | txQueue.push(['retract', ...ds.val()]) 91 | debouncedTransactQueue() 92 | } 93 | ref.on('child_added', onAdd) 94 | ref.on('child_removed', onRetract) 95 | ref.on('child_changed', onAdd) 96 | return () => { 97 | client.removeTransactListener() 98 | ref.off('child_added', onAdd) 99 | ref.off('child_removed', onRetract) 100 | ref.off('child_changed', onAdd) 101 | } 102 | }, [userId, client, transactListener]) 103 | return null 104 | } 105 | 106 | export default SyncToFirebase; 107 | ``` 108 | 109 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Homebase React Examples 2 | 3 | You can clone this repo to have ready to run [homebase-react](https://github.com/homebaseio/homebase-react) examples. 4 | 5 | ## Examples 6 | - [Counter Example](counter/) 7 | - **Live** — https://homebase-example-counter.vercel.app 8 | - [Todo Example](todo/) 9 | - **Live** — https://homebase-example-todo.vercel.app 10 | - [Typescript Firebase Todo Example](typescript-firebase-todo/) 11 | - **Live** — https://homebase-example-ts-firebase-todo.vercel.app 12 | - [Roam Research Example](roam/) 13 | - **Live** — https://homebase-example-roam.vercel.app 14 | 15 | ## Contributing 16 | We'd love to see your examples. PR them and we will add them to the repo if they show an application of Homebase not displayed in our current demos. 17 | -------------------------------------------------------------------------------- /examples/counter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | # Counter example 2 | 3 | Heavily adopted from `create-react-app` bootstrapping, here's a our Counter example bundled in a React application. 4 | 5 | ## Live demo 6 | - https://homebase-example-counter.vercel.app 7 | 8 | ## Installation 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ## Run it 14 | ``` 15 | yarn start 16 | ``` -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebase-react-counter-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "homebase-react": "^0.7.0", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.0", 13 | "web-vitals": "^0.2.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/counter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/counter/public/favicon.ico -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Homebase Counter Example 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/counter/src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | padding: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } -------------------------------------------------------------------------------- /examples/counter/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HomebaseProvider, useEntity, useTransact } from 'homebase-react' 3 | import './App.css' 4 | 5 | const config = { 6 | initialData: [{ 7 | counter: { 8 | identity: 'counter', 9 | count: 0 10 | } 11 | }] 12 | } 13 | 14 | export default function App() { 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | const Counter = () => { 23 | const [counter] = useEntity({ identity: 'counter' }) 24 | const [transact] = useTransact() 25 | return ( 26 |
27 | Count: {counter.get('count')} 28 |
29 | 37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/counter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/reagent/counter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /checkouts 6 | /src/gen 7 | 8 | pom.xml 9 | pom.xml.asc 10 | *.iml 11 | *.jar 12 | *.log 13 | .shadow-cljs 14 | .idea 15 | .lein-* 16 | .nrepl-* 17 | .DS_Store 18 | 19 | .hgignore 20 | .hg/ 21 | -------------------------------------------------------------------------------- /examples/reagent/counter/README.md: -------------------------------------------------------------------------------- 1 | # Reagent Counter Example 2 | 3 | ``` 4 | yarn install 5 | yarn dev 6 | ``` 7 | 8 | open http://localhost:3000 -------------------------------------------------------------------------------- /examples/reagent/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "shadow-cljs watch dev" 7 | }, 8 | "devDependencies": { 9 | "shadow-cljs": "2.14.5" 10 | }, 11 | "dependencies": { 12 | "highlight.js": "^9.12.0", 13 | "react": "16.13.0", 14 | "react-dom": "16.13.0", 15 | "react-grid-layout": "^0.16.6", 16 | "react-icons": "^2.2.7", 17 | "reakit": "^0.11.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/reagent/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | homebase examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/reagent/counter/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths ["src/main"] 3 | :dev-http {3000 "public"} 4 | :dependencies [[datascript "1.0.7"] 5 | [reagent "1.0.0-alpha2"] 6 | [io.homebase/homebase-react "0.1.1"] 7 | [io.homebase/datalog-console "0.2.2"]] 8 | :builds {:dev {:target :browser 9 | :output-dir "public/js" 10 | :asset-path "/js" 11 | :compiler-options {:externs ["datascript/externs.js"]} 12 | :modules {:main {:init-fn counter/init!}}}}} 13 | -------------------------------------------------------------------------------- /examples/reagent/counter/src/main/counter.cljs: -------------------------------------------------------------------------------- 1 | (ns counter 2 | (:require 3 | [reagent.dom :as rdom] 4 | [datascript.core :as d] 5 | [homebase.reagent :as hbr] 6 | [datalog-console.integrations.datascript :as datalog-console])) 7 | 8 | (def db-conn (d/create-conn {})) 9 | (d/transact! db-conn [[:db/add 1 :count 0]]) ; Transact some starting data. 10 | (hbr/connect! db-conn) ; Connect homebase.reagent to the database. 11 | (datalog-console/enable! {:conn db-conn}) ; Also connect the datalog-console extension for better debugging. 12 | 13 | (defn counter [] 14 | (let [[entity] (hbr/entity db-conn 1)] ; Get a homebase.reagent/Entity. Note the use of db-conn and not @db-conn, this makes it reactive. 15 | (js/console.log @entity) 16 | (fn [] 17 | [:div 18 | "Count: " (:count @entity) ; Deref the entity just like a reagent/atom. 19 | [:div 20 | [:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @entity))]])} ; Use d/transact! just like normal. 21 | "Increment"]]]))) 22 | 23 | (defn init! [] 24 | (rdom/render [counter] (.-body js/document))) 25 | -------------------------------------------------------------------------------- /examples/reagent/todo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /checkouts 6 | /src/gen 7 | 8 | pom.xml 9 | pom.xml.asc 10 | *.iml 11 | *.jar 12 | *.log 13 | .shadow-cljs 14 | .idea 15 | .lein-* 16 | .nrepl-* 17 | .DS_Store 18 | 19 | .hgignore 20 | .hg/ 21 | -------------------------------------------------------------------------------- /examples/reagent/todo/README.md: -------------------------------------------------------------------------------- 1 | # Reagent Todo Example 2 | 3 | ``` 4 | yarn install 5 | yarn dev 6 | ``` 7 | 8 | open http://localhost:3000 -------------------------------------------------------------------------------- /examples/reagent/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "shadow-cljs watch dev" 7 | }, 8 | "devDependencies": { 9 | "shadow-cljs": "2.14.5" 10 | }, 11 | "dependencies": { 12 | "highlight.js": "^9.12.0", 13 | "react": "16.13.0", 14 | "react-dom": "16.13.0", 15 | "react-grid-layout": "^0.16.6", 16 | "react-icons": "^2.2.7", 17 | "reakit": "^0.11.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/reagent/todo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | homebase examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/reagent/todo/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths ["src/main"] 3 | :dev-http {3000 "public"} 4 | :dependencies [[datascript "1.0.7"] 5 | [reagent "1.0.0-alpha2"] 6 | [io.homebase/homebase-react "0.1.1"] 7 | [io.homebase/datalog-console "0.2.2"]] 8 | :builds {:dev {:target :browser 9 | :output-dir "public/js" 10 | :asset-path "/js" 11 | :compiler-options {:externs ["datascript/externs.js"]} 12 | :modules {:main {:init-fn todo/init!}}}}} 13 | -------------------------------------------------------------------------------- /examples/reagent/todo/src/main/todo.cljs: -------------------------------------------------------------------------------- 1 | (ns todo 2 | (:require 3 | [reagent.core :as r] 4 | [reagent.dom :as rdom] 5 | [datascript.core :as d] 6 | [homebase.reagent :as hbr] 7 | [datalog-console.integrations.datascript :as datalog-console])) 8 | 9 | (def schema {:db/ident {:db/unique :db.unique/identity} 10 | :todo/project {:db/type :db.type/ref 11 | :db/cardinality :db.cardinality/one} 12 | :todo/owner {:db/type :db.type/ref 13 | :db/cardinality :db.cardinality/one}}) 14 | 15 | (def db-conn (d/create-conn schema)) 16 | 17 | (def initial-tx [{:db/ident :todo.filters 18 | :todo.filter/show-completed? true 19 | :todo.filter/owner 0 20 | :todo.filter/project 0} 21 | {:todo/name "Go home" 22 | :todo/created-at (js/Date.now) 23 | :todo/owner -2 24 | :todo/project -3} 25 | {:todo/name "Fix ship" 26 | :todo/completed? true 27 | :todo/created-at (js/Date.now) 28 | :todo/owner -1 29 | :todo/project -4} 30 | {:db/id -1 31 | :user/name "Stella"} 32 | {:db/id -2 33 | :user/name "Arpegius"} 34 | {:db/id -3 35 | :project/name "Do it"} 36 | {:db/id -4 37 | :project/name "Make it"}]) 38 | 39 | (d/transact! db-conn initial-tx) 40 | 41 | (hbr/connect! db-conn) 42 | 43 | (datalog-console/enable! {:conn db-conn}) 44 | 45 | (defn select [{:keys [attr]}] 46 | (let [[options] (hbr/q '[:find ?e ?v 47 | :in $ ?attr 48 | :where [?e ?attr ?v]] 49 | db-conn attr)] 50 | (fn [{:keys [label attr value on-change]}] 51 | [:label label " " 52 | [:select 53 | {:name (str attr) 54 | :value (or value "") 55 | :on-change (fn [e] (when on-change (on-change (js/Number (goog.object/getValueByKeys e #js ["target" "value"])))))} 56 | [:option {:value ""} ""] 57 | (for [[id value] @options] 58 | ^{:key id} [:option 59 | {:value id} 60 | value])]]))) 61 | 62 | (defn todo [id] 63 | (let [[todo] (hbr/entity db-conn id)] 64 | (js/console.log @todo) 65 | (fn [id] 66 | [:div {:style {:padding-bottom 20}} 67 | [:div 68 | [:input 69 | {:type "checkbox" 70 | :style {:width "18px" :height "18px" :margin-left "0"} 71 | :checked (true? (:todo/completed? @todo)) 72 | :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}] 73 | [:input 74 | {:type "text" 75 | :style {:text-decoration (when (:todo/completed? @todo) "line-through") :border "none" :width "auto" :font-weight "bold" :font-size "20px"} 76 | :value (:todo/name @todo) 77 | :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/name (goog.object/getValueByKeys % #js ["target" "value"])]])}]] 78 | [:div 79 | [select 80 | {:label "Owner:" 81 | :attr :user/name 82 | :value (get-in @todo [:todo/owner :db/id]) 83 | :on-change (fn [owner-id] (d/transact! db-conn [[(if (= 0 owner-id) :db/retract :db/add) (:db/id @todo) :todo/owner (when (not= 0 owner-id) owner-id)]]))}] 84 | " · " 85 | [select 86 | {:label "Project:" 87 | :attr :project/name 88 | :value (get-in @todo [:todo/project :db/id]) 89 | :on-change (fn [project-id] (d/transact! db-conn [[(if (= 0 project-id) :db/retract :db/add) (:db/id @todo) :todo/project (when (not= 0 project-id) project-id)]]))}] 90 | " · " 91 | [:button 92 | {:on-click #(d/transact! db-conn [[:db/retractEntity (:db/id @todo)]])} 93 | "Delete"]] 94 | [:div 95 | [:small {:style {:color "grey"}} 96 | (.toLocaleString (js/Date. (:todo/created-at @todo)))]]]))) 97 | 98 | (defn todo-filters [] 99 | (let [[filters] (hbr/entity db-conn [:db/ident :todo.filters])] 100 | (fn [] 101 | [:div {:style {:padding "20px 0"}} 102 | [:strong "Filters · "] 103 | [:label 104 | "Show completed " 105 | [:input 106 | {:type "checkbox" 107 | :checked (:todo.filter/show-completed? @filters) 108 | :on-change #(d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/show-completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}]] 109 | " · " 110 | [select 111 | {:label "Owner" 112 | :attr :user/name 113 | :value (:todo.filter/owner @filters) 114 | :on-change (fn [owner-id] (d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/owner (or owner-id 0)]]))}] 115 | " · " 116 | [select 117 | {:label "Project" 118 | :attr :project/name 119 | :value (:todo.filter/project @filters) 120 | :on-change (fn [project-id] (d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/project (or project-id 0)]]))}]]))) 121 | 122 | (defn todos [] 123 | (let [[todos] (hbr/q '[:find [(pull ?todo [:db/id :todo/created-at]) ...] 124 | :where 125 | ; Get all todos with names 126 | [?todo :todo/name] 127 | 128 | ; Get the id for :todo.filters 129 | [?filters :db/ident :todo.filters] 130 | 131 | ; Filter completed todos if not :todo.filter/show-completed? 132 | (or [?filters :todo.filter/show-completed? true] 133 | (not [?todo :todo/completed? true])) 134 | 135 | ; Filter by owner if :todo.filter/owner is not 0 136 | [?filter :todo.filter/owner ?owner] 137 | (or [(= 0 ?owner)] 138 | [?todo :todo/owner ?owner]) 139 | 140 | ; Filter by project if :todo.filter/project is not 0 141 | [?filter :todo.filter/project ?project] 142 | (or [(= 0 ?project)] 143 | [?todo :todo/project ?project])] 144 | db-conn)] 145 | (fn [] 146 | [:div 147 | [todo-filters] 148 | [:div 149 | (for [{:keys [db/id]} 150 | (->> @todos 151 | (sort-by :todo/created-at) 152 | (reverse))] 153 | ^{:key id} [todo id])]]))) 154 | 155 | (defn new-todo [] 156 | (let [name (r/atom "") 157 | [filters] (hbr/entity db-conn [:db/ident :todo.filters])] 158 | (fn [] 159 | [:form {:on-submit (fn [e] 160 | (.preventDefault e) 161 | (d/transact! db-conn [{:todo/name @name 162 | :todo/created-at (js/Date.now)} 163 | ; Also reset the filters to make sure the new todo shows up in the UI immediately 164 | {:db/id (:db/id @filters) 165 | :todo.filter/show-completed? true 166 | :todo.filter/owner 0 167 | :todo.filter/project 0}]) 168 | (reset! name ""))} 169 | [:input {:type "text" 170 | :on-change #(reset! name (goog.object/getValueByKeys % #js ["target" "value"])) 171 | :value @name 172 | :placeholder "Write a todo..."}] 173 | [:button {:type "submit"} "Create todo"]]))) 174 | 175 | (defn todo-app [] 176 | [:div 177 | [new-todo] 178 | [todos]]) 179 | 180 | (defn init! [] 181 | (rdom/render [todo-app] (.-body js/document))) 182 | -------------------------------------------------------------------------------- /examples/roam/.eslintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "root": true, 4 | "parser": "babel-eslint", 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:jsx-a11y/recommended", 11 | "plugin:react-hooks/recommended", 12 | "airbnb", 13 | "prettier" 14 | ], 15 | "env": { 16 | "browser": true, 17 | "commonjs": true, 18 | "es6": true, 19 | "node": true 20 | }, 21 | "parserOptions": { 22 | "ecmaVersion": 2018, 23 | "sourceType": "module", 24 | "ecmaFeatures": { 25 | "jsx": true 26 | } 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | }, 33 | "rules": { 34 | "semi": 0, 35 | "react/jsx-filename-extension": 0, 36 | "react/react-in-jsx-scope": 0, 37 | "react/prop-types": 0, 38 | "implicit-arrow-linebreak": 0, 39 | "object-curly-newline": 0, 40 | "react/no-unescaped-entities": 0, 41 | "react/jsx-one-expression-per-line": 0, 42 | "prettier/prettier": ["error"], 43 | "jsx-a11y/anchor-is-valid": 0 44 | }, 45 | "plugins": ["prettier"] 46 | } -------------------------------------------------------------------------------- /examples/roam/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /examples/roam/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 100 8 | } -------------------------------------------------------------------------------- /examples/roam/README.md: -------------------------------------------------------------------------------- 1 | # Roam example 2 | 3 | An example of [Roam Research](https://roamresearch.com/) built with Homebase React. 4 | 5 | Roam and Homebase use the same underlying [graph database](https://github.com/tonsky/datascript), making Roam a great demonstration of what can be accomplished with ease with a graph DB at the heart of your application. 6 | 7 | ## Live demo 8 | - https://homebase-example-roam.vercel.app 9 | 10 | ## Code Tour 11 | Start with [`Block.js`](src/components/Block.js). This is where most of the interesting things happen. 12 | 13 | ## Installation 14 | ``` 15 | yarn install 16 | ``` 17 | 18 | ## Run it 19 | ``` 20 | yarn start 21 | ``` 22 | 23 | ## Import your own Roam data 24 | 25 | See the [`/scripts`](scripts/convert_roam_edn/) directory 26 | -------------------------------------------------------------------------------- /examples/roam/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [require('tailwindcss'), require('autoprefixer')], 5 | }, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /examples/roam/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roam", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.0.0", 7 | "@tailwindcss/postcss7-compat": "^2.0.2", 8 | "@testing-library/dom": "^7.29.4", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "@types/react": "^17.0.1", 13 | "@webscopeio/react-textarea-autocomplete": "^4.7.3", 14 | "autoprefixer": "^9", 15 | "firebase": "^8.2.6", 16 | "firebaseui": "^4.7.3", 17 | "homebase-react": "^0.7.0", 18 | "lodash": "^4.17.20", 19 | "nanoid": "^3.1.20", 20 | "postcss": "^7", 21 | "prop-types": "^15.7.2", 22 | "react": "^16.14.0", 23 | "react-autosize-textarea": "^6.0.0", 24 | "react-dom": "^16.14.0", 25 | "react-is": "^17.0.1", 26 | "react-markdown": "^5.0.3", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "4.0.1", 29 | "react-syntax-highlighter": "^15.4.3", 30 | "react-twitter-embed": "^3.0.3", 31 | "remark-gfm": "^1.0.0", 32 | "styled-components": "^5.2.1", 33 | "tailwindcss": "npm:@tailwindcss/postcss7-compat", 34 | "typescript": "^4.1.4", 35 | "web-vitals": "^0.2.4" 36 | }, 37 | "devDependencies": { 38 | "babel-eslint": "^10.1.0", 39 | "babel-plugin-add-react-displayname": "^0.0.5", 40 | "babel-plugin-styled-components": "^1.12.0", 41 | "eslint": "^7.0.0", 42 | "eslint-config-airbnb": "18.2.1", 43 | "eslint-config-prettier": "^7.2.0", 44 | "eslint-plugin-import": "^2.22.1", 45 | "eslint-plugin-jsx-a11y": "^6.4.1", 46 | "eslint-plugin-prettier": "^3.3.1", 47 | "eslint-plugin-react": "^7.21.5", 48 | "eslint-plugin-react-hooks": "4.2.0", 49 | "prettier": "^2.2.1" 50 | }, 51 | "scripts": { 52 | "start": "craco start", 53 | "build": "craco build", 54 | "test": "craco test", 55 | "eject": "react-scripts eject" 56 | }, 57 | "eslintConfig": { 58 | "extends": [ 59 | "react-app", 60 | "react-app/jest" 61 | ] 62 | }, 63 | "browserslist": { 64 | "production": [ 65 | ">0.2%", 66 | "not dead", 67 | "not op_mini all" 68 | ], 69 | "development": [ 70 | "last 1 chrome version", 71 | "last 1 firefox version", 72 | "last 1 safari version" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/roam/public/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.824c211d.chunk.css", 4 | "main.js": "/static/js/main.637b4022.chunk.js", 5 | "main.js.map": "/static/js/main.637b4022.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.98f126f5.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.98f126f5.js.map", 8 | "static/js/2.284b0eb6.chunk.js": "/static/js/2.284b0eb6.chunk.js", 9 | "static/js/2.284b0eb6.chunk.js.map": "/static/js/2.284b0eb6.chunk.js.map", 10 | "static/js/3.b5393694.chunk.js": "/static/js/3.b5393694.chunk.js", 11 | "static/js/3.b5393694.chunk.js.map": "/static/js/3.b5393694.chunk.js.map", 12 | "index.html": "/index.html", 13 | "static/css/main.824c211d.chunk.css.map": "/static/css/main.824c211d.chunk.css.map", 14 | "static/js/2.284b0eb6.chunk.js.LICENSE.txt": "/static/js/2.284b0eb6.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.98f126f5.js", 18 | "static/js/2.284b0eb6.chunk.js", 19 | "static/css/main.824c211d.chunk.css", 20 | "static/js/main.637b4022.chunk.js" 21 | ] 22 | } -------------------------------------------------------------------------------- /examples/roam/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/roam/public/favicon.ico -------------------------------------------------------------------------------- /examples/roam/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 27 | Homebase React - Roam Demo 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/roam/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/roam/public/logo192.png -------------------------------------------------------------------------------- /examples/roam/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/roam/public/logo512.png -------------------------------------------------------------------------------- /examples/roam/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/roam/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/roam/scripts/convert_roam_edn/README.md: -------------------------------------------------------------------------------- 1 | 1. Press export all in Roam 2 | 1. Select EDN and export 3 | 1. Remove `#datascript/DB` from the first line 4 | 1. Install the [Clojure CLI](https://clojure.org/guides/getting_started) if you don't already have it 5 | 1. cd to the root of this repo and run `clj scripts/convert_roam_edn/convert.clj FILE_TO_CONVERT.edn` 6 | - E.g. `clj scripts/convert_roam_edn/convert.clj scripts/convert_roam_edn/datasets/hn.edn` 7 | - This will write a converted file of the same name to `public/edn` -------------------------------------------------------------------------------- /examples/roam/scripts/convert_roam_edn/convert.clj: -------------------------------------------------------------------------------- 1 | (ns convert 2 | "Converts an export from Roam Research into an 3 | edn file that can be read into Homebase React. 4 | 5 | Homebase React expects a single namespace per entity 6 | to make Datalog feel more like SQL and tabular data. 7 | 8 | This script normalizes all attributes to the 'block' namespace." 9 | {:no-doc true} 10 | (:require [clojure.pprint])) 11 | 12 | ;; (def input (read-string (slurp "scripts/convert_roam_edn/datasets/hn.edn"))) 13 | (def input (read-string (slurp (first *command-line-args*)))) 14 | 15 | (def output 16 | {:schema 17 | (reduce-kv 18 | (fn [acc k v] 19 | (assoc acc (keyword "block" (name k)) v)) 20 | {} (:schema input)) 21 | :datoms 22 | (reduce 23 | (fn [acc [e a v t]] 24 | (cond-> acc 25 | (= a :node/title) (conj [e :block/node? true t]) 26 | true (conj [e (keyword "block" (name a)) v t]))) 27 | [] (:datoms input))}) 28 | 29 | (def filename (str "public/edn/" (last (clojure.string/split (first *command-line-args*) #"\/")))) 30 | 31 | (spit filename "#datascript/DB ") 32 | (clojure.pprint/pprint output (clojure.java.io/writer filename :append true)) 33 | 34 | -------------------------------------------------------------------------------- /examples/roam/src/App.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app' 2 | import 'firebase/auth' 3 | import 'firebase/database' 4 | import * as firebaseui from 'firebaseui' 5 | import 'firebaseui/dist/firebaseui.css' 6 | import { HomebaseProvider, useClient, useEntity } from 'homebase-react' 7 | import debounce from 'lodash/debounce' 8 | import React from 'react' 9 | import { BrowserRouter as Router, Link, Route, Switch, useHistory } from 'react-router-dom' 10 | import ScrollToTop from './components/ScrollToTop' 11 | import Blocks from './pages/Blocks' 12 | import PageUid from './pages/PageUid' 13 | 14 | const firebaseConfig = { 15 | apiKey: 'AIzaSyA7HLuuo0GAAOlHZzYaAjhLK-IBrmj-nnA', 16 | authDomain: 'homebase-react-roam.firebaseapp.com', 17 | databaseURL: 'https://homebase-react-roam-default-rtdb.firebaseio.com', 18 | projectId: 'homebase-react-roam', 19 | storageBucket: 'homebase-react-roam.appspot.com', 20 | messagingSenderId: '885247589880', 21 | appId: '1:885247589880:web:28cedd7c615accfe20aa0d', 22 | } 23 | 24 | firebase.initializeApp(firebaseConfig) 25 | const firebaseUI = new firebaseui.auth.AuthUI(firebase.auth()) 26 | 27 | const AuthButton = () => { 28 | const [currentUser] = useEntity({ identity: 'currentUser' }) 29 | if (currentUser.get('uid')) { 30 | return ( 31 | <> 32 | 33 | 34 | 35 | ) 36 | } 37 | return 38 | } 39 | 40 | const SignIn = () => { 41 | const [show, setShow] = React.useState(false) 42 | React.useEffect(() => { 43 | if (show) { 44 | firebaseUI.start('#firebaseui-auth-container', { 45 | signInFlow: 'popup', 46 | signInSuccessUrl: window.location.href, 47 | signInOptions: [ 48 | firebase.auth.EmailAuthProvider.PROVIDER_ID, 49 | firebase.auth.GoogleAuthProvider.PROVIDER_ID, 50 | ], 51 | callbacks: { 52 | signInSuccessWithAuthResult: () => { 53 | window.location.href = '/' 54 | }, 55 | }, 56 | }) 57 | } 58 | }, [show]) 59 | return ( 60 | <> 61 | 64 | {show ?
: null} 65 | 66 | ) 67 | } 68 | 69 | const SignOut = () => { 70 | const [client] = useClient() 71 | const history = useHistory() 72 | 73 | return ( 74 | 84 | ) 85 | } 86 | 87 | const SyncToFirebase = () => { 88 | const [client] = useClient() 89 | const [currentUser] = useEntity({ identity: 'currentUser' }) 90 | const userId = currentUser.get('uid') 91 | const transactListener = React.useCallback( 92 | (changedDatoms) => { 93 | const cardinalityManyAttrs = new Set([':block/children', ':block/refs']) 94 | const localOnlyAttrs = new Set([':block/editing?', ':block/editing-starting-caret-index']) 95 | // Find the datoms that were changed more than once 96 | const numDatomChanges = changedDatoms.reduce( 97 | (acc, [id, attr]) => ({ ...acc, [id + attr]: (acc[id + attr] || 0) + 1 }), 98 | {}, 99 | ) 100 | // Only send one change to firebase per datom 101 | const datomsForFirebase = changedDatoms.filter( 102 | // eslint-disable-next-line no-unused-vars 103 | ([id, attr, _, __, isAdded]) => !(!isAdded && numDatomChanges[id + attr] > 1), 104 | ) 105 | datomsForFirebase.forEach(([id, attr, v, tx, isAdded]) => { 106 | if (!localOnlyAttrs.has(attr)) { 107 | const ref = firebase.database().ref( 108 | // This example uses firebase realtime database with the following rules. 109 | // { 110 | // "rules": { 111 | // "users": { 112 | // "$uid": { 113 | // ".read": "$uid === auth.uid", 114 | // ".write": "$uid === auth.uid" 115 | // } 116 | // } 117 | // } 118 | // } 119 | // Every user has a unique namespace with full read/write permission. 120 | // For single page apps like this we can write the raw datoms to this namespace. 121 | // Here we are generating a unique key for every datom. 122 | `users/${userId}/entities/${id}|${attr.replace('/', '|')}|${ 123 | // add the value to the key of cardinality many datoms since they are only unique when their value is included 124 | cardinalityManyAttrs.has(attr) ? v : '' 125 | }`, 126 | ) 127 | // eslint-disable-next-line no-unused-expressions 128 | isAdded ? ref.set([id, attr, v, tx, isAdded]) : ref.remove() 129 | } 130 | }) 131 | }, 132 | [userId], 133 | ) 134 | React.useEffect(() => { 135 | const softTransact = (tx) => { 136 | try { 137 | client.transactSilently(tx) 138 | } catch (er) { 139 | tx.forEach((txPart) => { 140 | try { 141 | client.transactSilent([txPart]) 142 | } catch (err) { 143 | // eslint-disable-next-line no-console 144 | console.warn(err, txPart) 145 | } 146 | }) 147 | } 148 | } 149 | // Homebase -> Firebase 150 | client.addTransactListener(transactListener) 151 | // Firebase -> Homebase 152 | const ref = firebase.database().ref(`users/${userId}/entities`) 153 | let txQueue = [] 154 | const debouncedTransactQueue = debounce(() => { 155 | softTransact(txQueue) 156 | txQueue = [] 157 | }, 300) 158 | const onAdd = (ds) => { 159 | txQueue.push(['add', ...ds.val()]) 160 | debouncedTransactQueue() 161 | } 162 | const onRetract = (ds) => { 163 | txQueue.push(['retract', ...ds.val()]) 164 | debouncedTransactQueue() 165 | } 166 | ref.on('child_added', onAdd) 167 | ref.on('child_removed', onRetract) 168 | ref.on('child_changed', onAdd) 169 | return () => { 170 | client.removeTransactListener() 171 | ref.off('child_added', onAdd) 172 | ref.off('child_removed', onRetract) 173 | ref.off('child_changed', onAdd) 174 | } 175 | }, [userId, client, transactListener]) 176 | return null 177 | } 178 | 179 | const LoadInitialData = ({ children }) => { 180 | const [client] = useClient() 181 | const [loading, setLoading] = React.useState(true) 182 | React.useEffect(() => { 183 | setLoading(true) 184 | window.emptyDB = client.dbToString() 185 | let currentUser 186 | const closeListener = firebase.auth().onAuthStateChanged((user) => { 187 | if (user) { 188 | currentUser = user 189 | client.transactSilently([{ currentUser: { identity: 'currentUser', uid: user.uid } }]) 190 | } 191 | }) 192 | async function init() { 193 | const res = await fetch('/edn/hn.edn') 194 | window.defaultDB = await res.text() 195 | setTimeout(() => { 196 | if (!currentUser) client.dbFromString(window.defaultDB) 197 | setLoading(false) 198 | }, 1000) 199 | } 200 | init() 201 | return closeListener 202 | }, [client]) 203 | if (loading) return 'Loading...' 204 | return children 205 | } 206 | 207 | const Header = () => ( 208 |
209 |

210 | Hombase React - Roam Demo 211 |

212 | 213 | Homebase↗️ 214 | 215 | 220 | GitHub↗️ 221 | 222 | 223 |
224 | ) 225 | 226 | const IntroBanner = () => ( 227 |
228 | This is a demo of the{' '} 229 | 235 | homebase-react 236 | {' '} 237 | state management library. It brings the same ClojureScript graph database used by Roam Research 238 | to React in a JS friendly way. 239 |
240 | ) 241 | 242 | const NotLoggedInBanner = () => { 243 | const [currentUser] = useEntity({ identity: 'currentUser' }) 244 | if (currentUser.get('uid')) return null 245 | return ( 246 |
247 | Changes will not be saved while signed out. 248 |
249 | ) 250 | } 251 | 252 | const config = { 253 | lookupHelpers: { 254 | block: { 255 | uid: { unique: 'identity' }, 256 | title: { unique: 'identity' }, 257 | children: { type: 'ref', cardinality: 'many' }, 258 | refs: { type: 'ref', cardinality: 'many' }, 259 | page: { type: 'ref', cardinality: 'one' }, 260 | }, 261 | }, 262 | } 263 | 264 | export default function App() { 265 | return ( 266 | 267 | 268 | 269 | 270 |
271 |
272 |
273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 |
284 |
285 |
286 |
287 |
288 | ) 289 | } 290 | -------------------------------------------------------------------------------- /examples/roam/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/roam/src/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import { PrismAsync as SyntaxHighlighter } from 'react-syntax-highlighter' 2 | // import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter' 3 | // import bash from 'refractor/lang/bash' 4 | // import clojure from 'refractor/lang/clojure' 5 | // import jsx from 'refractor/lang/jsx' 6 | import CodeBlockStyle from './CodeBlockStyle' 7 | 8 | // SyntaxHighlighter.registerLanguage('bash', bash) 9 | // SyntaxHighlighter.registerLanguage('clojure', clojure) 10 | // SyntaxHighlighter.registerLanguage('jsx', jsx) 11 | 12 | const rewriteLang = (lang) => { 13 | if (lang === 'js') return 'jsx' 14 | if (lang === 'javascript') return 'jsx' 15 | if (lang === 'clj') return 'clojure' 16 | if (lang === 'cljs') return 'clojure' 17 | if (lang === 'clojurescript') return 'clojure' 18 | return lang 19 | } 20 | 21 | export default function CodeBlock({ language = 'jsx', showLineNumbers = true, children }) { 22 | return ( 23 | 24 | 29 | {children} 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /examples/roam/src/components/CodeBlockStyle.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const CodeBlockStyle = styled.div` 4 | --code-background: #fafdff; 5 | --code-variable: #24292e; 6 | --code-keyword: #d73a49; 7 | --code-operator: #005cc5; 8 | --code-function: #6f42c1; 9 | --code-function-variable: #e36209; 10 | --code-parameter: #24292e; 11 | --code-number: #005cc5; 12 | --code-string: #032f62; 13 | --code-class-name: #22863a; 14 | --code-property-access: #005cc5; 15 | --code-attr-name: #005cc5; 16 | --code-regex: #24292e; 17 | --code-comment: #1b1f234d; 18 | --code-linenumber: #1b1f234d; 19 | 20 | position: relative; 21 | 22 | code { 23 | color: var(--code-variable); 24 | background: none; 25 | font-family: Hasklig, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 26 | text-align: left; 27 | white-space: pre; 28 | word-spacing: normal; 29 | word-break: normal; 30 | word-wrap: normal; 31 | line-height: 1.5; 32 | tab-size: 4; 33 | hyphens: none; 34 | } 35 | pre { 36 | color: var(--code-variable); 37 | background: var(--code-background); 38 | font-family: Hasklig, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 39 | text-align: left; 40 | white-space: pre; 41 | word-spacing: normal; 42 | word-break: normal; 43 | word-wrap: normal; 44 | line-height: 1.5; 45 | tab-size: 4; 46 | hyphens: none; 47 | overflow: auto; 48 | border-radius: 3px; 49 | border: 1px solid #d1d5db; 50 | } 51 | :not(pre) > code[class*=\language-\] { 52 | background: #282a36; 53 | padding: .1em; 54 | border-radius: .3em; 55 | white-space: normal; 56 | } 57 | 58 | .comment { 59 | color: var(--code-comment); 60 | } 61 | .prolog { 62 | color: var(--code-comment); 63 | } 64 | .doctype { 65 | color: var(--code-comment); 66 | } 67 | .cdata { 68 | color: var(--code-comment); 69 | } 70 | 71 | .linenumber { 72 | color: var(--code-linenumber); 73 | } 74 | 75 | .namespace { 76 | Opacity: .7; 77 | } 78 | 79 | .property { 80 | color: var(--code-regex); 81 | } 82 | .tag { 83 | color: var(--code-regex); 84 | } 85 | .constant { 86 | color: var(--code-regex); 87 | } 88 | .regex { 89 | color: var(--code-regex); 90 | } 91 | .deleted { 92 | color: var(--code-regex); 93 | } 94 | 95 | .boolean { 96 | color: var(--code-number); 97 | } 98 | .number { 99 | color: var(--code-number); 100 | } 101 | .selector { 102 | color: var(--code-string); 103 | } 104 | .attr-name { 105 | color: var(--code-attr-name); 106 | } 107 | .string { 108 | color: var(--code-string); 109 | } 110 | .char { 111 | color: var(--code-string); 112 | } 113 | .builtin { 114 | color: var(--code-string); 115 | } 116 | .inserted { 117 | color: var(--code-string); 118 | } 119 | 120 | .operator { 121 | color: var(--code-operator); 122 | } 123 | .entity { 124 | color: var(--code-variable); 125 | cursor: help; 126 | } 127 | .url { 128 | color: var(--code-variable); 129 | } 130 | .language-css .token.string { 131 | color: var(--code-variable); 132 | } 133 | .style .token.string { 134 | color: var(--code-variable); 135 | } 136 | .variable { 137 | color: var(--code-variable); 138 | } 139 | 140 | .atrule { 141 | color: var(--code-function); 142 | } 143 | .attr-value { 144 | color: var(--code-function); 145 | } 146 | .function { 147 | color: var(--code-function); 148 | } 149 | 150 | .function-variable { 151 | color: var(--code-function-variable); 152 | } 153 | 154 | .class-name { 155 | color: var(--code-class-name); 156 | } 157 | 158 | .parameter { 159 | color: var(--code-parameter); 160 | } 161 | .regex { 162 | color: var(--code-regex); 163 | } 164 | 165 | .property-access { 166 | color: var(--code-property-access); 167 | } 168 | 169 | .method { 170 | color: var(--code-function); 171 | } 172 | 173 | .script-punctuation { 174 | color: var(--code-keyword); 175 | } 176 | .keyword { 177 | color: var(--code-keyword); 178 | } 179 | 180 | .punctuation { 181 | color: var(--code-variable); 182 | } 183 | 184 | .important { 185 | color: var(--code-regex); 186 | font-weight: bold; 187 | } 188 | 189 | .bold { 190 | font-weight: bold; 191 | } 192 | .italic { 193 | font-style: italic; 194 | } 195 | ` 196 | 197 | export default CodeBlockStyle 198 | -------------------------------------------------------------------------------- /examples/roam/src/components/RoamMarkdown.js: -------------------------------------------------------------------------------- 1 | import { useEntity, useTransact } from 'homebase-react' 2 | import { nanoid } from 'nanoid' 3 | import React from 'react' 4 | import ReactMarkdownWithHTML from 'react-markdown/with-html' 5 | import { Link } from 'react-router-dom' 6 | import { TwitterTweetEmbed } from 'react-twitter-embed' 7 | import gfm from 'remark-gfm' 8 | import styled from 'styled-components' 9 | import CodeBlock from './CodeBlock' 10 | 11 | const TweetWrap = styled.div` 12 | display: flex; 13 | justify-content: center; 14 | & > div { 15 | width: 100%; 16 | display: flex; 17 | justify-content: center; 18 | } 19 | ` 20 | const matchTweetId = /twitter.com\/.*status\/([\d]+)/im 21 | const findTweetId = (text) => { 22 | const match = text.match(matchTweetId) 23 | return match && match[1] 24 | } 25 | 26 | const useFindOrCreatePage = (title, refBlockId) => { 27 | const [block] = useEntity({ block: { title } }) 28 | const [transact] = useTransact() 29 | React.useEffect(() => { 30 | if (!block?.get('id')) { 31 | transact([ 32 | { 33 | block: { 34 | id: -1, 35 | title, 36 | open: true, 37 | isNode: true, 38 | uid: nanoid(9), 39 | time: Date.now(), 40 | }, 41 | }, 42 | { 43 | block: { 44 | id: refBlockId, 45 | refs: -1, 46 | }, 47 | }, 48 | ]) 49 | } else if (refBlockId && !(block.get('_refs', 'id') || []).includes(refBlockId)) { 50 | transact([ 51 | { 52 | block: { 53 | id: refBlockId, 54 | refs: block.get('id'), 55 | }, 56 | }, 57 | ]) 58 | } 59 | }, [block, transact, title, refBlockId]) 60 | return [block] 61 | } 62 | 63 | const Tag = ({ title, blockId }) => { 64 | const [block] = useFindOrCreatePage(title, blockId) 65 | 66 | return ( 67 | e.stopPropagation()} 71 | > 72 | #{title} 73 | 74 | ) 75 | } 76 | 77 | const PageRef = ({ title, colon, blockId }) => { 78 | const [block] = useFindOrCreatePage(title, blockId) 79 | return colon ? ( 80 | e.stopPropagation()} 84 | > 85 | {title}: 86 | 87 | ) : ( 88 | 89 | [[ 90 | e.stopPropagation()} 94 | > 95 | {title} 96 | 97 | ]] 98 | 99 | ) 100 | } 101 | 102 | const BlockRef = ({ uid }) => { 103 | const [block] = useEntity({ block: { uid } }) 104 | return ( 105 | e.stopPropagation()}> 106 | {block?.get('string') || block?.get('title')} 107 | 108 | ) 109 | } 110 | 111 | const renderTextSection = (blockId) => (text, i) => { 112 | const tag = text.match(/^#\[\[(.+?)\]\]$|^#(.+?)$/s) 113 | if (tag) return 114 | const colonPageRef = text.match(/^(.+?)::/s) 115 | if (colonPageRef) return 116 | const pageRef = text.match(/(?!#)\[\[(.+?)\]\]/s) 117 | if (pageRef) return 118 | const blockRef = text.match(/\(\((.+?)\)\)/s) 119 | if (blockRef) return 120 | const hr = text.match(/^:hiccup \[:hr\]$/s) 121 | if (hr) return
122 | return ( 123 | 124 | {text} 125 | 126 | ) 127 | } 128 | 129 | const renderers = (blockId) => ({ 130 | link: ({ href, children }) => ( 131 | 132 | {children} 133 | 134 | ), 135 | // Swap bold and italic to match Roam 136 | strong: ({ children }) => {children}, 137 | emphasis: ({ children }) => {children}, 138 | heading: ({ level, children }) => { 139 | const Hx = `h${level}` 140 | return {children} 141 | }, 142 | blockquote: ({ children }) => ( 143 |
{children}
144 | ), 145 | inlineCode: ({ children }) => ( 146 | 147 | {children} 148 | 149 | ), 150 | code: ({ language, value }) => { 151 | if (value) return {value} 152 | return null 153 | }, 154 | paragraph: ({ children }) => children, 155 | text: ({ value }) => { 156 | // Adds support for Roam style markup 157 | // NOTE: these regexs are a quick hack for this demo. Extend the gfm parser in a prod setting 158 | const sections = value 159 | .split( 160 | /(^.+?::)|(#\[\[.+?\]\])|(\s|^)(#.+?)(\s|\n|\n\r|$)|((?!#)\[\[.+?\]\])|(\(\(.+?\)\))|(^:hiccup \[:hr\]$)/gs, 161 | ) 162 | .filter(Boolean) 163 | const roamifiedText = sections.map(renderTextSection(blockId)) 164 | const tweetId = findTweetId(value) 165 | if (tweetId) { 166 | return ( 167 | <> 168 | {roamifiedText} 169 | 170 | 171 | 172 | 173 | ) 174 | } 175 | return roamifiedText 176 | }, 177 | }) 178 | 179 | function RoamMarkdown({ children, blockId }) { 180 | return ( 181 | 182 | {children} 183 | 184 | ) 185 | } 186 | 187 | const RoamMarkdownMemo = React.memo(RoamMarkdown) 188 | 189 | export default RoamMarkdownMemo 190 | -------------------------------------------------------------------------------- /examples/roam/src/components/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useLocation } from 'react-router-dom' 3 | 4 | export default function ScrollToTop() { 5 | const { pathname } = useLocation() 6 | 7 | useEffect(() => { 8 | window.scrollTo(0, 0) 9 | }, [pathname]) 10 | 11 | return null 12 | } 13 | -------------------------------------------------------------------------------- /examples/roam/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/roam/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import './index.css' 5 | import reportWebVitals from './reportWebVitals' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ) 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals() 18 | -------------------------------------------------------------------------------- /examples/roam/src/pages/Blocks.js: -------------------------------------------------------------------------------- 1 | import { useQuery, useTransact } from 'homebase-react' 2 | import { nanoid } from 'nanoid' 3 | import React from 'react' 4 | import Block from '../components/Block' 5 | 6 | const Blocks = () => { 7 | const [transact] = useTransact() 8 | const [blocks] = useQuery({ 9 | $find: 'block', 10 | $where: { block: { isNode: true } }, 11 | }) 12 | 13 | // Add a page for the current date if none exists 14 | // Yes, this is hacky :P 15 | React.useEffect(() => { 16 | setTimeout(() => { 17 | try { 18 | transact([ 19 | { 20 | block: { 21 | uid: nanoid(9), 22 | open: true, 23 | isNode: true, 24 | title: new Date().toDateString(), 25 | time: Date.now(), 26 | }, 27 | }, 28 | ]) 29 | } catch (err) { 30 | // This is expected to fail due to the unique 31 | // constraint on the block.title attribute 32 | return false 33 | } 34 | return true 35 | }, 3000) 36 | }, [transact]) 37 | 38 | return blocks 39 | .sort((a, b) => (a.get('time') > b.get('time') ? -1 : 1)) 40 | .map((block) => ) 41 | } 42 | 43 | export default Blocks 44 | -------------------------------------------------------------------------------- /examples/roam/src/pages/PageUid.js: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom' 2 | import Block from '../components/Block' 3 | 4 | const PageUid = () => { 5 | const { uid } = useParams() 6 | return 7 | } 8 | 9 | export default PageUid 10 | -------------------------------------------------------------------------------- /examples/roam/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry) 5 | getFID(onPerfEntry) 6 | getFCP(onPerfEntry) 7 | getLCP(onPerfEntry) 8 | getTTFB(onPerfEntry) 9 | }) 10 | } 11 | } 12 | 13 | export default reportWebVitals 14 | -------------------------------------------------------------------------------- /examples/roam/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | 7 | -------------------------------------------------------------------------------- /examples/roam/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /examples/todo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /examples/todo/README.md: -------------------------------------------------------------------------------- 1 | # Todo example 2 | 3 | Heavily adopted from `create-react-app` bootstrapping, here's a our Todo example bundled in a React application. 4 | 5 | ## Live demo 6 | - https://homebase-example-todo.vercel.app 7 | 8 | ## Installation 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ## Run it 14 | ``` 15 | yarn start 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebase-react-todo-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "homebase-react": "^0.7.0", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-scripts": "4.0.0", 13 | "web-vitals": "^0.2.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/todo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/todo/public/favicon.ico -------------------------------------------------------------------------------- /examples/todo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Homebase Todo Example 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/todo/src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | padding: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } -------------------------------------------------------------------------------- /examples/todo/src/App.js: -------------------------------------------------------------------------------- 1 | import { HomebaseProvider, useEntity, useQuery, useTransact } from 'homebase-react' 2 | import React from 'react' 3 | import './App.css' 4 | 5 | export default function App() { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | const config = { 14 | // Lookup helpers are used to enforce 15 | // unique constraints and relationships. 16 | lookupHelpers: { 17 | project: { name: { unique: 'identity' } }, 18 | todo: { 19 | // refs are relationships 20 | project: { type: 'ref' }, 21 | owner: { type: 'ref' } 22 | } 23 | }, 24 | // Initial data let's you conveniently transact some 25 | // starting data on DB creation to hydrate your components. 26 | initialData: [ 27 | { 28 | todoFilter: { 29 | // identity is a special unique attribute for user generated ids 30 | // E.g. todoFilters are settings that should be easy to lookup by their identity 31 | identity: 'todoFilters', 32 | showCompleted: true 33 | } 34 | }, { 35 | user: { 36 | // Negative numbers can be used as temporary ids in a transaction. 37 | // Use them to relate multiple entities together at once. 38 | id: -1, 39 | name: 'Stella' 40 | } 41 | }, { 42 | user: { 43 | id: -2, 44 | name: 'Arpegius' 45 | } 46 | }, { 47 | project: { 48 | id: -3, 49 | name: 'Make it' 50 | } 51 | }, { 52 | project: { 53 | id: -4, 54 | name: 'Do it' 55 | } 56 | }, { 57 | todo: { 58 | name: 'Fix ship', 59 | owner: -1, 60 | project: -3, 61 | isCompleted: true, 62 | createdAt: new Date('2003/11/10') 63 | } 64 | }, { 65 | todo: { 66 | name: 'Go home', 67 | owner: -2, 68 | project: -4, 69 | createdAt: new Date('2003/11/10') 70 | } 71 | } 72 | ] 73 | } 74 | 75 | const Todos = () => { 76 | return ( 77 |
78 | 79 | 80 | 81 |
82 | ) 83 | } 84 | 85 | const NewTodo = () => { 86 | const [transact] = useTransact() 87 | return ( 88 |
{ 89 | e.preventDefault() 90 | transact([{ 91 | todo: { 92 | name: e.target.elements['todo-name'].value, 93 | createdAt: new Date() 94 | } 95 | }]) 96 | e.target.reset() 97 | }}> 98 | 106 |   107 | 108 |
109 | ) 110 | } 111 | 112 | const TodoList = () => { 113 | const [filters] = useEntity({ identity: 'todoFilters' }) 114 | const [todos] = useQuery({ 115 | $find: 'todo', 116 | $where: { todo: { name: '$any' } } 117 | }) 118 | return ( 119 |
120 | {todos.filter(todo => { 121 | if (!filters.get('showCompleted') && todo.get('isCompleted')) return false 122 | if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) return false 123 | if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false 124 | return true 125 | }).sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1) 126 | .map(todo => )} 127 |
128 | ) 129 | } 130 | 131 | // PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity 132 | // this component stays disconnected from the useQuery in the parent TodoList. 133 | // useEntity creates a separate scope for every Todo so changes to TodoList 134 | // or sibling Todos don't trigger unnecessary re-renders. 135 | const Todo = React.memo(({ id }) => { 136 | const [todo] = useEntity(id) 137 | return ( 138 |
139 |
140 | 141 | 142 |
143 |
144 | 145 |  ·  146 | 147 |  ·  148 | 149 |
150 | 151 | {todo.get('createdAt').toLocaleString()} 152 | 153 |
154 | ) 155 | }) 156 | 157 | const TodoCheck = ({ todo }) => { 158 | const [transact] = useTransact() 159 | return ( 160 | transact([{ todo: { id: todo.get('id'), isCompleted: e.target.checked } }])} 165 | /> 166 | ) 167 | } 168 | 169 | const TodoName = ({ todo }) => { 170 | const [transact] = useTransact() 171 | return ( 172 | transact([{ todo: { id: todo.get('id'), name: e.target.value }}])} 179 | /> 180 | ) 181 | } 182 | 183 | const TodoProject = ({ todo }) => { 184 | const [transact] = useTransact() 185 | return ( 186 | transact([{ todo: { id: todo.get('id'), project }}])} 191 | /> 192 | ) 193 | } 194 | 195 | const TodoOwner = ({ todo }) => { 196 | const [transact] = useTransact() 197 | return ( 198 | transact([{ todo: { id: todo.get('id'), owner }}])} 203 | /> 204 | ) 205 | } 206 | 207 | const TodoDelete = ({ todo }) => { 208 | const [transact] = useTransact() 209 | return ( 210 | 213 | ) 214 | } 215 | 216 | const TodoFilters = () => { 217 | const [filters] = useEntity({ identity: 'todoFilters' }) 218 | const [transact] = useTransact() 219 | return ( 220 |
221 | 228 |  ·  229 | transact([{ todoFilter: { id: filters.get('id'), project }}])} 234 | /> 235 |  ·  236 | transact([{ todoFilter: { id: filters.get('id'), owner }}])} 241 | /> 242 |
243 | ) 244 | } 245 | 246 | const EntitySelect = React.memo(({ label, entityType, value, onChange }) => { 247 | const [entities] = useQuery({ 248 | $find: entityType, 249 | $where: { [entityType]: { name: '$any' } } 250 | }) 251 | return ( 252 | 266 | ) 267 | }) -------------------------------------------------------------------------------- /examples/todo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/README.md: -------------------------------------------------------------------------------- 1 | # Firebase todo example 2 | 3 | Heavily adopted from `create-react-app` bootstrapping, here's a our Todo example with a Firebase backend bundled in a Typescript React application. 4 | 5 | ## Live demo 6 | - https://homebase-example-ts-firebase-todo.vercel.app 7 | 8 | ## Installation 9 | ``` 10 | yarn install 11 | ``` 12 | 13 | ## Run it 14 | ``` 15 | yarn start 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-todo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.53", 12 | "@types/react-dom": "^16.9.8", 13 | "firebase": "^8.1.1", 14 | "firebaseui": "^4.7.1", 15 | "homebase-react": "^0.7.0", 16 | "react": "^17.0.1", 17 | "react-dom": "^17.0.1", 18 | "react-scripts": "4.0.0", 19 | "typescript": "^4.0.3", 20 | "web-vitals": "^0.2.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/examples/typescript-firebase-todo/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); -------------------------------------------------------------------------------- /examples/typescript-firebase-todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | moduleFileExtensions: ["js", "jsx", "ts", "tsx"], 4 | transform: { 5 | '^.+\\.(ts|tsx)?$': 'ts-jest', 6 | "^.+\\.(js|jsx)$": "babel-jest", 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebase-react", 3 | "description": "A graph database for React.", 4 | "version": "0.0.0-development", 5 | "license": "MIT", 6 | "homepage": "https://homebase.io", 7 | "main": "./dist/js/homebase.react.js", 8 | "private": false, 9 | "scripts": { 10 | "dev": "shadow-cljs watch dev & babel src/dev/homebase/dev/example/js --out-dir src/dev/homebase/dev/example/js_compiled --watch && kill $!", 11 | "build": "rm -rf dist && shadow-cljs release npm && yarn bundle-ts", 12 | "build:dev": "rm -rf dist && shadow-cljs compile npm && yarn bundle-ts", 13 | "test:js": "yarn build && jest src/test/* && yarn tsd", 14 | "test:js:dev": "yarn build:dev && jest src/test/* && yarn tsd", 15 | "test:ts": "yarn bundle-ts && yarn tsd", 16 | "test:cljs": "shadow-cljs compile test && node out/node-tests.js", 17 | "test:cljs:watch": "shadow-cljs watch test-autorun", 18 | "test": "yarn test:cljs && yarn test:js", 19 | "test:dev": "yarn test:cljs && yarn test:js:dev", 20 | "report": "rm -rf dist && shadow-cljs run shadow.cljs.build-report npm report.html", 21 | "semantic-release": "semantic-release", 22 | "_postinstall": "husky install", 23 | "prepublish": "pinst --disable", 24 | "postpublish": "pinst --enable", 25 | "bundle-ts": "rsync -r types dist" 26 | }, 27 | "config": { 28 | "commitizen": { 29 | "path": "./node_modules/cz-conventional-changelog" 30 | } 31 | }, 32 | "peerDependencies": { 33 | "react": ">=16.8.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "7.11.6", 37 | "@babel/core": "^7.12.17", 38 | "@babel/plugin-transform-modules-commonjs": "7.12.1", 39 | "@babel/plugin-transform-react-jsx": "7.10.4", 40 | "@babel/preset-react": "^7.12.13", 41 | "@commitlint/cli": "^11.0.0", 42 | "@commitlint/config-conventional": "^11.0.0", 43 | "@peculiar/webcrypto": "^1.1.7", 44 | "@semantic-release/changelog": "5.0.1", 45 | "@semantic-release/commit-analyzer": "8.0.1", 46 | "@semantic-release/git": "9.0.0", 47 | "@semantic-release/github": "7.1.1", 48 | "@semantic-release/npm": "7.0.6", 49 | "@semantic-release/release-notes-generator": "9.0.1", 50 | "@testing-library/jest-dom": "^5.11.9", 51 | "@testing-library/react": "^11.2.5", 52 | "babel-eslint": "^10.1.0", 53 | "babel-runtime": "6.26.0", 54 | "commitizen": "^4.2.2", 55 | "create-react-class": "15.6.3", 56 | "cz-conventional-changelog": "^3.3.0", 57 | "enzyme": "3.11.0", 58 | "enzyme-adapter-react-16": "1.15.5", 59 | "eslint": "^7.20.0", 60 | "eslint-config-airbnb": "^18.2.1", 61 | "eslint-config-prettier": "^7.2.0", 62 | "eslint-plugin-import": "^2.22.1", 63 | "eslint-plugin-jest": "^24.1.3", 64 | "eslint-plugin-jest-dom": "^3.6.5", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-prettier": "^3.3.1", 67 | "eslint-plugin-react": "^7.22.0", 68 | "eslint-plugin-react-hooks": "^4.2.0", 69 | "firebase": "^8.0.2", 70 | "firebaseui": "^4.7.1", 71 | "global-jsdom": "^8.1.0", 72 | "highlight.js": "10.4.1", 73 | "hoist-non-react-statics": "^3.3.0", 74 | "husky": "5.0.0-beta.0", 75 | "jest": "26.6.0", 76 | "jest-performance-testing": "^1.0.0", 77 | "jsdom": "^16.6.0", 78 | "marked": "2.0.0", 79 | "pinst": "2.0.0", 80 | "prettier": "^2.2.1", 81 | "react": "16.14.0", 82 | "react-component-benchmark": "0.0.4", 83 | "react-dom": "16.14.0", 84 | "react-grid-layout": "^0.16.6", 85 | "react-icons": "^2.2.7", 86 | "react-performance-testing": "^1.2.3", 87 | "react-test-renderer": "^16.14.0", 88 | "reakit": "^0.11.1", 89 | "semantic-release": "17.2.3", 90 | "semantic-release-cli": "5.4.0", 91 | "shadow-cljs": "2.11.4", 92 | "ts-jest": "^26.4.4", 93 | "tsd": "^0.14.0", 94 | "typescript": "^4.1.0" 95 | }, 96 | "types": "./dist/types", 97 | "keywords": [ 98 | "react", 99 | "relational", 100 | "database", 101 | "datalog", 102 | "state", 103 | "graph" 104 | ], 105 | "repository": { 106 | "type": "git", 107 | "url": "https://github.com/homebaseio/homebase-react.git" 108 | }, 109 | "bugs": { 110 | "url": "https://github.com/homebaseio/homebase-react/issues" 111 | }, 112 | "publishConfig": { 113 | "access": "public", 114 | "registry": "https://registry.npmjs.org/" 115 | }, 116 | "author": "Chris Smothers (https://homebase.io)", 117 | "contributors": [ 118 | "JB Rubinovitz (https://homebase.io)" 119 | ], 120 | "dependencies": {} 121 | } 122 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | io.homebase 6 | homebase-react 7 | 0.1.1 8 | homebase-react 9 | Use a datalog DB to manage react application state 10 | https://github.com/homebaseio/homebase-react 11 | 12 | Homebase 13 | https://homebase.io 14 | 15 | 16 | 17 | MIT 18 | https://opensource.org/licenses/MIT 19 | 20 | 21 | 22 | https://github.com/homebaseio/homebase-react.git 23 | scm:git:git://github.com/homebaseio/homebase-react.git 24 | scm:git:ssh://git@github.com:homebaseio/homebase-react.git 25 | HEAD 26 | 27 | 28 | src/main 29 | 30 | 31 | 32 | org.clojure 33 | clojure 34 | 1.10.3 35 | 36 | 37 | datascript 38 | datascript 39 | 1.0.7 40 | 41 | 42 | reagent 43 | reagent 44 | 1.0.0-alpha2 45 | 46 | 47 | inflections 48 | inflections 49 | 0.13.2 50 | 51 | 52 | io.homebase 53 | datalog-console 54 | 0.2.2 55 | 56 | 57 | nano-id 58 | nano-id 59 | 1.0.0 60 | 61 | 62 | camel-snake-kebab 63 | camel-snake-kebab 64 | 0.4.2 65 | 66 | 67 | 68 | 69 | clojars 70 | https://repo.clojars.org/ 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/images/datalog_console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/public/images/datalog_console.png -------------------------------------------------------------------------------- /public/images/enable_chrome_formatters_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/public/images/enable_chrome_formatters_1.png -------------------------------------------------------------------------------- /public/images/enable_chrome_formatters_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/public/images/enable_chrome_formatters_2.png -------------------------------------------------------------------------------- /public/images/enable_chrome_formatters_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/public/images/enable_chrome_formatters_3.png -------------------------------------------------------------------------------- /public/images/logo-blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homebaseio/homebase-react/15f018d6410ba88233d7e784ea2393a540129844/public/images/logo-blk.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | homebase examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:deps {:aliases [:dev]} 3 | :dev-http {3000 "public"} 4 | :nrepl {:port 3333} 5 | :builds 6 | {:dev {:target :browser 7 | :output-dir "public/js" 8 | :asset-path "/js" 9 | :compiler-options {:devcards :true 10 | :externs ["datascript/externs.js"] 11 | :output-feature-set :es6} 12 | :modules {:main {:init-fn homebase.dev.example.core/init}} 13 | :js-options {:resolve {"devcards-marked" {:target :npm :require "marked"} 14 | "devcards-syntax-highlighter" {:target :npm :require "highlight.js"}}}} 15 | :test {:target :node-test 16 | :output-to "out/node-tests.js" 17 | :ns-regexp "-test$" 18 | :autorun false} 19 | :test-autorun {:target :node-test 20 | :output-to "out/node-tests.js" 21 | :ns-regexp "-test$" 22 | :autorun true} 23 | :npm {:target :node-library 24 | :output-to "dist/js/homebase.react.js" 25 | :output-dir "dist/js" 26 | :exports {:HomebaseProvider homebase.react/HomebaseProvider 27 | :useClient homebase.react/useClient 28 | :useTransact homebase.react/useTransact 29 | :useEntity homebase.react/useEntity 30 | :useQuery homebase.react/useQuery} 31 | :compiler-options {:optimizations :advanced 32 | :externs ["datascript/externs.js"] 33 | :pseudo-names false 34 | :pretty-print false 35 | :output-wrapper false 36 | :source-map false}}}} 37 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.core 2 | {:no-doc true} 3 | (:require 4 | ["highlight.js" :as highlight] 5 | ["marked" :as marked] 6 | [cljsjs.react] 7 | [cljsjs.react.dom] 8 | [reagent.core] 9 | [devcards.core :as dc] 10 | [homebase.dev.example.react.array] 11 | [homebase.dev.example.react.counter] 12 | [homebase.dev.example.react.todo] 13 | [homebase.dev.example.react.todo-firebase] 14 | [homebase.dev.example.reagent])) 15 | 16 | (js/goog.exportSymbol "marked" marked) 17 | (js/goog.exportSymbol "DevcardsMarked" marked) 18 | (js/goog.exportSymbol "highlight" highlight) 19 | (js/goog.exportSymbol "DevcardsSyntaxHighlighter" highlight) 20 | 21 | (defn init [] 22 | (dc/start-devcard-ui!)) 23 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js/array.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const { HomebaseProvider, useTransact, useEntity } = window.homebase.react 4 | 5 | const config = { 6 | lookupHelpers: { 7 | store: { 8 | items: { type: 'ref', cardinality: 'many' }, 9 | }, 10 | item: { 11 | date: { type: 'ref', cardinality: 'one' }, 12 | }, 13 | }, 14 | initialData: [ 15 | { 16 | store: { 17 | identity: 'store 1', 18 | items: [ 19 | { item: { name: 'item 1' } }, 20 | { item: { name: 'item 2' } }, 21 | { item: { name: 'item 3' } }, 22 | { item: { name: 'item 4' } }, 23 | { item: { name: 'item 5', date: { year: 2021, month: 1, day: 3 } } }, 24 | ], 25 | }, 26 | }, 27 | ], 28 | } 29 | 30 | export const App = () => ( 31 | 32 | 33 | 34 | ) 35 | 36 | const Items = () => { 37 | const [store] = useEntity({ identity: 'store 1' }) 38 | const [transact] = useTransact() 39 | 40 | // Try opening the console in Chrome with custom formatters enabled 41 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 42 | console.log(store) 43 | 44 | let newI = null 45 | const onDragOver = React.useCallback((e) => { 46 | e.preventDefault() 47 | newI = parseInt(e.target.dataset.index) 48 | }) 49 | 50 | const reorder = React.useCallback( 51 | (id, orderMin, orderMax) => { 52 | const order = (orderMin + orderMax) / 2.0 53 | transact([{ 'homebase.array': { id, order } }]) 54 | }, 55 | [transact], 56 | ) 57 | 58 | return ( 59 |
60 | {store.get('items').map((item, i) => ( 61 |
68 | reorder( 69 | item.get('id'), 70 | (newI > 0 && store.get('items', newI - 1, 'order')) || 0, 71 | store.get('items', newI, 'order'), 72 | ) 73 | } 74 | > 75 | ↕ {item.get('ref', 'name')}   76 | {item.get('ref', 'date', 'year')} 77 |
78 | ))} 79 |
80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js/counter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const { HomebaseProvider, useTransact, useEntity } = window.homebase.react 3 | 4 | const config = { 5 | initialData: [{ 6 | counter: { 7 | identity: 'counter', 8 | count: 0 9 | } 10 | }] 11 | } 12 | 13 | export const App = () => ( 14 | 15 | 16 | 17 | ) 18 | 19 | const Counter = () => { 20 | const [counter] = useEntity({ identity: 'counter' }) 21 | const [transact] = useTransact() 22 | // Try opening the console in Chrome with custom formatters enabled 23 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 24 | console.log(counter) 25 | return ( 26 |
27 | Count: {counter.get('count')} 28 |
29 | 37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js/todo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const { HomebaseProvider, useTransact, useQuery, useEntity } = window.homebase.react 4 | 5 | export const App = () => ( 6 | 7 | 8 | 9 | ) 10 | 11 | const config = { 12 | // Lookup helpers are used to enforce 13 | // unique constraints and relationships. 14 | lookupHelpers: { 15 | project: { name: { unique: 'identity' } }, 16 | todo: { 17 | // refs are relationships 18 | project: { type: 'ref' }, 19 | owner: { type: 'ref' }, 20 | }, 21 | }, 22 | // Initial data let's you conveniently transact some 23 | // starting data on DB creation to hydrate your components. 24 | initialData: [ 25 | { 26 | todoFilter: { 27 | // identity is a special unique attribute for user generated ids 28 | // E.g. todoFilters are settings that should be easy to lookup by their identity 29 | identity: 'todoFilters', 30 | showCompleted: true, 31 | }, 32 | }, 33 | { 34 | user: { 35 | // Negative numbers can be used as temporary ids in a transaction. 36 | // Use them to relate multiple entities together at once. 37 | id: -1, 38 | name: 'Stella', 39 | }, 40 | }, 41 | { 42 | user: { 43 | id: -2, 44 | name: 'Arpegius', 45 | }, 46 | }, 47 | { 48 | project: { 49 | id: -3, 50 | name: 'Make it', 51 | }, 52 | }, 53 | { 54 | project: { 55 | id: -4, 56 | name: 'Do it', 57 | }, 58 | }, 59 | { 60 | todo: { 61 | name: 'Fix ship', 62 | owner: -1, 63 | project: -3, 64 | isCompleted: true, 65 | createdAt: new Date('2003/11/10'), 66 | }, 67 | }, 68 | { 69 | todo: { 70 | name: 'Go home', 71 | owner: -2, 72 | project: -4, 73 | createdAt: new Date('2003/11/10'), 74 | }, 75 | }, 76 | ], 77 | } 78 | 79 | const Todos = () => ( 80 |
81 | 82 | 83 | 84 |
85 | ) 86 | 87 | const NewTodo = () => { 88 | const [transact] = useTransact() 89 | return ( 90 |
{ 92 | e.preventDefault() 93 | transact([ 94 | { 95 | todo: { 96 | name: e.target.elements['todo-name'].value, 97 | createdAt: new Date(), 98 | }, 99 | }, 100 | ]) 101 | e.target.reset() 102 | }} 103 | > 104 | 112 |   113 | 114 |
115 | ) 116 | } 117 | 118 | const TodoList = () => { 119 | const [filters] = useEntity({ identity: 'todoFilters' }) 120 | const [todos] = useQuery({ 121 | $find: 'todo', 122 | $where: { todo: { name: '$any' } }, 123 | }) 124 | return ( 125 |
126 | {todos 127 | .filter((todo) => { 128 | if (!filters.get('showCompleted') && todo.get('isCompleted')) return false 129 | if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) 130 | return false 131 | if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false 132 | return true 133 | }) 134 | .sort((a, b) => (a.get('createdAt') > b.get('createdAt') ? -1 : 1)) 135 | .map((todo) => ( 136 | 137 | ))} 138 |
139 | ) 140 | } 141 | 142 | // PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity 143 | // this component stays disconnected from the useQuery in the parent TodoList. 144 | // useEntity creates a separate scope for every Todo so changes to TodoList 145 | // or sibling Todos don't trigger unnecessary re-renders. 146 | const Todo = React.memo(({ id }) => { 147 | const [todo] = useEntity(id) 148 | // Try opening the console in Chrome with custom formatters enabled 149 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 150 | console.log(todo) 151 | return ( 152 |
153 |
156 | 157 | 158 |
159 |
160 | 161 |  ·  162 | 163 |  ·  164 | 165 |
166 | {todo.get('createdAt')?.toLocaleString()} 167 |
168 | ) 169 | }) 170 | 171 | const TodoCheck = ({ todo }) => { 172 | const [transact] = useTransact() 173 | return ( 174 | transact([{ todo: { id: todo.get('id'), isCompleted: e.target.checked } }])} 179 | /> 180 | ) 181 | } 182 | 183 | const TodoName = ({ todo }) => { 184 | const [transact] = useTransact() 185 | return ( 186 | transact([{ todo: { id: todo.get('id'), name: e.target.value } }])} 196 | /> 197 | ) 198 | } 199 | 200 | const TodoProject = ({ todo }) => { 201 | const [transact] = useTransact() 202 | return ( 203 | transact([{ todo: { id: todo.get('id'), project } }])} 208 | /> 209 | ) 210 | } 211 | 212 | const TodoOwner = ({ todo }) => { 213 | const [transact] = useTransact() 214 | return ( 215 | transact([{ todo: { id: todo.get('id'), owner } }])} 220 | /> 221 | ) 222 | } 223 | 224 | const TodoDelete = ({ todo }) => { 225 | const [transact] = useTransact() 226 | return 227 | } 228 | 229 | const TodoFilters = () => { 230 | const [filters] = useEntity({ identity: 'todoFilters' }) 231 | const [transact] = useTransact() 232 | return ( 233 |
234 | 244 |  ·  245 | transact([{ todoFilter: { id: filters.get('id'), project } }])} 250 | /> 251 |  ·  252 | transact([{ todoFilter: { id: filters.get('id'), owner } }])} 257 | /> 258 |
259 | ) 260 | } 261 | 262 | const EntitySelect = React.memo(({ label, entityType, value, onChange }) => { 263 | const [entities] = useQuery({ 264 | $find: entityType, 265 | $where: { [entityType]: { name: '$any' } }, 266 | }) 267 | return ( 268 | 283 | ) 284 | }) 285 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js_compiled/array.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.App = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 11 | 12 | const { 13 | HomebaseProvider, 14 | useTransact, 15 | useEntity 16 | } = window.homebase.react; 17 | const config = { 18 | lookupHelpers: { 19 | store: { 20 | items: { 21 | type: 'ref', 22 | cardinality: 'many' 23 | } 24 | }, 25 | item: { 26 | date: { 27 | type: 'ref', 28 | cardinality: 'one' 29 | } 30 | } 31 | }, 32 | initialData: [{ 33 | store: { 34 | identity: 'store 1', 35 | items: [{ 36 | item: { 37 | name: 'item 1' 38 | } 39 | }, { 40 | item: { 41 | name: 'item 2' 42 | } 43 | }, { 44 | item: { 45 | name: 'item 3' 46 | } 47 | }, { 48 | item: { 49 | name: 'item 4' 50 | } 51 | }, { 52 | item: { 53 | name: 'item 5', 54 | date: { 55 | year: 2021, 56 | month: 1, 57 | day: 3 58 | } 59 | } 60 | }] 61 | } 62 | }] 63 | }; 64 | 65 | const App = () => /*#__PURE__*/_react.default.createElement(HomebaseProvider, { 66 | config: config 67 | }, /*#__PURE__*/_react.default.createElement(Items, null)); 68 | 69 | exports.App = App; 70 | 71 | const Items = () => { 72 | const [store] = useEntity({ 73 | identity: 'store 1' 74 | }); 75 | const [transact] = useTransact(); // Try opening the console in Chrome with custom formatters enabled 76 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 77 | 78 | console.log(store); 79 | let newI = null; 80 | 81 | const onDragOver = _react.default.useCallback(e => { 82 | e.preventDefault(); 83 | newI = parseInt(e.target.dataset.index); 84 | }); 85 | 86 | const reorder = _react.default.useCallback((id, orderMin, orderMax) => { 87 | const order = (orderMin + orderMax) / 2.0; 88 | transact([{ 89 | 'homebase.array': { 90 | id, 91 | order 92 | } 93 | }]); 94 | }, [transact]); 95 | 96 | return /*#__PURE__*/_react.default.createElement("div", null, store.get('items').map((item, i) => /*#__PURE__*/_react.default.createElement("div", { 97 | key: item.get('ref', 'id'), 98 | style: { 99 | cursor: 'move' 100 | }, 101 | "data-index": i, 102 | draggable: true, 103 | onDragOver: onDragOver, 104 | onDragEnd: e => reorder(item.get('id'), newI > 0 && store.get('items', newI - 1, 'order') || 0, store.get('items', newI, 'order')) 105 | }, "\u2195 ", item.get('ref', 'name'), " \xA0", /*#__PURE__*/_react.default.createElement("small", null, item.get('ref', 'date', 'year'))))); 106 | }; -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js_compiled/counter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.App = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 11 | 12 | const { 13 | HomebaseProvider, 14 | useTransact, 15 | useEntity 16 | } = window.homebase.react; 17 | const config = { 18 | initialData: [{ 19 | counter: { 20 | identity: 'counter', 21 | count: 0 22 | } 23 | }] 24 | }; 25 | 26 | const App = () => /*#__PURE__*/_react.default.createElement(HomebaseProvider, { 27 | config: config 28 | }, /*#__PURE__*/_react.default.createElement(Counter, null)); 29 | 30 | exports.App = App; 31 | 32 | const Counter = () => { 33 | const [counter] = useEntity({ 34 | identity: 'counter' 35 | }); 36 | const [transact] = useTransact(); // Try opening the console in Chrome with custom formatters enabled 37 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 38 | 39 | console.log(counter); 40 | return /*#__PURE__*/_react.default.createElement("div", null, "Count: ", counter.get('count'), /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("button", { 41 | onClick: () => transact([{ 42 | counter: { 43 | id: counter.get('id'), 44 | count: counter.get('count') + 1 45 | } 46 | }]) 47 | }, "Increment"))); 48 | }; -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/js_compiled/todo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.App = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 11 | 12 | const { 13 | HomebaseProvider, 14 | useTransact, 15 | useQuery, 16 | useEntity 17 | } = window.homebase.react; 18 | 19 | const App = () => /*#__PURE__*/_react.default.createElement(HomebaseProvider, { 20 | config: config 21 | }, /*#__PURE__*/_react.default.createElement(Todos, null)); 22 | 23 | exports.App = App; 24 | const config = { 25 | // Lookup helpers are used to enforce 26 | // unique constraints and relationships. 27 | lookupHelpers: { 28 | project: { 29 | name: { 30 | unique: 'identity' 31 | } 32 | }, 33 | todo: { 34 | // refs are relationships 35 | project: { 36 | type: 'ref' 37 | }, 38 | owner: { 39 | type: 'ref' 40 | } 41 | } 42 | }, 43 | // Initial data let's you conveniently transact some 44 | // starting data on DB creation to hydrate your components. 45 | initialData: [{ 46 | todoFilter: { 47 | // identity is a special unique attribute for user generated ids 48 | // E.g. todoFilters are settings that should be easy to lookup by their identity 49 | identity: 'todoFilters', 50 | showCompleted: true 51 | } 52 | }, { 53 | user: { 54 | // Negative numbers can be used as temporary ids in a transaction. 55 | // Use them to relate multiple entities together at once. 56 | id: -1, 57 | name: 'Stella' 58 | } 59 | }, { 60 | user: { 61 | id: -2, 62 | name: 'Arpegius' 63 | } 64 | }, { 65 | project: { 66 | id: -3, 67 | name: 'Make it' 68 | } 69 | }, { 70 | project: { 71 | id: -4, 72 | name: 'Do it' 73 | } 74 | }, { 75 | todo: { 76 | name: 'Fix ship', 77 | owner: -1, 78 | project: -3, 79 | isCompleted: true, 80 | createdAt: new Date('2003/11/10') 81 | } 82 | }, { 83 | todo: { 84 | name: 'Go home', 85 | owner: -2, 86 | project: -4, 87 | createdAt: new Date('2003/11/10') 88 | } 89 | }] 90 | }; 91 | 92 | const Todos = () => /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(NewTodo, null), /*#__PURE__*/_react.default.createElement(TodoFilters, null), /*#__PURE__*/_react.default.createElement(TodoList, null)); 93 | 94 | const NewTodo = () => { 95 | const [transact] = useTransact(); 96 | return /*#__PURE__*/_react.default.createElement("form", { 97 | onSubmit: e => { 98 | e.preventDefault(); 99 | transact([{ 100 | todo: { 101 | name: e.target.elements['todo-name'].value, 102 | createdAt: new Date() 103 | } 104 | }]); 105 | e.target.reset(); 106 | } 107 | }, /*#__PURE__*/_react.default.createElement("input", { 108 | autoFocus: true, 109 | type: "text", 110 | name: "todo-name", 111 | placeholder: "What needs to be done?", 112 | autoComplete: "off", 113 | required: true 114 | }), "\xA0", /*#__PURE__*/_react.default.createElement("button", { 115 | type: "submit" 116 | }, "Create Todo")); 117 | }; 118 | 119 | const TodoList = () => { 120 | const [filters] = useEntity({ 121 | identity: 'todoFilters' 122 | }); 123 | const [todos] = useQuery({ 124 | $find: 'todo', 125 | $where: { 126 | todo: { 127 | name: '$any' 128 | } 129 | } 130 | }); 131 | return /*#__PURE__*/_react.default.createElement("div", null, todos.filter(todo => { 132 | if (!filters.get('showCompleted') && todo.get('isCompleted')) return false; 133 | if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) return false; 134 | if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false; 135 | return true; 136 | }).sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1).map(todo => /*#__PURE__*/_react.default.createElement(Todo, { 137 | key: todo.get('id'), 138 | id: todo.get('id') 139 | }))); 140 | }; // PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity 141 | // this component stays disconnected from the useQuery in the parent TodoList. 142 | // useEntity creates a separate scope for every Todo so changes to TodoList 143 | // or sibling Todos don't trigger unnecessary re-renders. 144 | 145 | 146 | const Todo = /*#__PURE__*/_react.default.memo(({ 147 | id 148 | }) => { 149 | const [todo] = useEntity(id); // Try opening the console in Chrome with custom formatters enabled 150 | // https://homebase.io/docs/homebase-react/main/debugging#custom-chrome-formatters 151 | 152 | console.log(todo); 153 | return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", { 154 | style: { 155 | display: 'flex', 156 | flexDirection: 'row', 157 | alignItems: 'flex-end', 158 | paddingTop: 20 159 | } 160 | }, /*#__PURE__*/_react.default.createElement(TodoCheck, { 161 | todo: todo 162 | }), /*#__PURE__*/_react.default.createElement(TodoName, { 163 | todo: todo 164 | })), /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(TodoProject, { 165 | todo: todo 166 | }), "\xA0\xB7\xA0", /*#__PURE__*/_react.default.createElement(TodoOwner, { 167 | todo: todo 168 | }), "\xA0\xB7\xA0", /*#__PURE__*/_react.default.createElement(TodoDelete, { 169 | todo: todo 170 | })), /*#__PURE__*/_react.default.createElement("small", { 171 | style: { 172 | color: 'grey' 173 | } 174 | }, todo.get('createdAt')?.toLocaleString())); 175 | }); 176 | 177 | const TodoCheck = ({ 178 | todo 179 | }) => { 180 | const [transact] = useTransact(); 181 | return /*#__PURE__*/_react.default.createElement("input", { 182 | type: "checkbox", 183 | style: { 184 | width: 20, 185 | height: 20, 186 | cursor: 'pointer' 187 | }, 188 | checked: !!todo.get('isCompleted'), 189 | onChange: e => transact([{ 190 | todo: { 191 | id: todo.get('id'), 192 | isCompleted: e.target.checked 193 | } 194 | }]) 195 | }); 196 | }; 197 | 198 | const TodoName = ({ 199 | todo 200 | }) => { 201 | const [transact] = useTransact(); 202 | return /*#__PURE__*/_react.default.createElement("input", { 203 | style: { 204 | border: 'none', 205 | fontSize: 20, 206 | marginTop: -2, 207 | cursor: 'pointer', 208 | ...(todo.get('isCompleted') && { 209 | textDecoration: 'line-through ' 210 | }) 211 | }, 212 | defaultValue: todo.get('name'), 213 | onChange: e => transact([{ 214 | todo: { 215 | id: todo.get('id'), 216 | name: e.target.value 217 | } 218 | }]) 219 | }); 220 | }; 221 | 222 | const TodoProject = ({ 223 | todo 224 | }) => { 225 | const [transact] = useTransact(); 226 | return /*#__PURE__*/_react.default.createElement(EntitySelect, { 227 | label: "Project", 228 | entityType: "project", 229 | value: todo.get('project', 'id'), 230 | onChange: project => transact([{ 231 | todo: { 232 | id: todo.get('id'), 233 | project 234 | } 235 | }]) 236 | }); 237 | }; 238 | 239 | const TodoOwner = ({ 240 | todo 241 | }) => { 242 | const [transact] = useTransact(); 243 | return /*#__PURE__*/_react.default.createElement(EntitySelect, { 244 | label: "Owner", 245 | entityType: "user", 246 | value: todo.get('owner', 'id'), 247 | onChange: owner => transact([{ 248 | todo: { 249 | id: todo.get('id'), 250 | owner 251 | } 252 | }]) 253 | }); 254 | }; 255 | 256 | const TodoDelete = ({ 257 | todo 258 | }) => { 259 | const [transact] = useTransact(); 260 | return /*#__PURE__*/_react.default.createElement("button", { 261 | onClick: () => transact([['retractEntity', todo.get('id')]]) 262 | }, "Delete"); 263 | }; 264 | 265 | const TodoFilters = () => { 266 | const [filters] = useEntity({ 267 | identity: 'todoFilters' 268 | }); 269 | const [transact] = useTransact(); 270 | return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("label", null, "Show Completed?", /*#__PURE__*/_react.default.createElement("input", { 271 | type: "checkbox", 272 | checked: filters.get('showCompleted'), 273 | onChange: e => transact([{ 274 | todoFilter: { 275 | id: filters.get('id'), 276 | showCompleted: e.target.checked 277 | } 278 | }]) 279 | })), "\xA0\xB7\xA0", /*#__PURE__*/_react.default.createElement(EntitySelect, { 280 | label: "Project", 281 | entityType: "project", 282 | value: filters.get('project'), 283 | onChange: project => transact([{ 284 | todoFilter: { 285 | id: filters.get('id'), 286 | project 287 | } 288 | }]) 289 | }), "\xA0\xB7\xA0", /*#__PURE__*/_react.default.createElement(EntitySelect, { 290 | label: "Owner", 291 | entityType: "user", 292 | value: filters.get('owner'), 293 | onChange: owner => transact([{ 294 | todoFilter: { 295 | id: filters.get('id'), 296 | owner 297 | } 298 | }]) 299 | })); 300 | }; 301 | 302 | const EntitySelect = /*#__PURE__*/_react.default.memo(({ 303 | label, 304 | entityType, 305 | value, 306 | onChange 307 | }) => { 308 | const [entities] = useQuery({ 309 | $find: entityType, 310 | $where: { 311 | [entityType]: { 312 | name: '$any' 313 | } 314 | } 315 | }); 316 | return /*#__PURE__*/_react.default.createElement("label", null, label, ":\xA0", /*#__PURE__*/_react.default.createElement("select", { 317 | name: entityType, 318 | value: value || '', 319 | onChange: e => onChange && onChange(Number(e.target.value) || null) 320 | }, /*#__PURE__*/_react.default.createElement("option", { 321 | key: "-", 322 | value: "" 323 | }), entities.map(entity => /*#__PURE__*/_react.default.createElement("option", { 324 | key: entity.get('id'), 325 | value: entity.get('id') 326 | }, entity.get('name'))))); 327 | }); -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/react/array.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.react.array 2 | {:no-doc true} 3 | (:require 4 | [devcards.core :as dc] 5 | [homebase.react] 6 | ["../js_compiled/array" :as react-example]) 7 | (:require-macros 8 | [devcards.core :refer [defcard-rg defcard-doc]] 9 | [homebase.dev.macros :refer [inline-resource]])) 10 | 11 | (defcard-rg array-example 12 | [react-example/App]) 13 | 14 | (def code-snippet 15 | (clojure.string/replace-first 16 | (inline-resource "src/dev/homebase/dev/example/js/array.jsx") 17 | "const { HomebaseProvider, useTransact, useEntity } = window.homebase.react" 18 | "import { HomebaseProvider, useTransact, useEntity } from 'homebase-react'")) 19 | (defcard-doc 20 | "[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/src/dev/homebase/dev/example/js/array.jsx)" 21 | (str "```javascript\n" code-snippet "\n```")) 22 | 23 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/react/counter.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.react.counter 2 | {:no-doc true} 3 | (:require 4 | [devcards.core :as dc] 5 | [homebase.react] 6 | ["../js_compiled/counter" :as react-example]) 7 | (:require-macros 8 | [devcards.core :refer [defcard-rg defcard-doc]] 9 | [homebase.dev.macros :refer [inline-resource]])) 10 | 11 | (defcard-rg counter-example 12 | [react-example/App]) 13 | 14 | (def code-snippet 15 | (clojure.string/replace-first 16 | (inline-resource "src/dev/homebase/dev/example/js/counter.jsx") 17 | "const { HomebaseProvider, useTransact, useEntity } = window.homebase.react" 18 | "import { HomebaseProvider, useTransact, useEntity } from 'homebase-react'")) 19 | (defcard-doc 20 | "[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/src/dev/homebase/dev/example/js/counter.jsx)" 21 | (str "```javascript\n" code-snippet "\n```")) 22 | 23 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/react/todo.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.react.todo 2 | {:no-doc true} 3 | (:require 4 | [devcards.core :as dc] 5 | [homebase.react] 6 | ["../js_compiled/todo" :as react-example]) 7 | (:require-macros 8 | [devcards.core :refer [defcard-rg defcard-doc]] 9 | [homebase.dev.macros :refer [inline-resource]])) 10 | 11 | (defcard-rg todo-example 12 | [react-example/App]) 13 | 14 | (def code-snippet 15 | (clojure.string/replace-first 16 | (inline-resource "src/dev/homebase/dev/example/js/todo.jsx") 17 | "const { HomebaseProvider, useTransact, useQuery, useEntity } = window.homebase.react" 18 | "import { HomebaseProvider, useTransact, useQuery, useEntity } from 'homebase-react'")) 19 | (defcard-doc 20 | "[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/src/dev/homebase/dev/example/js/todo.jsx)" 21 | "For an annotated walkthrough of this code [check out the tutorial 📖](https://www.notion.so/Homebase-Alpha-Docs-0f0e22f3adcd4e9d87a13440ab0c7a0b)." 22 | (str "```javascript\n" code-snippet "\n```")) 23 | 24 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/react/todo_firebase.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.react.todo-firebase 2 | {:no-doc true} 3 | (:require 4 | [devcards.core :as dc] 5 | [homebase.react] 6 | ["../js_compiled/todo-firebase" :as react-example]) 7 | (:require-macros 8 | [devcards.core :refer [defcard-rg defcard-doc]] 9 | [homebase.dev.macros :refer [inline-resource]])) 10 | 11 | (defcard-rg todo-firebase-example 12 | [react-example/App]) 13 | 14 | (def code-snippet 15 | (clojure.string/replace-first 16 | (inline-resource "src/dev/homebase/dev/example/js/todo-firebase.jsx") 17 | "const { HomebaseProvider, useClient, useTransact, useQuery, useEntity } = window.homebase.react" 18 | "import { HomebaseProvider, useClient, useTransact, useQuery, useEntity } from 'homebase-react'")) 19 | (defcard-doc 20 | "[🔗GitHub](https://github.com/homebaseio/homebase-react/blob/master/src/dev/homebase/dev/example/js/todo-firebase.jsx)" 21 | (str "```javascript\n" code-snippet "\n```")) 22 | 23 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/reagent.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.reagent 2 | {:no-doc true} 3 | (:require 4 | [devcards.core :as dc] 5 | [homebase.dev.example.reagent.counter :as counter] 6 | [homebase.dev.example.reagent.todo :as todo]) 7 | (:require-macros 8 | [devcards.core :refer [defcard-rg defcard-doc]] 9 | [homebase.dev.macros :refer [inline-resource]])) 10 | 11 | (defcard-doc 12 | "# [Homebase React](https://github.com/homebaseio/homebase-react) | Reagent Examples 13 | 14 | **[cljdoc](https://cljdoc.org/d/io.homebase/homebase-react/CURRENT)** 15 | 16 | Examples 17 | 18 | - counter 19 | - todo") 20 | 21 | (defcard-doc "---") 22 | 23 | (defcard-doc "## Counter Example") 24 | (defcard-rg counter 25 | counter/counter) 26 | (defcard-doc 27 | (str "```clojure\n" (inline-resource "src/dev/homebase/dev/example/reagent/counter.cljs") "\n```")) 28 | (defcard-doc "---") 29 | 30 | (defcard-doc "## Todo Example") 31 | (defcard-rg todo 32 | todo/todo-app) 33 | (defcard-doc 34 | (str "```clojure\n" (inline-resource "src/dev/homebase/dev/example/reagent/todo.cljs") "\n```")) -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/reagent/counter.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.reagent.counter 2 | {:no-doc true} 3 | (:require 4 | [datascript.core :as d] 5 | [homebase.reagent :as hbr] 6 | [datalog-console.integrations.datascript :as datalog-console])) 7 | 8 | (def db-conn (d/create-conn {})) 9 | 10 | (d/transact! db-conn [[:db/add 1 :count 0]]) ; Transact some starting data. 11 | 12 | (hbr/connect! db-conn) ; Connect homebase.reagent to the database. 13 | 14 | (datalog-console/enable! {:conn db-conn}) ; Also connect the datalog-console extension for better debugging. 15 | 16 | (defn counter [] 17 | (let [[entity] (hbr/entity db-conn 1)] ; Get a homebase.reagent/Entity. Note the use of db-conn and not @db-conn, this makes it reactive. 18 | (js/console.log @entity) ; Demo custom entity formatters https://cljdoc.org/d/io.homebase/homebase-react/CURRENT/doc/misc/tooling-debugging#custom-chrome-formatters 19 | (fn [] 20 | [:div 21 | "Count: " (:count @entity) ; Deref the entity just like a reagent/atom. 22 | [:div 23 | [:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @entity))]])} ; Use d/transact! just like normal. 24 | "Increment"]]]))) 25 | 26 | -------------------------------------------------------------------------------- /src/dev/homebase/dev/example/reagent/todo.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.example.reagent.todo 2 | {:no-doc true} 3 | (:require 4 | [datascript.core :as d] 5 | [reagent.core :as r] 6 | [homebase.reagent :as hbr] 7 | [datalog-console.integrations.datascript :as datalog-console])) 8 | 9 | (def schema {:db/ident {:db/unique :db.unique/identity} 10 | :todo/project {:db/type :db.type/ref 11 | :db/cardinality :db.cardinality/one} 12 | :todo/owner {:db/type :db.type/ref 13 | :db/cardinality :db.cardinality/one}}) 14 | 15 | (def db-conn (d/create-conn schema)) 16 | 17 | (def initial-tx [{:db/ident :todo.filters 18 | :todo.filter/show-completed? true 19 | :todo.filter/owner 0 20 | :todo.filter/project 0} 21 | {:todo/name "Go home" 22 | :todo/created-at (js/Date.now) 23 | :todo/owner -2 24 | :todo/project -3} 25 | {:todo/name "Fix ship" 26 | :todo/completed? true 27 | :todo/created-at (js/Date.now) 28 | :todo/owner -1 29 | :todo/project -4} 30 | {:db/id -1 31 | :user/name "Stella"} 32 | {:db/id -2 33 | :user/name "Arpegius"} 34 | {:db/id -3 35 | :project/name "Do it"} 36 | {:db/id -4 37 | :project/name "Make it"}]) 38 | 39 | (d/transact! db-conn initial-tx) 40 | 41 | (hbr/connect! db-conn) 42 | 43 | (datalog-console/enable! {:conn db-conn}) 44 | 45 | (defn select [{:keys [attr]}] 46 | (let [[options] (hbr/q '[:find ?e ?v 47 | :in $ ?attr 48 | :where [?e ?attr ?v]] 49 | db-conn attr)] 50 | (fn [{:keys [label attr value on-change]}] 51 | [:label label " " 52 | [:select 53 | {:name (str attr) 54 | :value (or value "") 55 | :on-change (fn [e] (when on-change (on-change (js/Number (goog.object/getValueByKeys e #js ["target" "value"])))))} 56 | [:option {:value ""} ""] 57 | (for [[id value] @options] 58 | ^{:key id} [:option 59 | {:value id} 60 | value])]]))) 61 | 62 | (defn todo [id] 63 | (let [[todo] (hbr/entity db-conn id)] 64 | (js/console.log @todo) 65 | (fn [id] 66 | [:div {:style {:padding-bottom 20}} 67 | [:div 68 | [:input 69 | {:type "checkbox" 70 | :style {:width "18px" :height "18px" :margin-left "0"} 71 | :checked (true? (:todo/completed? @todo)) 72 | :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}] 73 | [:input 74 | {:type "text" 75 | :style {:text-decoration (when (:todo/completed? @todo) "line-through") :border "none" :width "auto" :font-weight "bold" :font-size "20px"} 76 | :value (:todo/name @todo) 77 | :on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/name (goog.object/getValueByKeys % #js ["target" "value"])]])}]] 78 | [:div 79 | [select 80 | {:label "Owner:" 81 | :attr :user/name 82 | :value (get-in @todo [:todo/owner :db/id]) 83 | :on-change (fn [owner-id] (d/transact! db-conn [[(if (= 0 owner-id) :db/retract :db/add) (:db/id @todo) :todo/owner (when (not= 0 owner-id) owner-id)]]))}] 84 | " · " 85 | [select 86 | {:label "Project:" 87 | :attr :project/name 88 | :value (get-in @todo [:todo/project :db/id]) 89 | :on-change (fn [project-id] (d/transact! db-conn [[(if (= 0 project-id) :db/retract :db/add) (:db/id @todo) :todo/project (when (not= 0 project-id) project-id)]]))}] 90 | " · " 91 | [:button 92 | {:on-click #(d/transact! db-conn [[:db/retractEntity (:db/id @todo)]])} 93 | "Delete"]] 94 | [:div 95 | [:small {:style {:color "grey"}} 96 | (.toLocaleString (js/Date. (:todo/created-at @todo)))]]]))) 97 | 98 | (defn todo-filters [] 99 | (let [[filters] (hbr/entity db-conn [:db/ident :todo.filters])] 100 | (fn [] 101 | [:div {:style {:padding "20px 0"}} 102 | [:strong "Filters · "] 103 | [:label 104 | "Show completed " 105 | [:input 106 | {:type "checkbox" 107 | :checked (:todo.filter/show-completed? @filters) 108 | :on-change #(d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/show-completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}]] 109 | " · " 110 | [select 111 | {:label "Owner" 112 | :attr :user/name 113 | :value (:todo.filter/owner @filters) 114 | :on-change (fn [owner-id] (d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/owner (or owner-id 0)]]))}] 115 | " · " 116 | [select 117 | {:label "Project" 118 | :attr :project/name 119 | :value (:todo.filter/project @filters) 120 | :on-change (fn [project-id] (d/transact! db-conn [[:db/add (:db/id @filters) :todo.filter/project (or project-id 0)]]))}]]))) 121 | 122 | (defn todos [] 123 | (let [[todos] (hbr/q '[:find [(pull ?todo [:db/id :todo/created-at]) ...] 124 | :where 125 | ; Get all todos with names 126 | [?todo :todo/name] 127 | 128 | ; Get the id for :todo.filters 129 | [?filters :db/ident :todo.filters] 130 | 131 | ; Filter completed todos if not :todo.filter/show-completed? 132 | (or [?filters :todo.filter/show-completed? true] 133 | (not [?todo :todo/completed? true])) 134 | 135 | ; Filter by owner if :todo.filter/owner is not 0 136 | [?filter :todo.filter/owner ?owner] 137 | (or [(= 0 ?owner)] 138 | [?todo :todo/owner ?owner]) 139 | 140 | ; Filter by project if :todo.filter/project is not 0 141 | [?filter :todo.filter/project ?project] 142 | (or [(= 0 ?project)] 143 | [?todo :todo/project ?project])] 144 | db-conn)] 145 | (fn [] 146 | [:div 147 | [todo-filters] 148 | [:div 149 | (for [{:keys [db/id]} 150 | (->> @todos 151 | (sort-by :todo/created-at) 152 | (reverse))] 153 | ^{:key id} [todo id])]]))) 154 | 155 | (defn new-todo [] 156 | (let [name (r/atom "") 157 | [filters] (hbr/entity db-conn [:db/ident :todo.filters])] 158 | (fn [] 159 | [:form {:on-submit (fn [e] 160 | (.preventDefault e) 161 | (d/transact! db-conn [{:todo/name @name 162 | :todo/created-at (js/Date.now)} 163 | ; Also reset the filters to make sure the new todo shows up in the UI immediately 164 | {:db/id (:db/id @filters) 165 | :todo.filter/show-completed? true 166 | :todo.filter/owner 0 167 | :todo.filter/project 0}]) 168 | (reset! name ""))} 169 | [:input {:type "text" 170 | :on-change #(reset! name (goog.object/getValueByKeys % #js ["target" "value"])) 171 | :value @name 172 | :placeholder "Write a todo..."}] 173 | [:button {:type "submit"} "Create todo"]]))) 174 | 175 | (defn todo-app [] 176 | [:div 177 | [new-todo] 178 | [todos]]) -------------------------------------------------------------------------------- /src/dev/homebase/dev/macros.clj: -------------------------------------------------------------------------------- 1 | (ns homebase.dev.macros 2 | {:no-doc true}) 3 | 4 | (defmacro inline-resource [resource-path] 5 | (slurp resource-path)) 6 | 7 | -------------------------------------------------------------------------------- /src/main/homebase/cache.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.cache 2 | "A homebase cache intermediates between a view layer like React or Reagent and a data layer like Datascript or Datahike. 3 | 4 | It ensures that as data changes the view is incremently updated to reflect the most recent state while maintaining consistency/transactionality, performing all updates for a transaction simultaneously. 5 | 6 | The cache takes the form of a map where components (identified by site-ids) can subscribe to updates of specific data by associng change-handlers into the cache. 7 | 8 | E.g. 9 | 10 | ```clojure 11 | {:ea 12 | {[\"EntityId\" :attribute] 13 | {\"uuid-for-a-component-or-'hook'\" ; multiple components may subscribe to changes in the same datom so each gets their own change-handler 14 | (fn on-entity-attr-change-handler-fn 15 | [{:db-after ; the db value that corresponds with this update. Should be used by the consuming component to update the view so it stays in sync because all change-handlers use the same DB snapshot for the same TX. 16 | :datom ; the updated datom 17 | :site-id \"uuid-for-a-component-or-'hook'\"}] 18 | ; INSERT code to update the view in this component with the newest state of this datom 19 | )}} 20 | :q 21 | {['[:find ?e ?a ?v 22 | :where [?e ?a ?v]] 23 | other-inputs] 24 | {\"uuid-for-a-component-or-'hook'\" ; multiple components may subscribe to changes in the same query so each gets their own change-handler 25 | (fn on-query-change-handler-fn 26 | [{:db-after ; the db value that corresponds with this update. Should be used by the consuming component to update the view so it stays in sync because all change-handlers use the same DB snapshot for the same TX. 27 | :site-id \"uuid-for-a-component-or-'hook'\"}] 28 | ; INSERT code to update the view with the latest results of the query 29 | )}}} 30 | ``` 31 | 32 | The cache takes care of appropriately invoking change-handlers after every transaction." 33 | (:refer-clojure :exclude [assoc dissoc]) 34 | (:require 35 | [datascript.core] 36 | [datascript.db])) 37 | 38 | (defn create-conn 39 | "Returns a homebase.cache in an atom." 40 | [] 41 | (atom 42 | {:ea {} 43 | :q {}})) 44 | 45 | (defn assoc 46 | "Helper to assoc a change-handler into the cache. 47 | 48 | Usage: 49 | ```clojure 50 | ; assoc to the Entity cache 51 | (homebase.cache/assoc cache :ea [1 :attr] \"a uid for the call site\" (fn change-handler [...] ...)) 52 | 53 | ; assoc to the Query cache 54 | (homebase.cache/assoc cache :q '[:find ... :where ...] \"a uid for the call site\" (fn change-handler [...] ...)) 55 | ```" 56 | [cache cache-type lookup site-id change-handler] 57 | (assoc-in cache [cache-type lookup site-id] change-handler)) 58 | 59 | (defn dissoc 60 | [cache cache-type lookup site-id] 61 | (let [cache (update-in cache [cache-type lookup] clojure.core/dissoc site-id)] 62 | (if (empty? (get-in cache [cache-type lookup])) 63 | (update cache cache-type clojure.core/dissoc lookup) 64 | cache))) 65 | 66 | (defn create-listener 67 | "Returns a db listener function that invokes all subscribed change-handlers in the cache when a datom is transacted. 68 | 69 | Entity Attribute cache updates are mostly complete and dispatch on the smallest possible set of change-handlers. 70 | 71 | Query cache updates are NOT complete and all of them dispatch on every transaction regardless of whether the transaction can be infered to change the results of a query or not. This is tends to be fine for datasets with thousands of datoms, but could be expensive for applications with lots of datoms and lots of complex queries. Improvements via differential datalog need to be investigated." 72 | [cache-conn] 73 | (fn [{:keys [tx-data db-after]}] 74 | (let [cache @cache-conn 75 | ;; The EA change-handler only needs to be triggered once for each site-id. 76 | ;; NOTE: this is complected with knowledge of how homebase.reagent currently handles updates and a clearer seperation of concerns should probably be drawn. But for now it's easier to just do this check here. 77 | triggered-ea-handlers (atom #{})] 78 | ;; EA handlers 79 | (doseq [[e a :as datom] tx-data] 80 | (let [subscriptions (get-in cache [:ea [e a]])] 81 | (doseq [[site-id change-handler] subscriptions] 82 | (when (not (get @triggered-ea-handlers site-id)) 83 | (swap! triggered-ea-handlers conj site-id) 84 | (change-handler {:db-after db-after 85 | :datom datom 86 | :site-id site-id}))))) 87 | ;; Query handlers 88 | ;; TODO: dispatch on change-handlers more judiciously instead of on every transaction. 89 | ;; See work on incremental view manintinence e.g. https://github.com/sixthnormal/clj-3df 90 | (let [subscriptions (mapcat seq (vals (:q cache)))] 91 | (doseq [[site-id change-handler] subscriptions] 92 | (change-handler {:db-after db-after 93 | :site-id site-id})))))) 94 | 95 | (defn db-conn-type [db-conn] 96 | (if (instance? cljs.core/Atom db-conn) 97 | (type @db-conn) 98 | (type db-conn))) 99 | 100 | (defmulti connect! 101 | "Connect the cache to a database connection and listen to changes in the transaction log." 102 | (fn [cache-conn db-conn] (db-conn-type db-conn))) 103 | (defmethod connect! datascript.db/DB [cache-conn db-conn] 104 | (swap! db-conn with-meta (merge (meta @db-conn) {::conn cache-conn})) 105 | (datascript.core/listen! db-conn ::connection (create-listener cache-conn))) 106 | 107 | (defmulti disconnect! 108 | "Disconnect the transaction log listener." 109 | (fn [db-conn] (db-conn-type db-conn))) 110 | (defmethod disconnect! datascript.db/DB [db-conn] 111 | (swap! db-conn with-meta (clojure.core/dissoc (meta @db-conn) ::conn)) 112 | (datascript.core/unlisten! db-conn ::connection)) 113 | 114 | (comment 115 | (do 116 | (def cache-conn (create-conn)) 117 | (def db-conn (datascript.core/create-conn {})) 118 | (connect! cache-conn db-conn) 119 | (swap! cache-conn assoc-ea [1 :a] "abc123" #(print "yolo" %))) 120 | (datascript.core/transact! db-conn [{:a "a" :b "b" :c "c"}]) 121 | (datascript.core/transact! db-conn [[:db/retract 1 :a]]) 122 | (datascript.core/transact! db-conn [[:db/retractEntity 1]]) 123 | (swap! cache-conn dissoc-ea [1 :a] "abc123") 124 | (disconnect! db-conn)) -------------------------------------------------------------------------------- /src/main/homebase/reagent.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.reagent 2 | (:require 3 | [homebase.cache :as hbc] 4 | [datalog-console.chrome.formatters] ; Load the formatters ns to extend cljs-devtools to better render db entities in the chrome console if cljs-devtools is enabled. 5 | [devtools.protocols :as dtp :refer [IFormat]] 6 | [datascript.impl.entity :as de] 7 | [reagent.core :as r] 8 | [nano-id.core :refer [nano-id]] 9 | [datascript.core :as d])) 10 | 11 | (declare lookup-entity) 12 | 13 | (deftype Entity [^de/Entity entity meta] 14 | IFormat 15 | (-header [_] (dtp/-header entity)) 16 | (-has-body [_] (dtp/-has-body entity)) 17 | (-body [_] (dtp/-body entity)) 18 | IMeta 19 | (-meta [_] meta) 20 | IWithMeta 21 | (-with-meta [_ new-meta] (Entity. entity new-meta)) 22 | ILookup 23 | (-lookup [this attr] (lookup-entity this attr nil)) 24 | (-lookup [this attr not-found] (lookup-entity this attr not-found)) 25 | IAssociative 26 | (-contains-key? [this k] (not= ::nf (lookup-entity this k ::nf))) 27 | IFn 28 | (-invoke [this k] (lookup-entity this k nil)) 29 | (-invoke [this k not-found] (lookup-entity this k not-found))) 30 | 31 | (defn ^:no-doc lookup-entity [^Entity entity attr not-found] 32 | (let [result (de/lookup-entity ^de/Entity (.-entity entity) attr not-found) 33 | after-lookup (::after-lookup (meta entity))] 34 | (when after-lookup (after-lookup {:entity (.-entity entity) :attr attr :result result})) 35 | (cond 36 | (instance? de/Entity result) 37 | (Entity. result {::after-lookup after-lookup}) 38 | 39 | (and (set? result) (instance? de/Entity (first result))) 40 | (set (map #(Entity. % {::after-lookup after-lookup}) result)) 41 | 42 | :else result))) 43 | 44 | (defn connect! 45 | "Connects a db-conn to a homebase.cache. This is a prerequisite for any of the db read functions in this namespace ([[entity]], [[q]]) to be reactive. Returns a homebase.cache connection. 46 | 47 | ```clojure 48 | (def db-conn (datascript/create-conn)) 49 | (hbr/connect! db-conn) 50 | ```" 51 | [db-conn] 52 | (let [cache-conn (hbc/create-conn)] 53 | (hbc/connect! cache-conn db-conn) 54 | {:cache-conn cache-conn})) 55 | 56 | (defn disconnect! [db-conn] 57 | (hbc/disconnect! db-conn)) 58 | 59 | (defn ^:no-doc get-cache-conn-from-db [db] 60 | (let [cache-conn (:homebase.cache/conn (meta db)) 61 | _ (when (not cache-conn) 62 | (throw (ex-info "Cache not connected. Connect your db to the cache with (homebase.reagent/connect! db-conn) first." 63 | {})))] 64 | cache-conn)) 65 | 66 | (defn ^:no-doc make-reactive-entity [{:keys [^de/Entity entity r-entity tracked-ea-pairs db-conn cache-conn site-id] :as args}] 67 | (let [top-level-entity-id (:db/id entity) 68 | e (Entity. entity {::after-lookup 69 | (fn after-lookup [{:keys [^de/Entity entity attr]}] 70 | (swap! tracked-ea-pairs conj [(:db/id entity) attr]) 71 | (swap! cache-conn hbc/assoc :ea [(:db/id entity) attr] site-id 72 | (fn change-handler [{:keys [db-after]}] 73 | (reset! r-entity 74 | (make-reactive-entity 75 | (merge args {:entity (d/entity db-after top-level-entity-id)}))))) 76 | #_(js/console.log top-level-entity-id (:db/id entity) attr @cache-conn))})] 77 | e)) 78 | 79 | (defn entity 80 | "Returns a reactive `homebase.reagent/Entity` wrapped in a vector. 81 | 82 | It offers a normalized subset of other entity APIs with the 83 | primary addition being that implemented protocols are reactive 84 | and trigger re-renders when related datoms change. 85 | 86 | Usage: 87 | 88 | ```clojure 89 | (defn your-component [] 90 | (let [[entity-1] (hbr/entity db-conn 1) 91 | [entity-2] (hbr/entity db-conn [:uniq-attr :value])] 92 | (fn [] 93 | [:div 94 | (:attr @entity-1) 95 | (get-in @entity-2 [:ref-attr :attr])]))) 96 | ``` 97 | 98 | Gotchas: 99 | 100 | - **This takes a conn, not a db.** 101 | - `homebase.reagent/Entity` only implements the `ILookup` and `IFn` protocols, i.e. only attribute lookups like `(:attr hbr-entity)`. 102 | - Use non-reactive entities from your DB if you need to use other protocols. 103 | - E.g. `(datascript/entity @db-conn 1)`" 104 | [db-conn lookup] 105 | (let [cache-conn (get-cache-conn-from-db @db-conn) 106 | entity (d/entity @db-conn lookup) 107 | site-id (nano-id) 108 | tracked-ea-pairs (atom #{}) 109 | r-entity (r/atom nil) 110 | hbr-entity (make-reactive-entity {:entity entity :r-entity r-entity :tracked-ea-pairs tracked-ea-pairs :db-conn db-conn :cache-conn cache-conn :site-id site-id}) 111 | _ (reset! r-entity hbr-entity) 112 | f (fn [] 113 | (r/with-let [] 114 | @r-entity 115 | (finally ; handle unmounting this component 116 | (doseq [ea @tracked-ea-pairs] 117 | (swap! cache-conn hbc/dissoc :ea ea site-id) 118 | #_(js/console.log ea @cache-conn)))))] 119 | [(r/track f)])) 120 | 121 | (defn q 122 | "Returns a reactive query result wrapped in a vector. 123 | 124 | It will trigger a re-render when its result changes. 125 | 126 | Usage: 127 | 128 | ```clojure 129 | (defn your-component [] 130 | (let [[query-result] (hbr/q db-conn [:find [?e ...] 131 | :where [?e :attr]])] 132 | (fn [] 133 | [:div 134 | (for [eid @query-result] 135 | ^{:key eid}[another-component eid])]))) 136 | ``` 137 | 138 | Gotchas: 139 | 140 | - **This takes a conn, not a db.** 141 | - At the moment it's only possible to [[connect!]] to one DB at a time, so reactive query results are only supported on one DB. If you pass more DBs as args the query will only be rerun if the first DB changes." 142 | [query db-conn & inputs] 143 | (let [cache-conn (get-cache-conn-from-db @db-conn) 144 | result (apply d/q query @db-conn inputs) 145 | r-result (r/atom result) 146 | site-id (nano-id) 147 | _ (swap! cache-conn hbc/assoc :q [query inputs] site-id 148 | (fn [{:keys [db-after]}] 149 | (reset! r-result (apply d/q query db-after inputs)))) 150 | f (fn [] 151 | (r/with-let [] 152 | @r-result 153 | (finally ; handle unmounting this component 154 | (swap! cache-conn hbc/dissoc :q [query inputs] site-id) 155 | #_(js/console.log query @cache-conn))))] 156 | [(r/track f)])) -------------------------------------------------------------------------------- /src/main/homebase/util.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.util 2 | {:no-doc true}) 3 | 4 | (defn paths [m] 5 | (if (or (not (map? m)) (empty? m)) 6 | '(()) 7 | (for [[k v] m 8 | subkey (paths v)] 9 | (cons k subkey)))) -------------------------------------------------------------------------------- /src/test/homebase/benchmarks.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | /* eslint-disable jest/no-hooks */ 3 | /* eslint-disable react/prefer-stateless-function */ 4 | /* eslint-disable react/button-has-type */ 5 | /* eslint-env jest */ 6 | import Enzyme, { mount } from 'enzyme' 7 | import Adapter from 'enzyme-adapter-react-16' 8 | import React from 'react' 9 | import Benchmark from 'react-component-benchmark' 10 | import { HomebaseProvider, useEntity, useTransact } from '../../../dist/js/homebase.react' 11 | 12 | const config = { 13 | initialData: [ 14 | { 15 | counter: { 16 | identity: 'counter', 17 | count: 0, 18 | }, 19 | }, 20 | ], 21 | } 22 | 23 | const Counter = () => { 24 | const [counter] = useEntity({ identity: 'counter' }) 25 | const [transact] = useTransact() 26 | return ( 27 |
28 | Count: {counter.get('count')} 29 |
30 | 44 |
45 |
46 | ) 47 | } 48 | 49 | Enzyme.configure({ adapter: new Adapter() }) 50 | 51 | class Test extends React.Component { 52 | render() { 53 | const counters = Array(100) 54 | .fill() 55 | .map((_, i) => ) 56 | return {counters} 57 | } 58 | } 59 | 60 | describe('benchmark', () => { 61 | let props 62 | let meanTime 63 | 64 | beforeEach(() => { 65 | meanTime = 0 66 | props = { 67 | component: Test, 68 | onComplete: jest.fn((results) => { 69 | meanTime = results.mean 70 | }), 71 | samples: 10, 72 | } 73 | }) 74 | 75 | it('mounts in a reasonable amount of time', () => { 76 | expect.assertions(1) 77 | const component = mount() 78 | component.instance().start() 79 | expect(meanTime).toBeLessThan(250) 80 | }) 81 | 82 | it('updates in a reasonable amount of time', () => { 83 | expect.assertions(1) 84 | const component = mount() 85 | component.instance().start() 86 | expect(meanTime).toBeLessThan(250) 87 | }) 88 | 89 | it('unmounts in a reasonable amount of time', () => { 90 | expect.assertions(1) 91 | const component = mount() 92 | component.instance().start() 93 | expect(meanTime).toBeLessThan(250) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /src/test/homebase/reagent_test.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.reagent-test 2 | {:no-doc true} 3 | (:require 4 | [homebase.test-polyfills] 5 | [reagent.core :as r] 6 | [datascript.core :as d] 7 | [homebase.reagent :as hbr] 8 | [clojure.test :refer [deftest testing is use-fixtures]] 9 | [homebase.dev.example.reagent.counter :as counter] 10 | [homebase.dev.example.reagent.todo :as todo] 11 | ["@testing-library/react" :as rt])) 12 | 13 | (set! *warn-on-infer* false) 14 | 15 | (use-fixtures :each 16 | {:after rt/cleanup}) 17 | 18 | ;; Idea from https://github.com/reagent-project/reagent/blob/master/test/reagenttest/utils.cljs 19 | (defn with-mounted-component [comp f] 20 | (let [mounted-component (rt/render (r/as-element comp))] 21 | (try 22 | (f mounted-component) 23 | (finally 24 | (.unmount mounted-component) 25 | (r/flush))))) 26 | 27 | (defn click-element [el] 28 | (.click rt/fireEvent el) 29 | (r/flush)) 30 | 31 | (deftest test-counter 32 | (do 33 | (reset! counter/db-conn @(d/create-conn)) 34 | (d/transact! counter/db-conn [[:db/add 1 :count 0]]) 35 | (hbr/connect! counter/db-conn) 36 | (with-mounted-component 37 | [counter/counter] 38 | (fn [^js/React component] 39 | (testing "The count should start at 0" 40 | (is (not (nil? (.getByText component "Count: 0"))))) 41 | (testing "The count should inc by 1" 42 | (click-element (.getByText component "Increment")) 43 | (is (not (nil? (.getByText component "Count: 1"))))) 44 | (testing "The count should inc by 2 more" 45 | (click-element (.getByText component "Increment")) 46 | (click-element (.getByText component "Increment")) 47 | (is (not (nil? (.getByText component "Count: 3"))))))))) 48 | 49 | (deftest test-todo 50 | (do 51 | (reset! todo/db-conn @(d/create-conn todo/schema)) 52 | (d/transact! todo/db-conn todo/initial-tx) 53 | (hbr/connect! todo/db-conn) 54 | (with-mounted-component 55 | [todo/todo-app] 56 | (fn [component] 57 | (testing "render list" 58 | (is (not (nil? (.getByDisplayValue component "Go home")))) 59 | (is (not (nil? (.getByDisplayValue component "Fix ship"))))) 60 | (testing "query updates list on filter change" 61 | (d/transact! todo/db-conn [{:db/id (:db/id (d/entity @todo/db-conn [:db/ident :todo.filters])) 62 | :todo.filter/show-completed? false}]) 63 | (r/flush) 64 | (is (not (nil? (.getByDisplayValue component "Go home")))) 65 | (is (thrown-with-msg? 66 | js/Error 67 | #"Unable to find an element with the display value: Fix ship" 68 | (.getByDisplayValue component "Fix ship"))) 69 | (d/transact! todo/db-conn [{:db/id (:db/id (d/entity @todo/db-conn [:db/ident :todo.filters])) 70 | :todo.filter/show-completed? true}]) 71 | (r/flush)) 72 | (testing "deletion" 73 | (click-element (nth (.getAllByText component "Delete") 0)) 74 | (is (thrown-with-msg? 75 | js/Error 76 | #"Unable to find an element with the display value: Fix ship." 77 | (.getByDisplayValue component "Fix ship"))) 78 | (is (not (nil? (.getByDisplayValue component "Go home"))))) 79 | (testing "insertion" 80 | (d/transact! todo/db-conn [{:todo/name "A new test todo" :todo/created-at (js/Date.now)}]) 81 | (r/flush) 82 | (is (not (nil? (.getByDisplayValue component "A new test todo"))))))))) -------------------------------------------------------------------------------- /src/test/homebase/test_polyfills.cljs: -------------------------------------------------------------------------------- 1 | (ns homebase.test-polyfills 2 | {:no-doc true} 3 | (:require 4 | ["@peculiar/webcrypto" :refer [Crypto]] 5 | ["jsdom" :refer [JSDOM]])) 6 | 7 | ; nano-id node.js polyfill 8 | (set! js/crypto (Crypto.)) 9 | 10 | ; jsdom polyfill 11 | (def dom (JSDOM. "" #js {:pretendToBeVisual true})) 12 | (set! js/window dom.window) 13 | (set! js/document dom.window.document) 14 | (set! js/navigator #js {:userAgent "node.js"}) -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A database transaction. 3 | * @example 4 | * [{ todo: { name: "a new todo" } }, 5 | * { todo: { id: 1, name: "an update (see the id)", project: -1 } }, 6 | * { project: { id: -1, name: "a new project with a temp id" } }] 7 | */ 8 | export type Transaction = Array; 9 | 10 | /** 11 | * Add schema to simplify relational queries. Define relationships and uniqueness constraints in just one place. 12 | * @example 13 | * { todo: { project: { type: 'ref' } 14 | * name: { unique: 'identity' } } } 15 | */ 16 | export type Schema = object; 17 | 18 | /** 19 | * Add lookup helpers to simplify relational queries. Define relationships and uniqueness constraints in just one place. 20 | * @example 21 | * { todo: { project: { type: 'ref' } 22 | * name: { unique: 'identity' } } } 23 | */ 24 | export type LookupHelpers = object; 25 | 26 | /** 27 | * A homebase configuration. 28 | * @typedef {Object} config 29 | * @property {?object} schema - an optional schema 30 | * @property {?object} lookupHelpers - optional lookupHelpers 31 | * @property {?array} initialData - an optional initial transaction 32 | */ 33 | export type config = {schema?: Schema, lookupHelpers?: LookupHelpers, initialData?: Transaction }; 34 | 35 | /** 36 | * A reactive reference to an entity's data. 37 | */ 38 | export type Entity = { 39 | /** 40 | * Retrieves an attribute from the entity. Traverse arbitrarily deep relationships between entities by passing multiple attributes. 41 | * @param {...string} attribute - an attribute of the entity 42 | * @example 43 | * // returns 'a todo name' 44 | * aTodoEntity.get('name') 45 | * @example 46 | * // returns 'a project name' 47 | * aTodoEntity.get('project', 'name') 48 | */ 49 | get: (...attribute:string[]) => any; 50 | } 51 | 52 | /** 53 | * Datoms are the smallest unit of data in the database. They are key-value pairs with extra information like entity id, transaction id, and if this key was added or deleted from the database. 54 | * @example 55 | * [10, ":todo/name", "some todo", 536870922, true] 56 | */ 57 | export type Datom = [number, string, string | number | object | Array, number, boolean]; 58 | 59 | /** 60 | * The homebase client. Provides additional functions to read and write data. It's primarily used when synchronizing data with a backend. 61 | */ 62 | export type homebaseClient = { 63 | /** 64 | * Serializes the whole db including the lookupHelpers to a string. 65 | * @returns {string} Returns the whole db as a string 66 | */ 67 | dbToString: () => string, 68 | /** 69 | * Replaces the current db with one generated by `homebaseClient.dbToString()`. 70 | * @param {string} dbString - a serialized db string 71 | */ 72 | dbFromString: (dbString: string) => void, 73 | /** 74 | * Datoms are the smallest unit of data in the database, similar to a key-value pair with extra info. 75 | * @returns {Array.} Returns all the datoms in the database. 76 | */ 77 | dbToDatoms: () => Datom[], 78 | /** 79 | * Adds a listener callback that fires after every transaction. Typically used to save data to a backend. Only one transact listener is supported per homebaseClient instance. 80 | * @param {transactListener} listener - A callback that provides an array of changedDatoms. 81 | */ 82 | addTransactListener: (listener: (changedDatoms: Datom[]) => void) => void, 83 | 84 | /** 85 | * This callback is displayed as part of the Requester class. 86 | * @callback transactListener 87 | * @param {Array.} changedDatoms - The datoms that were added and removed in a transaction. 88 | */ 89 | 90 | /** 91 | * Removes the transact listener. Only one transact listener is supported per homebaseClient instance. 92 | */ 93 | removeTransactListener: () => void, 94 | /** 95 | * Transacts data without triggering any listeners. Typically used to sync data from your backend into the client. 96 | * @param transaction - A database transaction. 97 | */ 98 | transactSilently: (transaction: Transaction) => any, 99 | 100 | /** 101 | * Returns a promise that contains a single entity by `lookup`. 102 | * @param lookup - an entity id or lookup object. 103 | * @returns Promise - A promise wrapping an entity. 104 | * @example const entity = await client.entity(10) 105 | * @example const entity = await client.entity({ identity: "a unique lookup key" }) 106 | * @example 107 | * const project = await client.entity({ project: { name: "a unique name" }}) 108 | * project.get('name') 109 | */ 110 | entity: (lookup: object | number) => Promise, 111 | 112 | /** 113 | * Returns a promise that contains a collection of entities by `query`. 114 | * @param query - a query object or datalog string. 115 | * @param args - optional query arguments. 116 | * @returns Promise<[Entity]> - A promise wrapping an array of entities. 117 | * @example 118 | * const todos = await client.query({ 119 | * $find: 'todo', 120 | * $where: { todo: { name: '$any' } } 121 | * }) 122 | * todos.map(todo => todo.get('name')) 123 | */ 124 | query: (query: object | string, ...args: any) => Promise<[Entity]> 125 | } 126 | 127 | /** 128 | * The Homebase React context component. It creates a local database and feeds it to child hooks. Put it high in your component tree. 129 | * @param props.config - an object with optional lookupHelpers and initialData parameters. 130 | * @param props.children - children elements 131 | */ 132 | export function HomebaseProvider(props: {config?:config, children:React.ReactNode}): React.ReactElement; 133 | 134 | /** 135 | * React hook to transact data to the local homebase database. 136 | * @returns [transact] - A tuple with a transact function. 137 | * @example 138 | * const [transact] = useTransact() 139 | * transact([{ todo: { name: "a new todo" } }]) 140 | */ 141 | export function useTransact(): [(transaction:Transaction) => void]; 142 | 143 | /** 144 | * React hook to return a single entity by `lookup`. 145 | * @param lookup - an entity id or lookup object. 146 | * @returns [entity] - A tuple with an entity. 147 | * @example const [entity] = useEntity(10) 148 | * @example const [entity] = useEntity({ identity: "a unique lookup key" }) 149 | * @example 150 | * const [project] = useEntity({ project: { name: "a unique name" }}) 151 | * project.get('name') 152 | */ 153 | export function useEntity(lookup: object | number): [Entity]; 154 | 155 | /** 156 | * React hook to return a collection of entities by `query`. 157 | * @param query - a query object or datalog string. 158 | * @param args - optional query arguments. 159 | * @returns [entities] - A tuple with an array of entities. 160 | * @example 161 | * const [todos] = useQuery({ 162 | * $find: 'todo', 163 | * $where: { todo: { name: '$any' } } 164 | * }) 165 | * todos.map(todo => todo.get('name')) 166 | */ 167 | export function useQuery(query: object | string, ...args: any): [Array]; 168 | 169 | /** 170 | * React hook to return a homebaseClient. 171 | * @returns [client] - A tuple with a homebaseClient 172 | * @example 173 | * const [client] = useClient() 174 | * client.dbToString() 175 | * client.dbToDatoms() 176 | */ 177 | export function useClient(): [homebaseClient]; 178 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectError } from 'tsd' 2 | import { 3 | HomebaseProvider, 4 | useTransact, 5 | useEntity, 6 | useQuery, 7 | useClient, 8 | Entity, 9 | Transaction, 10 | homebaseClient, 11 | } from '.' 12 | 13 | expectType(HomebaseProvider({ children: [] })) 14 | expectError(HomebaseProvider('a')) 15 | expectError(HomebaseProvider(1)) 16 | 17 | expectType<[(transaction: Transaction) => void]>(useTransact()) 18 | expectError(useTransact('blurb')) 19 | 20 | expectType<[Entity]>(useEntity(1)) 21 | expectError(useEntity()) 22 | 23 | expectType<[Entity[]]>(useQuery({})) 24 | expectType<[Entity[]]>(useQuery('a datalog query could live here')) 25 | expectError(useQuery(1)) 26 | 27 | expectType<[homebaseClient]>(useClient()) --------------------------------------------------------------------------------