├── .circleci └── config.yml ├── .githooks ├── post-commit └── pre-commit ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── .yarnrc ├── LICENSE ├── README.md ├── deploy.sh ├── docs ├── .vuepress │ ├── config.js │ ├── enhanceApp.js │ ├── public │ │ ├── DankMono-Regular.ttf │ │ ├── logo.png │ │ └── logo.svg │ ├── styles │ │ ├── index.styl │ │ └── palette.styl │ └── theme │ │ ├── .npmignore │ │ ├── README.md │ │ ├── __tests__ │ │ └── components │ │ │ ├── DropdownLink.spec.js │ │ │ ├── NavLink.spec.js │ │ │ └── __snapshots__ │ │ │ ├── DropdownLink.spec.js.snap │ │ │ └── NavLink.spec.js.snap │ │ ├── components │ │ ├── AlgoliaSearchBox.vue │ │ ├── DropdownLink.vue │ │ ├── DropdownTransition.vue │ │ ├── Home.vue │ │ ├── NavLink.vue │ │ ├── NavLinks.vue │ │ ├── Navbar.vue │ │ ├── Page.vue │ │ ├── PageEdit.vue │ │ ├── PageNav.vue │ │ ├── Sidebar.vue │ │ ├── SidebarButton.vue │ │ ├── SidebarGroup.vue │ │ ├── SidebarLink.vue │ │ └── SidebarLinks.vue │ │ ├── global-components │ │ └── Badge.vue │ │ ├── index.js │ │ ├── layouts │ │ ├── 404.vue │ │ └── Layout.vue │ │ ├── noopModule.js │ │ ├── package.json │ │ ├── styles │ │ ├── arrow.styl │ │ ├── code.styl │ │ ├── config.styl │ │ ├── custom-blocks.styl │ │ ├── index.styl │ │ ├── mobile.styl │ │ ├── toc.styl │ │ └── wrapper.styl │ │ └── util │ │ └── index.js ├── readme.md ├── v1 │ ├── developers │ │ └── structure.md │ ├── examples │ │ ├── UsageWithReact.md │ │ ├── UsageWithVueJS.md │ │ └── authentication.md │ ├── getting-started │ │ ├── setup-with-react.md │ │ └── setup-with-vue.md │ ├── guide │ │ ├── actions.md │ │ ├── collections.md │ │ ├── context-object.md │ │ ├── data-relations.md │ │ ├── debugging.md │ │ ├── filters.md │ │ ├── http-requests.md │ │ ├── leftovers.md │ │ ├── library.md │ │ ├── models.md │ │ ├── mutating-data.md │ │ ├── namespacing.md │ │ ├── persisting-data.md │ │ └── using-data.md │ └── introduction │ │ ├── changelog.md │ │ └── what-is-pulse.md ├── v2 │ ├── developers │ │ ├── ideas.md │ │ └── structure.md │ ├── docs │ │ ├── collection-methods.md │ │ ├── collections.md │ │ ├── concepts.md │ │ ├── context-object.md │ │ ├── debugging.md │ │ ├── http-requests.md │ │ ├── library.md │ │ ├── module-methods.md │ │ ├── modules.md │ │ ├── persisting-data.md │ │ └── using-pulse-data.md │ ├── examples │ │ ├── UsageWithReact.md │ │ ├── UsageWithVueJS.md │ │ └── authentication.md │ ├── getting-started │ │ ├── setup-with-react.md │ │ └── setup-with-vue.md │ ├── introduction │ │ ├── changelog.md │ │ └── what-is-pulse.md │ └── under-the-hood │ │ └── runtime.md ├── v3 │ ├── docs │ │ ├── actions.md │ │ ├── api.md │ │ ├── collections.md │ │ ├── computed.md │ │ ├── controllers.md │ │ ├── core.md │ │ ├── events.md │ │ ├── persisting-data.md │ │ ├── pulse-instance.md │ │ └── state.md │ ├── getting-started │ │ ├── concepts.md │ │ ├── setup-with-next.md │ │ ├── setup-with-react.md │ │ ├── setup-with-vue.md │ │ └── style-guide.md │ ├── introduction │ │ ├── changelog.md │ │ └── what-is-pulse.md │ └── resources │ │ ├── examples.md │ │ ├── ideas.md │ │ └── snippets.md └── v4 │ ├── docs │ ├── actions.md │ ├── collections.md │ ├── controllers.md │ ├── core.md │ ├── events.md │ ├── persisting-data.md │ ├── route.md │ └── state.md │ ├── getting-started │ ├── concepts.md │ ├── setup-with-next.md │ ├── setup-with-react.md │ ├── setup-with-vue.md │ └── style-guide.md │ ├── introduction │ ├── changelog.md │ └── what-is-pulse.md │ └── resources │ ├── examples.md │ ├── ideas.md │ └── snippets.md ├── examples └── typescript │ ├── core │ ├── package.json │ ├── src │ │ ├── accounts │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── app │ │ │ ├── api.ts │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ ├── persistance.ts │ │ │ └── types.ts │ │ ├── channels │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── core.ts │ │ ├── index.ts │ │ └── ui │ │ │ ├── controller.ts │ │ │ ├── index.ts │ │ │ ├── themes.ts │ │ │ └── types.ts │ ├── tsconfig.json │ └── yarn.lock │ ├── react │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── react-app-env.d.ts │ │ ├── serviceWorker.ts │ │ └── setupTests.ts │ ├── tsconfig.json │ └── yarn.lock │ └── vue │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── HelloWorld.vue │ └── main.js │ ├── vue.config.js │ └── yarn.lock ├── jest.config.js ├── lerna.json ├── now.json ├── package.json ├── packages ├── pulse-core │ ├── .watchmanconfig │ ├── LICENSE │ ├── lib │ │ ├── action.ts │ │ ├── api.ts │ │ ├── collection │ │ │ ├── collection.ts │ │ │ ├── data.ts │ │ │ ├── group.ts │ │ │ ├── model.ts │ │ │ └── selector.ts │ │ ├── computed.ts │ │ ├── controller.ts │ │ ├── dep.ts │ │ ├── event.ts │ │ ├── helpers │ │ │ ├── debounce.ts │ │ │ ├── deepmerge.ts │ │ │ └── isWatchableObj.ts │ │ ├── index.ts │ │ ├── instance.ts │ │ ├── integrate.ts │ │ ├── internal.ts │ │ ├── pulse.ts │ │ ├── runtime.ts │ │ ├── state.ts │ │ ├── status.ts │ │ ├── storage.ts │ │ ├── sub.ts │ │ ├── tracker.ts │ │ └── utils.ts │ ├── package.json │ ├── tests │ │ ├── collection.oldtest.ts │ │ ├── collection.test.ts │ │ ├── computed.oldtest.ts │ │ ├── computed.test.ts │ │ ├── pulse.oldtest.ts │ │ ├── pulse.test.ts │ │ ├── state.oldtest.ts │ │ ├── state.test.ts │ │ └── util.ts │ └── tsconfig.json ├── pulse-next │ ├── LICENSE │ ├── lib │ │ ├── index.ts │ │ └── loader.ts │ ├── package.json │ └── tsconfig.json ├── pulse-react │ ├── LICENSE │ ├── lib │ │ ├── index.ts │ │ ├── pulseHOC.ts │ │ ├── useEvent.ts │ │ ├── usePulse.ts │ │ └── useWatcher.ts │ ├── package.json │ ├── tests │ │ └── react.oldtest.tsx │ └── tsconfig.json ├── pulse-vue │ ├── LICENSE │ ├── lib │ │ └── index.ts │ ├── package.json │ └── tsconfig.json └── tsconfig.settings.json ├── tsconfig.test.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: node:8 6 | working_directory: ~/repo 7 | 8 | steps: 9 | - checkout 10 | - run: yarn --version 11 | 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "yarn.lock" }} 15 | - v1-dependencies- 16 | 17 | - run: yarn install --frozen-lockfile 18 | - run: yarn bootstrap 19 | - run: yarn build 20 | - run: ls -la node_modules/@quramy 21 | - run: ls -la node_modules/.bin 22 | 23 | - run: yarn test 24 | 25 | - save_cache: 26 | paths: 27 | - node_modules 28 | key: v1-dependencies-{{ checksum "yarn.lock" }} 29 | 30 | -------------------------------------------------------------------------------- /.githooks/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git update-index -g 3 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FILES=$(git diff --cached --name-only --diff-filter=ACMR "*.js" "*.jsx" "*.ts" "*.tsx" "*.json" "*.vue" | sed 's| |\\ |g') 3 | [ -z "$FILES" ] && exit 0 4 | 5 | # Run prettier on all files that matched 6 | echo "$FILES" | xargs ./node_modules/.bin/prettier --write 7 | # Re-stage the prettified files 8 | echo "$FILES" | xargs git add 9 | 10 | exit 0 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Current Behavior 7 | 8 | 9 | ## Extra Information 10 | 11 | 12 | ## Environment Information 13 | - Operating System: `OS NAME/VERSION` 14 | - Browser Version (If applicable): `BROWSER NAME/VERSION` 15 | - Pulse Version: `PULSE VERSION NUMBER` 16 | - Framework/Framework Version (If applicable): `If using a framework like Vue or React, fill this out.` 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## Expected Behavior 13 | 14 | 15 | ## Current Behavior 16 | 17 | 18 | ## Extra Information 19 | 20 | 21 | ## Environment Information 22 | - Operating System: `OS NAME/VERSION` 23 | - Browser Version (If applicable): `BROWSER NAME/VERSION` 24 | - Pulse Version: `PULSE VERSION NUMBER` 25 | - Framework/Framework Version (If applicable): `If using a framework like Vue or React, fill this out.` 26 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | ## Context 11 | 12 | 13 | 14 | ## How Has This Been Tested? 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | .DS_Store 4 | dist 5 | .env 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | tsconfig.tsbuildinfo 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (http://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | 69 | # End of https://www.gitignore.io/api/node -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 150, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": false, 8 | "noSemi": false, 9 | "rcVerbose": true, 10 | "ignoreChainWithDepth": 10, 11 | "arrowParens": "avoid", 12 | "semi": true 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | // { 8 | // "type": "pwa-chrome", 9 | // "request": "launch", 10 | // "name": "Launch Chrome against localhost", 11 | // "url": "http://localhost:8080", 12 | // "webRoot": "${workspaceFolder}" 13 | // }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Core Jest Tests", 18 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 19 | "args": [ 20 | "-i" 21 | ], 22 | "internalConsoleOptions": "openOnSessionStart", 23 | "outFiles": [ 24 | "${workspaceRoot}/packages/pulse-core/**/*" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jamie Pine 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 | # Pulse Framework 2 | ______________________________________________________________ 3 | 4 | ### NO LONGER MAINTAINED 5 | There's a sea of brilliant state libraries out there to use instead. 6 | Pulse taught me Typescript and had a good run, but I use TanStack Query + Valtio in newer projects. 7 | -Jamie 8 | ______________________________________________________________ 9 | **Global state and logic for reactive JavaScript applications.** Supports frameworks like React, Vue, and React Native. 10 | 11 | **Lightweight, modular and powerful.** Provides a **core** state & logic framework for your entire application; plug-and-play directly into any UI framework. 12 | 13 | Replaces Redux, Vuex, and MobX for state; and for API requests, replaces Axios and `fetch`. 14 | 15 | **Created by [@jamiepine](https://twitter.com/jamiepine)** 16 | 17 | ### Read the docs at [pulsejs.org](https://pulsejs.org). 18 | 19 | [![Join Discord](https://discordapp.com/api/guilds/658189217746255881/embed.png)](https://discord.gg/RjG8ShB) 20 | [![Follow Pulse on Twitter](https://img.shields.io/twitter/follow/pulseframework.svg?label=Pulse+on+Twitter)](https://twitter.com/pulseframework) 21 | [![Follow Jamie Pine on Twitter](https://img.shields.io/twitter/follow/jamiepine.svg?label=Jamie+on+Twitter)](https://twitter.com/jamiepine) 22 | 23 | ```ts 24 | const App = new Pulse(); 25 | 26 | const hello = App.State('the sound of music'); 27 | ``` 28 | 29 | ## Why Pulse? 30 | 31 | Pulse Framework provides a complete toolset to build front-end applications quickly and efficiently. It encourages you to construct a single core library that can be used in any UI framework. The core handles everything at the center of your application — state management, API requests, and all miscellaneous logic and calculations. Pulse supplies computed data to your UI components with full reactivity, in the framework of your choice. 32 | 33 | ### TypeScript 34 | 35 | Pulse is fully written in TypeScript and supports it heavily. Everything is type-safe out of the box and encourages you to write clean, typed code. 36 | 37 | **Read the [documentation](https://pulsejs.org/v3/introduction/what-is-pulse.html) to learn more!** 38 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | yarn docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | # if you are deploying to a custom domain 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # if you are deploying to https://.github.io 20 | # git push -f git@github.com:jamiepine/jamiepine.github.io.git master 21 | 22 | # if you are deploying to https://.github.io/ 23 | git push -f git@github.com:jamiepine/pulse.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | export default ({ Vue, router }) => { 2 | Vue.mixin({ 3 | computed: { 4 | versions() { 5 | const { $themeConfig } = this; 6 | return Object.getOwnPropertyNames($themeConfig.sidebar); 7 | }, 8 | isDocs() { 9 | const { $frontmatter } = this; 10 | if ($frontmatter.home === true) { 11 | return false; 12 | } else { 13 | return true; 14 | } 15 | } 16 | } 17 | }), 18 | router.addRoutes([ 19 | { path: '/v1.html', redirect: '/v1/introduction/what-is-pulse' }, 20 | { path: '/v1', redirect: '/v1/introduction/what-is-pulse' }, 21 | { path: '/v2.html', redirect: '/v2/introduction/what-is-pulse' }, 22 | { path: '/v2', redirect: '/v2/introduction/what-is-pulse' }, 23 | { path: '/v3.html', redirect: '/v3/introduction/what-is-pulse' }, 24 | { path: '/v3', redirect: '/v3/introduction/what-is-pulse' } 25 | ]); 26 | }; 27 | -------------------------------------------------------------------------------- /docs/.vuepress/public/DankMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/docs/.vuepress/public/DankMono-Regular.ttf -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/docs/.vuepress/public/logo.png -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $accentColor = #EF425A 2 | $textColor = #9db2c8 3 | $codeBgColor = #131620 4 | $blockBgColor = #131620 5 | $borderColor = #1b1b1b 6 | $BgColor = #0e1117 7 | $badgeTipColor = #42b983 8 | $badgeWarningColor = darken(#ffe564, 35%) 9 | $badgeErrorColor = #DA5961 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | __mocks__ 3 | .temp 4 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/README.md: -------------------------------------------------------------------------------- 1 | # @vuepress/theme-default 2 | 3 | > theme-default for VuePress 4 | 5 | ## Plugins 6 | 7 | The default theme has the following plugin built in: 8 | 9 | - [@vuepress/plugin-active-header-links](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/plugin-active-header-links) 10 | - [@vuepress/plugin-google-analytics](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/plugin-google-analytics) 11 | - [@vuepress/plugin-search](https://github.com/vuejs/vuepress/tree/master/packages/%40vuepress/plugin-search) 12 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/__tests__/components/DropdownLink.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, RouterLinkStub } from '@vue/test-utils' 2 | import DropdownLink from '../../components/DropdownLink.vue' 3 | import { createLocalVue } from '@vuepress/test-utils/client' 4 | 5 | describe('DropdownLink', () => { 6 | test('renders dropdown link.', () => { 7 | const item = { 8 | text: 'Learn More', 9 | ariaLabel: 'Learn More Select', 10 | items: [ 11 | { 12 | text: 'Guide', 13 | link: '/guide/' 14 | }, 15 | { 16 | text: 'Config Reference', 17 | link: '/config/' 18 | } 19 | ] 20 | } 21 | const wrapper = mount(DropdownLink, { 22 | localVue: createLocalVue(), 23 | stubs: { 24 | 'RouterLink': RouterLinkStub 25 | }, 26 | propsData: { item } 27 | }) 28 | expect(wrapper.html()).toMatchSnapshot() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/__tests__/components/NavLink.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, RouterLinkStub } from '@vue/test-utils' 2 | import { createLocalVue } from '@vuepress/test-utils/client' 3 | import NavLink from '../../components/NavLink.vue' 4 | 5 | describe('NavLink', () => { 6 | test('renders nav link with internal link', () => { 7 | const item = { 8 | link: '/', 9 | text: 'VuePress' 10 | } 11 | const wrapper = mount(NavLink, { 12 | localVue: createLocalVue(), 13 | stubs: { 14 | 'RouterLink': RouterLinkStub 15 | }, 16 | propsData: { item } 17 | }) 18 | expect(wrapper.html()).toMatchSnapshot() 19 | }) 20 | 21 | test('renders nav link with external link', () => { 22 | const item = { 23 | link: 'http://vuejs.org/', 24 | text: 'Vue' 25 | } 26 | const wrapper = mount(NavLink, { 27 | localVue: createLocalVue(), 28 | propsData: { item } 29 | }) 30 | expect(wrapper.html()).toMatchSnapshot() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/__tests__/components/__snapshots__/DropdownLink.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DropdownLink renders dropdown link. 1`] = ` 4 | 16 | `; 17 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/__tests__/components/__snapshots__/NavLink.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NavLink renders nav link with external link 1`] = ` 4 | 5 | Vue 6 | 7 | `; 8 | 9 | exports[`NavLink renders nav link with internal link 1`] = ` 10 | 11 | VuePress 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/DropdownTransition.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/NavLink.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 88 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Page.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 65 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarLinks.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 103 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/global-components/Badge.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Theme API. 4 | module.exports = (options, ctx) => { 5 | const { themeConfig, siteConfig } = ctx 6 | 7 | // resolve algolia 8 | const isAlgoliaSearch = ( 9 | themeConfig.algolia 10 | || Object 11 | .keys(siteConfig.locales && themeConfig.locales || {}) 12 | .some(base => themeConfig.locales[base].algolia) 13 | ) 14 | 15 | const enableSmoothScroll = themeConfig.smoothScroll === true 16 | 17 | return { 18 | alias () { 19 | return { 20 | '@AlgoliaSearchBox': isAlgoliaSearch 21 | ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') 22 | : path.resolve(__dirname, 'noopModule.js') 23 | } 24 | }, 25 | 26 | plugins: [ 27 | ['@vuepress/active-header-links', options.activeHeaderLinks], 28 | '@vuepress/search', 29 | '@vuepress/plugin-nprogress', 30 | ['container', { 31 | type: 'tip', 32 | defaultTitle: { 33 | '/': 'TIP', 34 | '/zh/': '提示' 35 | } 36 | }], 37 | ['container', { 38 | type: 'warning', 39 | defaultTitle: { 40 | '/': 'WARNING', 41 | '/zh/': '注意' 42 | } 43 | }], 44 | ['container', { 45 | type: 'danger', 46 | defaultTitle: { 47 | '/': 'WARNING', 48 | '/zh/': '警告' 49 | } 50 | }], 51 | ['container', { 52 | type: 'details', 53 | before: info => `
${info ? `${info}` : ''}\n`, 54 | after: () => '
\n' 55 | }], 56 | ['smooth-scroll', enableSmoothScroll] 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/layouts/404.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/noopModule.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vuepress/theme-default", 3 | "version": "1.2.0", 4 | "description": "Default theme for VuePress", 5 | "main": "index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vuejs/vuepress.git", 12 | "directory": "packages/@vuepress/theme-default" 13 | }, 14 | "keywords": [ 15 | "documentation", 16 | "vue", 17 | "vuepress", 18 | "generator" 19 | ], 20 | "author": "Evan You", 21 | "maintainers": [ 22 | { 23 | "name": "ULIVZ", 24 | "email": "chl814@foxmail.com" 25 | } 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/vuejs/vuepress/issues" 30 | }, 31 | "homepage": "https://github.com/vuejs/vuepress/packages/@vuepress/theme-default#readme", 32 | "dependencies": { 33 | "@vuepress/plugin-active-header-links": "^1.2.0", 34 | "@vuepress/plugin-nprogress": "^1.2.0", 35 | "@vuepress/plugin-search": "^1.2.0", 36 | "docsearch.js": "^2.5.2", 37 | "lodash": "^4.17.15", 38 | "stylus": "^0.54.5", 39 | "stylus-loader": "^3.0.2", 40 | "vuepress-plugin-container": "^2.0.2", 41 | "vuepress-plugin-smooth-scroll": "^0.0.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/arrow.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | .arrow 4 | display inline-block 5 | width 0 6 | height 0 7 | &.up 8 | border-left 4px solid transparent 9 | border-right 4px solid transparent 10 | border-bottom 6px solid $arrowBgColor 11 | &.down 12 | border-left 4px solid transparent 13 | border-right 4px solid transparent 14 | border-top 6px solid $arrowBgColor 15 | &.right 16 | border-top 4px solid transparent 17 | border-bottom 4px solid transparent 18 | border-left 6px solid $arrowBgColor 19 | &.left 20 | border-top 4px solid transparent 21 | border-bottom 4px solid transparent 22 | border-right 6px solid $arrowBgColor 23 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/code.styl: -------------------------------------------------------------------------------- 1 | {$contentClass} 2 | code 3 | color lighten($textColor, 20%) 4 | padding 0.25rem 0.5rem 5 | margin 0 6 | font-size 0.85em 7 | // background-color rgba(27,31,35,0.05) 8 | background-color $codeBgColor 9 | border-radius 3px 10 | .token 11 | &.deleted 12 | color #EC5975 13 | &.inserted 14 | color $accentColor 15 | 16 | {$contentClass} 17 | pre, pre[class*="language-"] 18 | line-height 1.4 19 | padding 1.25rem 1.5rem 20 | margin 0.85rem 0 21 | background-color $codeBgColor 22 | border-radius 6px 23 | overflow auto 24 | code 25 | color #fff 26 | padding 0 27 | background-color transparent 28 | border-radius 0 29 | 30 | div[class*="language-"] 31 | position relative 32 | background-color $codeBgColor 33 | border-radius 6px 34 | .highlight-lines 35 | user-select none 36 | padding-top 1.3rem 37 | position absolute 38 | top 0 39 | left 0 40 | width 100% 41 | line-height 1.4 42 | .highlighted 43 | background-color rgba(0, 0, 0, 66%) 44 | pre, pre[class*="language-"] 45 | background transparent 46 | position relative 47 | z-index 1 48 | &::before 49 | position absolute 50 | z-index 3 51 | top 0.8em 52 | right 1em 53 | font-size 0.75rem 54 | color rgba(255, 255, 255, 0.4) 55 | &:not(.line-numbers-mode) 56 | .line-numbers-wrapper 57 | display none 58 | &.line-numbers-mode 59 | .highlight-lines .highlighted 60 | position relative 61 | &:before 62 | content ' ' 63 | position absolute 64 | z-index 3 65 | left 0 66 | top 0 67 | display block 68 | width $lineNumbersWrapperWidth 69 | height 100% 70 | background-color rgba(0, 0, 0, 66%) 71 | pre 72 | padding-left $lineNumbersWrapperWidth + 1 rem 73 | vertical-align middle 74 | .line-numbers-wrapper 75 | position absolute 76 | top 0 77 | width $lineNumbersWrapperWidth 78 | text-align center 79 | color rgba(255, 255, 255, 0.3) 80 | padding 1.25rem 0 81 | line-height 1.4 82 | br 83 | user-select none 84 | .line-number 85 | position relative 86 | z-index 4 87 | user-select none 88 | font-size 0.85em 89 | &::after 90 | content '' 91 | position absolute 92 | z-index 2 93 | top 0 94 | left 0 95 | width $lineNumbersWrapperWidth 96 | height 100% 97 | border-radius 6px 0 0 6px 98 | border-right 1px solid rgba(0, 0, 0, 66%) 99 | background-color $codeBgColor 100 | 101 | 102 | for lang in $codeLang 103 | div{'[class~="language-' + lang + '"]'} 104 | &:before 105 | content ('' + lang) 106 | 107 | div[class~="language-javascript"] 108 | &:before 109 | content "js" 110 | 111 | div[class~="language-typescript"] 112 | &:before 113 | content "ts" 114 | 115 | div[class~="language-markup"] 116 | &:before 117 | content "html" 118 | 119 | div[class~="language-markdown"] 120 | &:before 121 | content "md" 122 | 123 | div[class~="language-json"]:before 124 | content "json" 125 | 126 | div[class~="language-ruby"]:before 127 | content "rb" 128 | 129 | div[class~="language-python"]:before 130 | content "py" 131 | 132 | div[class~="language-bash"]:before 133 | content "sh" 134 | 135 | div[class~="language-php"]:before 136 | content "php" 137 | 138 | @import '~prismjs/themes/prism-tomorrow.css' 139 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/config.styl: -------------------------------------------------------------------------------- 1 | $contentClass = '.theme-default-content' 2 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/custom-blocks.styl: -------------------------------------------------------------------------------- 1 | $pulseCodeBackground = #282e3f; 2 | .custom-block 3 | .custom-block-title 4 | font-weight 600 5 | margin-bottom -0.4rem 6 | &.tip, &.warning, &.danger 7 | padding .1rem 1.5rem 8 | border-left-width .5rem 9 | border-left-style solid 10 | margin 1rem 0 11 | &.tip 12 | background-color $blockBgColor 13 | border-color #42b983 14 | &.warning 15 | background-color #3b3510 16 | border-color darken(#ffe564, 35%) 17 | color darken(#ffe564, 10%) 18 | .custom-block-title 19 | color darken(#ffe564, 50%) 20 | a 21 | color $textColor 22 | &.danger 23 | background-color #4d1515 24 | border-color darken(red, 20%) 25 | color lighten(#4d1515, 60%) 26 | .custom-block-title 27 | color lighten(#4d1515, 80%) 28 | a 29 | color $textColor 30 | &.details 31 | display block 32 | position relative 33 | border-radius 2px 34 | margin 1.6em 0 35 | padding 1.6em 36 | background-color $pulseCodeBackground 37 | h4 38 | margin-top 0 39 | figure, p 40 | &:last-child 41 | margin-bottom 0 42 | padding-bottom 0 43 | summary 44 | outline none 45 | cursor pointer 46 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/mobile.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | $mobileSidebarWidth = $sidebarWidth * 0.82 4 | 5 | // narrow desktop / iPad 6 | @media (max-width: $MQNarrow) 7 | .sidebar 8 | font-size 15px 9 | width $mobileSidebarWidth 10 | .page 11 | padding-left $mobileSidebarWidth 12 | 13 | // wide mobile 14 | @media (max-width: $MQMobile) 15 | .sidebar 16 | top 0 17 | padding-top $navbarHeight 18 | transform translateX(-100%) 19 | transition transform .2s ease 20 | .page 21 | padding-left 0 22 | .theme-container 23 | &.sidebar-open 24 | .sidebar 25 | transform translateX(0) 26 | &.no-navbar 27 | .sidebar 28 | padding-top: 0 29 | 30 | // narrow mobile 31 | @media (max-width: $MQMobileNarrow) 32 | h1 33 | font-size 1.9rem 34 | {$contentClass} 35 | div[class*="language-"] 36 | margin 0.85rem -1.5rem 37 | border-radius 0 38 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/toc.styl: -------------------------------------------------------------------------------- 1 | .table-of-contents 2 | .badge 3 | vertical-align middle 4 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/wrapper.styl: -------------------------------------------------------------------------------- 1 | $wrapper 2 | max-width $contentWidth 3 | margin 0 auto 4 | padding 2rem 2.5rem 5 | @media (max-width: $MQNarrow) 6 | padding 2rem 7 | @media (max-width: $MQMobileNarrow) 8 | padding 1.5rem 9 | 10 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: 'Pulse' 4 | actionText: Walk me through → 5 | actionLink: /v4/introduction/what-is-pulse.html 6 | 7 | footer: MIT Licensed | Copyright © 2020 - Jamie Pine 8 | --- 9 |
10 | 11 | :tada: Pulse 4 is out now! [See changelog →](/v4/introduction/changelog.html) 12 | 13 | ```ts 14 | const hello = state("the moon is beautiful, isn't it") 15 | ``` 16 | 17 | 18 | With support for React, React Native, Vue, and an API to support more. 19 | 20 | 21 |
22 |
23 | 24 |
25 | -------------------------------------------------------------------------------- /docs/v1/developers/structure.md: -------------------------------------------------------------------------------- 1 | ## Structure 2 | 3 | This document should help explain the architecture of Pulse and allow developers to gain a better understanding of how it functions. 4 | 5 | ## _global Object 6 | 7 | ## Proxies 8 | 9 | Pulse uses Javascript proxies to handle the majority of reactivity. 10 | The `initProxy()` method on the Collection class is where that logic is located. 11 | This proxy is applied to the `_public` property of each collection. This property is exposed to the components and the `context` object for filters and actions to use. The proxy is also added to properties `_public.data`, `_public.groups` and `_public.filters`. 12 | 13 | On the proxy's `set` trap, we report access to certain values when required, it is used to determine which components access certain properties from Pulse ( `_global.componentDependencyGraph` ), and which internal properties are dependent on one another ( `_global.dependencyGraph` ). 14 | 15 | For example, when a filter is run we set `_global.record` to true, and then immediately back to false once execution is complete. The proxy's set trap will only record properties accessed if `record` is true. Now `_global.dependenciesFound` will contain the properties that filter is dependent on. We use a similar method for discovering component dependencies. 16 | 17 | ## Reactive Flow 18 | 19 | ### `deliverUpdate()` 20 | 21 | - **Purpose**: Sets data to the `_public` object, persists to local storage and updates the subscribed components 22 | - **Called By**: `executeFilter()`, `buildDataFromIndex()` 23 | - **Calls**: `updateSubscribers()` 24 | 25 | That takes care of groups and filters, but what about when data properties are changed? This is caught by the Proxy. 26 | 27 | The Proxy's set trap calls `updateSubscribers()` directly. -------------------------------------------------------------------------------- /docs/v1/examples/UsageWithReact.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/docs/v1/examples/UsageWithReact.md -------------------------------------------------------------------------------- /docs/v1/examples/UsageWithVueJS.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using with VueJS 3 | --- 4 | # Using with VueJS 5 | ```js 6 | // VueJS data property 7 | data() { 8 | return { 9 | ...this.mapData({ 10 | settings: 'accounts/settings' 11 | }) 12 | }, 13 | }, 14 | // VueJS computed methods are like Pulse filters, they're cached until one of their dependencies change. Here we're just writing a shortcut to return a boolean if `DARK_THEME` exists based on the Pulse data for `accounts/settings`. 15 | computed: { 16 | darkTheme() { 17 | return this.settings.DARK_THEME || false 18 | } 19 | // as `settings` is defined in mapData(), it will trigger this computed function to re-render when it changes. 20 | }, 21 | watch: { 22 | settings() { 23 | this.$accounts.updateTheme() 24 | } 25 | } 26 | ``` 27 | 28 | You may notice in that watcher I used `this.$accounts`. This is possible as every Pulse collection can be accessed on the Vue instance with the prefix `$`. You can use this to set data, read data in methods, and call actions. 29 | 30 | **Do not use \$ collection references in your template or computed properties, Vue does not see them as reactive, and will not trigger a re-render when Pulse data updates. This is why we have mapData()** 31 | 32 | The \$ references are there to make it easy to interact with Pulse data from the component, like calling actions and setting new values. 33 | 34 | ```JS 35 | // VueJS mounted hook 36 | mounted() { 37 | this.$collection.doSomething() 38 | this.$accounts.someValue = true 39 | }, 40 | methods: { 41 | doSomething() { 42 | this.$collection.someAction() 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/v1/examples/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authentication 3 | --- 4 | 5 | ## Basic authentication 6 | 7 | - Assuming you use a JWT as a Bearer token. 8 | - Remember in Pulse, "base" is the name of the root collection. 9 | - Actions, watchers, requestIntercept and responseIntercept all recieve the "context" object as the first parameter, allowing full access to anything within Pulse 10 | - Watchers can watch data, groups or filters, they should be the same name as the thing you're watching, whenever they change the watch function will run. 11 | 12 | ```js 13 | import Pulse from 'pulse-framework'; 14 | 15 | const core = new Pulse.Library({ 16 | // settings for the request 17 | request: { 18 | baseURL: 'https://api.mysite.me', 19 | 20 | // do something before each request 21 | requestIntercept({ base }, options) {}, 22 | 23 | // do something after each request 24 | responseIntercept({ base }, response) { 25 | if (response.status === 401) base.isAuthenticated = false; 26 | } 27 | }, 28 | // define a route for login 29 | routes: { 30 | login: (request, creds) => request.post('login', creds) 31 | }, 32 | data: { 33 | token: null 34 | }, 35 | persist: ['token'], 36 | watch: { 37 | // when the token changes, update the request handler 38 | token({ data, request }) { 39 | request.headers.Bearer = `Bearer ${data.token}`; 40 | } 41 | }, 42 | actions: { 43 | // this action calls the login route, then saves the token to the data 44 | login({ routes, data }, creds) { 45 | return routes.login(creds).then(res => (data.token = res.token)); 46 | } 47 | } 48 | }); 49 | 50 | // Call login 51 | core.base.login({ username: 'jamie', password: 'jeff' }); 52 | ``` -------------------------------------------------------------------------------- /docs/v1/getting-started/setup-with-vue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Vue 3 | --- 4 | 5 | # Install 6 | 7 | ``` 8 | npm i pulse-framework --save 9 | ``` 10 | 11 | Firstly create your [Pulse library](/guide/library.html), here we're going to make a file named `pulse.js`, but you can call it whatever you want. In this file we'll configure & initialize the Pulse library and export it so your components can use it. 12 | 13 | ```js 14 | import Pulse from 'pulse-framework'; 15 | 16 | const pulse = new Pulse.Library({ 17 | collections: { 18 | myCollection: { 19 | data: { 20 | thing: false 21 | } 22 | } 23 | } 24 | }); 25 | 26 | export default pulse; 27 | ``` 28 | 29 | Somewhere in your Vue project you're going to need to import this file and call `Vue.use(pulse)`, this will install Pulse into Vue. 30 | 31 | ```js 32 | import Vue from 'vue'; 33 | import pulse from '../'; 34 | 35 | Vue.use(pulse); 36 | ``` 37 | 38 | Now you can use [mapData](./guide/using-data.html) to bring data into your Vue component. mapData is accessible under `this`, since we've installed it into Vue. 39 | 40 | ```js 41 | export default { 42 | name: 'My Vue Component', 43 | data() { 44 | return { 45 | ...this.mapData({ 46 | something: 'myCollection/thing' 47 | }) 48 | }; 49 | } 50 | }; 51 | ``` 52 | 53 | Since mapped data is immutable within the component, to mutate data you'll need to call the collection directly. In Vue, this is as easy as calling the collection using `$`. 54 | 55 | ```js 56 | this.$myCollection.thing = true; 57 | ``` 58 | 59 | Remember, we've mapped `thing` to `something` locally in our Vue component, so for it to be reactive we must use `this.something` inside the template or computed methods. 60 | 61 | ::: tip Summary 62 | The main thing to learn is that mapData() is reactive, `$` is not- though we need to use the `$` to make mutations and call actions. 63 | ::: 64 | -------------------------------------------------------------------------------- /docs/v1/guide/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Actions 3 | --- 4 | 5 | ### Actions 6 | 7 | Actions are simply functions within your pulse collections that can be called externally. 8 | 9 | Actions receive a context object (see [Context Object](#context-object)) as the first parameter, this includes every registered collection by name, the routes object and all default collection functions. 10 | 11 | ```js 12 | actionName({ collectionOne, collectionTwo }, customParam, ...etc) { 13 | // do something 14 | collectionOne.collect 15 | collectionTwo.anotherAction() 16 | collectionTwo.someOtherData = true 17 | }; 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/v1/guide/context-object.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Context Object 3 | --- 4 | 5 | ### Context Object 6 | 7 | [Filters](/guide/filters.html), [actions](/guide/actions.html) and [watchers](/guide/watchers.html) receive the "context" object the first parameter. 8 | 9 | | Name | Type | Description | Filters | Actions | 10 | | ------------------ | --------- | ---------------------------------------------------------------------------------------------------------- | ------- | ------- | 11 | | Collection Objects | Object(s) | For each collection within pulse, this is its public data and functions. | True | True | 12 | | routes | Object | The local routes for the current collection. | False | True | 13 | | actions | Object | The local actions for the current collection. | True | True | 14 | | filters | Object | The local filters for the current collection. | True | True | 15 | | groups | Object | The local groups for the current collection. | True | True | 16 | | findById | Function | A helper function to return data directly by primary key. | True | True | 17 | | collect | Function | The collect function, to save data to this collection. | False | True | 18 | | put | Function | Insert data into a group by primary key. | False | True | 19 | | move | Function | Move data from one group to another. | False | True | 20 | | update | Function | Mutate properties of a data entry by primary key. | False | True | 21 | | delete | Function | Delete data. | False | True | 22 | | deleteGroup | Function | Delete data in a group | False | True | 23 | | clear | Function | Remove unused data. | False | True | 24 | | undo | Function | Revert all changes made by this action. | False | True | 25 | | throttle | Function | Used to prevent an action from running more than once in a specified time frame. EG: throttle(2000) for 2s | False | True | 26 | | purge | Function | Clears all collection data and empties groups. | False | True | 27 | -------------------------------------------------------------------------------- /docs/v1/guide/data-relations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data Relations 3 | --- 4 | 5 | ### Data Relations 6 | 7 | Creating data relations between collections is easy and extremely useful. 8 | 9 | But why would you need to create data relations? The simple answer is keeping to our rule that data should not be repeated, but when it is needed in multiple places we should make it dependent on a single copy of that data, which when changed, causes any dependencies using that data to regenerate. 10 | 11 | Let's say you have a `channel` and a several `posts` which have been made by that channel. In the post object you have an `owner` property, which is a channel id (the primary key). We can establish a relation between that `owner` id and the primary key in the channel collection. Now when groups or filters are generated for the posts collection, each piece of data will include the full `channel` object. 12 | 13 | When that channel is modified, any groups containing a post dependent on that channel will regenerate, and filters dependent on those groups will regenerate also. 14 | 15 | Here's a full example using the names I referenced above. 16 | 17 | ```js 18 | collections: { 19 | posts: { 20 | model: { 21 | owner: { 22 | hasOne: 'channels', // name of the sister collection 23 | assignTo: 'channel;' // the local property to assign the channel data to 24 | } 25 | } 26 | }, 27 | channels: {} // etc.. 28 | } 29 | ``` 30 | 31 | That's it! It just works. 32 | 33 | A situation where this proved extremely satisfying, was updating a channel avatar on the Notify app, every instance of that data changed reactively. Here's a gif of that in action. 34 | 35 | ![Gif showing reactivity using Pulse relations](https://i.imgur.com/kDjkHNx.gif 'All instances of the avatar update when the source is changed, including the related posts from a different collection.') 36 | -------------------------------------------------------------------------------- /docs/v1/guide/debugging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Degugging 3 | --- 4 | 5 | ### Degugging 6 | 7 | ::: tip Coming soon... 8 | We're planning to work on a dev tools for Pulse soon, if you want to contribute please join the [Discord](https://discord.gg/Huhe48c) 9 | ::: 10 | 11 | For now to debug Pulse you'll need to use the console. Pulse is accessible directly in the console by typing `_pulse`, this is because a refrence to Pulse is bound to the `window` object. 12 | 13 | ## Logging 14 | 15 | When you log out the instance of Pulse you'll notice there are properties prefixed with an underscore, these are internal properties and should only be modified by Pulse itself. When debugging, unless you know what you're doing, stick to the collections without the `_`. 16 | 17 | ## What is a Proxy? 18 | 19 | You'll see certain objects inside Pulse are marked "Proxy", and it might look weird in the console. Proxies are awesome for reactivity, but they look ugly in the console which is a shame (another reason for Pulse dev tools!). 20 | 21 | If you want to see the _actual_ properties of the object, they exist in the `[[handler]]` section, so tab that down and you'll see what you're looking for. 22 | 23 | They work **exactly** the same as normal objects- you can directly modify their properties,stringify them, use Object.keys() etc... and the proxy will not get in the way. 24 | -------------------------------------------------------------------------------- /docs/v1/guide/filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filters 3 | --- 4 | 5 | ### Filters 6 | 7 | Filters allow you to alter data before passing it to your component without changing the original data. Essentially getters in VueX. 8 | 9 | They're cached for performance, meaning the output of the filter function is what gets served to the component, so each time it is accessed the entire filter doesn't need to re-run. 10 | 11 | Each filter is analyzed to see which data properties they depend on, and when those data properties change the appropriate filters regenerate. 12 | 13 | ```js 14 | channels: { 15 | groups: ['subscribed'], 16 | filters: { 17 | liveChannels({ groups }) => { 18 | return groups.subscribed.filter(channel => channel.live === true) 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | Filters have access to the context object (see [Context Object](/guide/context-object.html)) as the first parameter. 25 | 26 | Filters can also be dependent on each other via the context object. 27 | -------------------------------------------------------------------------------- /docs/v1/guide/http-requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HTTP Requests 3 | --- 4 | 5 | ### HTTP Requests 6 | 7 | Pulse completely replaces the need to use a third party HTTP request library such as Axios. Define endpoints within your collection and easily handle the response and collect the data. 8 | 9 | First you must define your `baseURL` in the request config (in the root of your Pulse library): 10 | 11 | ```js 12 | request: { 13 | baseURL: 'https://api.notify.me' 14 | headers: { 15 | 'Access-Control-Allow-Origin': 'https://notify.me' 16 | //etc.. 17 | } 18 | } 19 | // for context ... 20 | collections: {} 21 | storage: {} 22 | //etc.. 23 | ``` 24 | 25 | Now you can define routes in your collections: 26 | 27 | ```js 28 | routes: { 29 | getStuff: request => request.get('stuff/something'); 30 | } 31 | ``` 32 | 33 | Each route takes in the request object as the first parameter, which contains HTTP methods like, GET, POST, PATCH, DELETE etc. 34 | 35 | Route functions are promises, meaning you can either use then() or async/await. 36 | 37 | You can access routes externally or within Pulse actions. 38 | 39 | ```js 40 | collection.routes.getStuff(); 41 | ``` 42 | 43 | ```js 44 | actions: { 45 | doSomething({collection, routes}) { 46 | return routes.getStuff().then(res => { 47 | collection.collect(res.data) 48 | }) 49 | } 50 | } 51 | ``` 52 | 53 | The request library is an extension of a collection, meaning it's built on top of the collection class. It's exposed on the instance the same way as a collection, data such as `baseURL` and the `headers` can be changed on the fly. 54 | 55 | ```js 56 | request.baseURL = 'https://api.notify.gg'; 57 | 58 | request.headers['Origin'] = 'https://notify.me'; 59 | ``` 60 | 61 | Request history is saved (collected) into the request collection by default, though this can be disabled: 62 | 63 | ```js 64 | request: { 65 | saveHistory: false; 66 | } 67 | ``` 68 | 69 | HTTP requests will eventually have many more useful features, but for now basic function is implemented. 70 | -------------------------------------------------------------------------------- /docs/v1/guide/library.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pulse Library 3 | --- 4 | 5 | # Pulse Library 6 | 7 | The "Library" refers to the Pulse configuration files, this is where you define and configure collections and basic config for everything within Pulse. 8 | 9 | The library itself is an object, the `Pulse.Library` constructor takes it as the only parameter. 10 | 11 | ```js 12 | 13 | import Pulse from 'pulse-framework' 14 | 15 | export default new Pulse.Library({ 16 | config: { 17 | framework: 'vue' 18 | } 19 | collections: { 20 | // A collection named "test" 21 | test: { 22 | data: { 23 | hi: true 24 | }, 25 | actions: { 26 | doSomething({ data }) { 27 | return data.hi 28 | } 29 | } 30 | } 31 | } 32 | }) 33 | 34 | ``` 35 | 36 | ::: tip Tip 37 | We export the initialized Pulse library so that it can be imported into our components, which is necessary in React though not so much in Vue. 38 | ::: 39 | 40 | For small applications you can keep this in one or two files like shown above, but a medium to large application building out a file structure like this might be preferred: 41 | 42 | ``` 43 | ├── library 44 | | ├── index.js 45 | | ├── request.js 46 | | ├── channels 47 | | | └── index.js 48 | | | └── channel.collection.js 49 | | | └── channel.actions.js 50 | | | └── channel.filters.js 51 | | | └── channel.model.js 52 | | ├── services 53 | | | └── ... 54 | | ├── utils 55 | | | └── ... 56 | 57 | ``` 58 | 59 | You're free to do whatever suits your project. 60 | 61 | ### Tree example 62 | 63 | This is everything currently supported by the Pulse Library and how it fits in the object tree, use this as a reference when building your library to ensure you put everything in the right place. 64 | 65 | ```js 66 | const pulse = new Pulse.Library({ 67 | collections: { 68 | collectionOne: {}, 69 | collectionTwo: { 70 | // example 71 | model: {}, 72 | data: {}, 73 | groups: [], 74 | persist: [], 75 | routes: {}, 76 | actions: {}, 77 | filters: {}, 78 | watch: {} 79 | }, 80 | collectionThree: {} 81 | //etc.. 82 | }, 83 | request: { 84 | baseURL: 'https://api.notify.me', 85 | headers: [] 86 | }, 87 | services: {}, // coming soon 88 | utils: {}, // coming soon 89 | jobs: {} 90 | 91 | // base 92 | model: {}, 93 | data: {}, 94 | groups: [], 95 | persist: [], 96 | routes: {}, 97 | actions: {}, 98 | filters: {}, 99 | watch: {} 100 | }); 101 | ``` 102 | -------------------------------------------------------------------------------- /docs/v1/guide/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Models 3 | --- 4 | 5 | ### Models 6 | 7 | Collections allow you to define models for the data that you collect. This is great for ensuring valid data is always passed to your components. It also allows you to define data relations between collections, as shown in the next section. 8 | 9 | Here's an example of a model: 10 | 11 | ```js 12 | collection: { 13 | model: { 14 | id: { 15 | // id is the default primary key, but you can set another 16 | // property to a primary key if your data is different. 17 | primaryKey: true; 18 | type: Number; // coming soon 19 | required: true; // coming soon 20 | } 21 | } 22 | } 23 | ``` 24 | 25 | Data that does not fit the model requirements you define will not be collected, it will instead be saved in the Errors object as a "Data Rejection", so you can easily debug. 26 | -------------------------------------------------------------------------------- /docs/v1/guide/mutating-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mutating Data 3 | --- 4 | 5 | ### Mutating Data 6 | 7 | Changing data in Pulse is easy, you just set it to a new value. 8 | 9 | ```js 10 | collection.currentlyEditingChannel = true; 11 | ``` 12 | 13 | We don't need mutation functions like VueX's "commit" because we use Proxies to intercept changes and queue them to prevent race conditions. Those changes are stored and can be reverted easily. (Intercepting and queueing coming soon) 14 | -------------------------------------------------------------------------------- /docs/v1/guide/namespacing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Namespacing 3 | --- 4 | 5 | ### Namespacing 6 | 7 | Pulse has the following namespaces for each collection 8 | 9 | - Groups (cached data based on arrays of primary keys) 10 | - Data (custom data, good for stuff related to a collection, but not part the main body of data like booleans and strings) 11 | - Filters (like VueX getters, these are cached data based on filter functions you define) 12 | - Actions (functions to do stuff) 13 | 14 | By default, you can access everything under the collection namespace, like this: 15 | 16 | ```js 17 | collection.groupName; 18 | collection.someDataName; 19 | collection.filterName; 20 | collection.doSomething(); 21 | ``` 22 | 23 | But if you prefer to separate everything by type, you can access areas of your collection like so: 24 | 25 | ```js 26 | collection.groups.groupName; 27 | collection.data.someDataName; 28 | collection.filters.filterName; 29 | collection.actions.doSomething(); 30 | ``` 31 | 32 | For groups, if you'd like to access the raw array of primary keys, instead of the constructed data you can under `indexes`. 33 | 34 | ```js 35 | collection.indexes.groupName; // EG: [ 123, 1435, 34634 ] 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/v1/guide/persisting-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persisting Data 3 | --- 4 | 5 | ### What is Persisting? 6 | 7 | It's a common need for applications to store little pieces of data on the clients browser, Pulse makes it beyond easy to achieve this. Simply putting the name of a data property in the `persist` array on your collection will store it in local storage. On initialization properties saved in local storage will automatically be loaded back into state. 8 | 9 | ```js 10 | collection: { 11 | data: { 12 | haha: true; 13 | } 14 | persist: ['haha']; 15 | } 16 | ``` 17 | 18 | Pulse will only save the data property into local storage if it has been set to something other than the original value defined in the collection. 19 | 20 | ::: tip Note 21 | Currently it is not possible to persist data collected using the `collect` method, this would be better suited for "indexed storage", as local storage requires stringifying the data. If you need this functionality consider opening an issue or making a PR yourself. 22 | ::: 23 | Pulse integrates directly with local storage and session storage, and even has an API to configure your own storage. 24 | 25 | ```js 26 | { 27 | collections: {...} 28 | // use session storage 29 | storage: 'sessionStorage' 30 | // use custom storage 31 | storage: { 32 | async: false, 33 | set: ... 34 | get: ... 35 | remove: ... 36 | clear: ... 37 | } 38 | } 39 | ``` 40 | 41 | Local storage is the default and you don't need to define a storage object for it to work. 42 | 43 | ::: warning React Native & non browser users: 44 | Some environments, such as React Native, do not have local storage. You must bind a custom storage solution as shown above, in React Native you can use Async Storage. If your storage solution is asyncronous, you can toggle that there to be sure, otherwise Pulse will attempt to detect it. 45 | ::: 46 | 47 | More features will be added to data persisting soon, such as persisting entire collection data, custom storage per collection and more configuration options. 48 | -------------------------------------------------------------------------------- /docs/v1/guide/using-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Data 3 | --- 4 | 5 | ### Using data with mapData() 6 | 7 | Using data in VueJS and React is simple with `mapData()`. It will return an object containing Pulse data properties that you request. The string must contain a slash, first the name of the collection, then the data property. 8 | 9 | ```js 10 | pulse.mapData({ 11 | localName: 'collection/property' 12 | }); 13 | // returns: { localName: value } 14 | ``` 15 | 16 | You can set `localName` to anything that suits your component. 17 | 18 | You can now bind each returned property to the data in your component using object spreading. In VueJS the `mapData()` function is available on the Vue instance as `this.mapData()`, in React you must import it. 19 | 20 | ::: tip More Info 21 | To see how mapData can be integrated with your components, see: Setup with [React](/getting-started/setup-with-react.html) / [Vue](/getting-started/setup-with-vue.html) 22 | ::: 23 | 24 | `mapData()` has access to all public facing **data, filters, groups, indexes** and even **actions**. Using mapData enures this component is tracked as a dependency inside Pulse so that it can be reactive. 25 | 26 | mapData should be injected into the component's state, so you can access your data inside your component using the `localName` that you define in the mapData object. 27 | 28 | **Note: `mapData()` is read-only.** To mutate data or call actions, you must use the Pulse instance itself. A good way is to export the Pulse instance and import it into your component as shown earlier. 29 | 30 | In Vue, mapped data can be used in computed methods and even trigger Vue watchers, just like regular Vue data. 31 | 32 | In React, data should be mapped to state, and it is compatible with React hooks. 33 | -------------------------------------------------------------------------------- /docs/v1/introduction/changelog.md: -------------------------------------------------------------------------------- 1 | # 2.0 - Internal rewrite 2 | 3 | Pulse version two is a complete ground-up rewrite. For the most part it should not affect your code, it is indeed backwards compatible. However there are a few things that have changed externally that you should know about before updating to V2. 4 | 5 | ## Breaking changes 6 | 7 | These are changes that could 8 | 9 | - Namespacing changes (see below) 10 | - Model relations "hasOne, hasMany" removed in place of populate() function (need to update docs....) 11 | - Constructor changed from `Pulse.Library()` to just `Pulse()` 12 | - "Filters" renamed to "Computed" although using "filters" as a property name on your collections still works. 13 | - remove() renamed to removeFromGroup() 14 | 15 | ## New Features (docs soon) 16 | 17 | - Global events 18 | - Added more config options 19 | - Better debugging helpers 20 | - Added more defaults to Base class 21 | 22 | ## Namespacing updates 23 | 24 | Reactive properties on collections are no longer accessible under their type names, so a data property on a collection called `thing` is now only accessible via `collection.thing`. Before a reactive copy (or alias) of a data property could be found under `collection.data.thing`. With the new reactivity system 25 | 26 | ## Main Improvements Under The Hood 27 | 28 | - Written in TypeScript 29 | - Improved internal structure 30 | - Internal architecture now follows a clear structure with an efficient job queuing system. Code broken up into classes to group logic and de-spaghettify code. 31 | - Added component update squashing 32 | - Prevents repeat component updates by waiting until all jobs are complete before updating subscribed components (Vue, React), squashing updates together per component. 33 | - Removed all Javascript proxies 34 | - **Why:** Proxies are new to javascript, they allow developers to do more with reactive objects but are not supported by many environments, including React Native's new JS engine "Hermes". Pulse now uses getters & setters which is the same system as Vue. 35 | -------------------------------------------------------------------------------- /docs/v1/introduction/what-is-pulse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is Pulse? 3 | --- 4 | 5 | ::: warning NOTE 6 | Pulse is still in development, some features are not working yet. In this document they're marked as "coming soon". 7 | ::: 8 | 9 | Pulse is an application logic library for reactive Javascript frameworks with support for VueJS, React and React Native. It is lightweight, modular and powerful, but most importantly easy to understand. 10 | 11 | Pulse replaces global state management solutions such as Redux, VueX and MobX, including HTTP libraries such as Axios, Fetch or Request.js. It makes your application more modular, ensuring you follow the best practices while writing significantly less code. 12 | 13 | ## Why Pulse? 14 | 15 | After exploring the many options for Javascript state libraries, including the popular VueX and Redux, I felt like I needed a simpler solution. I wanted to get more out of a library than just state management― something that could provide solid structure for the **entire** application. It needed to be structured and simple, but also scalable. This library provides everything needed to get a reactive javascript front-end application working fast, taking care to follow best practices and to employ simple terminology that makes sense even to beginners. 16 | 17 | I built this framework reflective of the architecture in which we use at Notify.me, and as a replacement for VueX at Notify also, making sure it is also compatible with React and vanilla environments. The team at Notify love it and I think you will too. 18 | 19 | ## Features 20 | 21 | - :gear: Modular structure using "collections" 22 | - :zap: Cached data & filters with dependency based regeneration 23 | - :sparkles: Automatic data normalization 24 | - :lock: Model based data validation 25 | - :timer_clock: History tracking with smart undo functions 26 | - :crystal_ball: Create data relations between collections 27 | - :nerd_face: Database style functions 28 | - :gem: SSOT architecture (single source of truth) 29 | - :closed_book: Error logging & snapshot bug reporting 30 | - :wrench: Wrappers for helpers, utilities and service workers 31 | - :construction: Task queuing for race condition prevention 32 | - :telephone_receiver: Promise based HTTP requests and websocket connections (web sockets coming soon) 33 | - :hourglass_flowing_sand: Timed interval task handler (coming soon) 34 | - :bus: Event bus (coming soon) 35 | - :floppy_disk: Persisted data API for localStorage, sessionStorage & more 36 | - :key: Optional pre-built authentication layer 37 | - :leaves: Lightweight (only 22KB) with 0 dependencies 38 | - :fire: Supports Vue, React and React Native 39 | - :yellow_heart: Well documented (I'm getting there...) 40 | 41 | ## Is Pulse for you? 42 | 43 | The most attractive part of Pulse for me personally is how easy it is to work with, which makes it good for a variety of different projects. Though it does scale well for applications that have many different types of data. 44 | -------------------------------------------------------------------------------- /docs/v2/developers/ideas.md: -------------------------------------------------------------------------------- 1 | ```js 2 | import Pulse from 'pulse-framework'; 3 | 4 | const pulse = new Pulse(); 5 | 6 | pulse.on('EVENT_NAME', this, payload => console.log(payload)); 7 | 8 | pulse.emit('EVENT_NAME', {}); 9 | 10 | export const collection = pulse.createCollection(); 11 | 12 | export const someData = collection 13 | .createData(defaultValue) 14 | .persist() 15 | .watch(() => {}); 16 | 17 | export const myGroup = collection.createGroup([]); 18 | 19 | someData.value; 20 | someData.set(); 21 | someData.subscribe(this); 22 | 23 | collection.createAction('getChannels', () => {}); 24 | 25 | export const myComputedValue = collection.createComputed(() => {}, [someData]); 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/v2/developers/structure.md: -------------------------------------------------------------------------------- 1 | ## Structure 2 | 3 | This document should help explain the architecture of Pulse and allow developers to gain a better understanding of how it functions. 4 | 5 | ## _global Object 6 | 7 | ## Proxies 8 | 9 | Pulse uses Javascript proxies to handle the majority of reactivity. 10 | The `initProxy()` method on the Collection class is where that logic is located. 11 | This proxy is applied to the `_public` property of each collection. This property is exposed to the components and the `context` object for filters and actions to use. The proxy is also added to properties `_public.data`, `_public.groups` and `_public.filters`. 12 | 13 | On the proxy's `set` trap, we report access to certain values when required, it is used to determine which components access certain properties from Pulse ( `_global.componentDependencyGraph` ), and which internal properties are dependent on one another ( `_global.dependencyGraph` ). 14 | 15 | For example, when a filter is run we set `_global.record` to true, and then immediately back to false once execution is complete. The proxy's set trap will only record properties accessed if `record` is true. Now `_global.dependenciesFound` will contain the properties that filter is dependent on. We use a similar method for discovering component dependencies. 16 | 17 | ## Reactive Flow 18 | 19 | ### `deliverUpdate()` 20 | 21 | - **Purpose**: Sets data to the `_public` object, persists to local storage and updates the subscribed components 22 | - **Called By**: `executeFilter()`, `buildDataFromIndex()` 23 | - **Calls**: `updateSubscribers()` 24 | 25 | That takes care of groups and filters, but what about when data properties are changed? This is caught by the Proxy. 26 | 27 | The Proxy's set trap calls `updateSubscribers()` directly. -------------------------------------------------------------------------------- /docs/v2/docs/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pulse Concepts 3 | --- 4 | 5 | ## Structure 6 | 7 | Pulse was designed to take all business logic out of components, meaning your React/Vue/Angular components are essencially puppets for Pulse to orchestrate. The benifit of keeping logic seperate to visual components is versitility, upgradablity and cleanliness. An example would be with Notify's codebase, we built most of our app using Pulse in a repository called "core" and then our components are mostly markup, css and a router file. Both our mobile app in React Native and our webapp in Vue behave exactly the same aside from a few visual differences, as both use the core. 8 | 9 | ## Reactivity 10 | 11 | Reactive data is state that will react to mutations in order to cause component re-renders. More traditional programming styles will use a function to "update state" (like setState in React), whereas frameworks like Vue and Pulse modify the object in such away that tracks for changes automatically, causing components that have "subscribed" to that data to re-render, as well as other interal side-effects within Pulse which will be explained later in these docs (Computed data and Watchers). 12 | 13 | In Pulse the simplest example possible would look like this: 14 | 15 | ```js 16 | import Pulse from 'pulse-framework'; 17 | 18 | const pulse = new Pulse({ 19 | data: { 20 | something: true 21 | } 22 | }); 23 | ``` 24 | 25 | Changing that data can now be done as if it were a plain Javascript object: 26 | 27 | ```js 28 | pulse.something = false; 29 | ``` 30 | 31 | The typical way to use Pulse is with another framework, learn how to intergrate this into [React]() or [Vue]() for automatic component re-renders when Pulse data changes. 32 | 33 | A manual way to listen for a state change would be using `watch()` 34 | 35 | ```js 36 | pulse.watch('something', ({ data }) => { 37 | if (data.something) // do something only if true 38 | }) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/v2/docs/debugging.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Debugging 3 | --- 4 | 5 | ### Debugging 6 | 7 | ::: tip Coming soon... 8 | We're planning to work on a dev tools for Pulse soon, if you want to contribute please join the [Discord](https://discord.gg/Huhe48c) 9 | ::: 10 | 11 | For now to debug Pulse you'll need to use the console. Pulse is accessible directly in the console by typing `_pulse`, this is because a refrence to Pulse is bound to the `window` object. 12 | -------------------------------------------------------------------------------- /docs/v2/docs/http-requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request API 3 | --- 4 | 5 | ### HTTP Request API 6 | 7 | Pulse replaces the need to use a third party HTTP request library such as Axios. Define endpoints within your modules & collections, then easily handle the response and process response data. 8 | 9 | The request object goes in the root of the Pulse config. 10 | 11 | ```js 12 | request: { 13 | baseURL: 'https://api.notify.me' 14 | headers: { 15 | 'Access-Control-Allow-Origin': 'https://notify.me' 16 | //etc.. 17 | } 18 | } 19 | // for context ... 20 | modules: {} 21 | collections: {} 22 | storage: {} 23 | //etc.. 24 | ``` 25 | 26 | Now you can define a routes object in your modules & collections: 27 | 28 | ```js 29 | routes: { 30 | getStuff: request => request.get('stuff/something'), 31 | postStuff: (request, body) => request.post('stuff/something', body) 32 | } 33 | ``` 34 | 35 | Each route takes in the request object as the first parameter, which contains HTTP methods like, GET, POST, PATCH, DELETE etc. 36 | 37 | Any parameters passed to the route function will be available after the "request" param. 38 | 39 | Route functions are promises, meaning you can either use then() or async/await. 40 | 41 | You can access routes externally or within Pulse actions. 42 | 43 | ```js 44 | collection.routes.getStuff(); 45 | ``` 46 | 47 | ```js 48 | actions: { 49 | doSomething({collection, routes}) { 50 | return routes.getStuff().then(res => { 51 | collection.collect(res.data) 52 | }) 53 | } 54 | } 55 | ``` 56 | 57 | The request library is an extension of a module, meaning it's built on top of the Module class. So data such as `baseURL` and the `headers` can be changed reactively. 58 | 59 | ```js 60 | request.baseURL = 'https://api.notify.gg'; 61 | 62 | request.headers['Origin'] = 'https://notify.me'; 63 | ``` 64 | 65 | ## Request Interceptors 66 | 67 | Two useful hooks to modify requests before they're sent, and the responses as they're recieved. Here's an example as used for authentication. 68 | 69 | ```js 70 | request: { 71 | baseURL: 'http://localhost:3000', 72 | // static headers 73 | headers: { 74 | 'Access-Control-Allow-Origin': 'https://notify.me', 75 | } 76 | requestIntercept({ data, accounts }) { 77 | // inject the auth token from a collection/module called "accounts" 78 | data.headers.token = accounts.jwtToken; 79 | }, 80 | responseIntercept({ error }, response) { 81 | // if the request was not successful send to a custom error handler module 82 | if (!response.ok) error.handle(response.data) 83 | } 84 | }, 85 | ``` 86 | 87 | Since the headers object is static, the interceptors can be used to dynamically inject data from anywhere in Pulse into your request. 88 | -------------------------------------------------------------------------------- /docs/v2/docs/module-methods.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Module Methods 3 | --- 4 | 5 | ## About Module Methods 6 | 7 | Modules have some out-of-the-box functionality, you can use these functions in the [Context Object](/v2/docs/context-object.html) or as `module.functionName()`. 8 | 9 | This also means these functions are part of the [Module Namespace](/v2/docs/modules.html#namespacing), so don't create any data or actions with the same name as one of these functions! 10 | 11 | ## `forceUpdate()` 12 | 13 | ## `watch()` 14 | 15 | ## `throttle()` 16 | 17 | ## `addStaticData()` 18 | 19 | ## `undo()` 20 | 21 | ::: warning 22 | Undo is ONLY useable from the [Context Object](/v2/docs/context-object.html), and not from outside the module. This is because `undo()` is relevent to the specific action it was called in, see [Actions](/v2/docs/modules.html#actions) 23 | ::: 24 | 25 | ![Undo in Pulse](https://i.imgur.com/wSAkxuX.png) 26 | -------------------------------------------------------------------------------- /docs/v2/docs/persisting-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persisting Data 3 | --- 4 | 5 | ### What is Persisting? 6 | 7 | It's a common need for applications to store little pieces of data on the clients browser, Pulse makes it beyond easy to achieve this. Simply putting the name of a data property in the `persist` array on your collection will store it in local storage. On initialization properties saved in local storage will automatically be loaded back into state. 8 | 9 | ```js 10 | collection: { 11 | data: { 12 | haha: true; 13 | } 14 | persist: ['haha']; 15 | } 16 | ``` 17 | 18 | Pulse will only save the data property into local storage if it has been set to something other than the original value defined in the collection. 19 | 20 | ::: tip Note 21 | Currently it is not possible to persist data collected using the `collect` method, this would be better suited for "indexed storage", as local storage requires stringifying the data. If you need this functionality consider opening an issue or making a PR yourself. 22 | ::: 23 | Pulse integrates directly with local storage and session storage, and even has an API to configure your own storage. 24 | 25 | ```js 26 | { 27 | collections: {...} 28 | // use session storage 29 | storage: 'sessionStorage' 30 | // use custom storage 31 | storage: { 32 | async: false, 33 | set: ... 34 | get: ... 35 | remove: ... 36 | clear: ... 37 | } 38 | } 39 | ``` 40 | 41 | Local storage is the default and you don't need to define a storage object for it to work. 42 | 43 | ::: warning React Native & non browser users: 44 | Some environments, such as React Native, do not have local storage. You must bind a custom storage solution as shown above, in React Native you can use Async Storage. If your storage solution is asyncronous, you can toggle that there to be sure, otherwise Pulse will attempt to detect it. 45 | ::: 46 | 47 | More features will be added to data persisting soon, such as persisting entire collection data, custom storage per collection and more configuration options. 48 | -------------------------------------------------------------------------------- /docs/v2/docs/using-pulse-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Pulse Data 3 | --- 4 | 5 | ::: tip More Info 6 | To see how Pulse can be integrated with your components, see: Setup with [React](/getting-started/setup-with-react.html) / [Vue](/getting-started/setup-with-vue.html) 7 | ::: 8 | 9 | Pulse can be used in many different ways, you can directly intergrate with a framework, which will have it's own unique API; in React we use `Pulse.React()` and as a wrapper for a component, and in Vue we use `this.mapData()` and deconstruct it into Vue's data function. 10 | 11 | However if you're looking for a more manual way to interact and work with data within Pulse, continue reading... 12 | 13 | TODO: write about manual intergration using `subscribe()` and `watch()`. 14 | -------------------------------------------------------------------------------- /docs/v2/examples/UsageWithReact.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using with React 3 | --- 4 | 5 | ## Current Pulse implementation with React 6 | 7 | ```js 8 | import React from 'react'; 9 | import core from 'logic/core'; 10 | 11 | function myComponent({ pulse }) { 12 | const { jeff } = pulse; 13 | 14 | return
; 15 | } 16 | 17 | export default core.wrapped(myComponent, (core) => { 18 | return { 19 | jeff: core.jeff; 20 | } 21 | }) 22 | ``` 23 | 24 | ## New Pulse potential implementation with React 25 | 26 | ```js 27 | import React from 'react'; 28 | import core from 'logic/core'; 29 | 30 | export default function myComponent() { 31 | // first line is always core.subscribe 32 | 33 | // useEffect is a react hook for function components. It fires when mounted and the return value fires when unmounted if it is a function and the second param is an empty array 34 | useEffect(() => { 35 | core.subscribe(this, () => [core.jeff]); 36 | return () => core.unsubscribe(this); 37 | }, []); 38 | 39 | // if core.unscubscribe returned the unsubscribe function we could do this: 40 | useEffect(() => core.subscribe(this, () => [core.jeff]), []); 41 | 42 | // ^^ EPIC 43 | 44 | // the rest is up to you 45 | const jeff = core.jeff; 46 | return
; 47 | } 48 | ``` 49 | 50 | Problems: 51 | 52 | - Assinging the data from Pulse to a local const will not give the subscribe() function any way to identify it. You would have to use `core.jeff`. 53 | - Another issue: `[core.jeff]` will create an array with the value, leaving us with the same problem as above. 54 | - useEffect wouldn't work since React is not tracking a prop named "pulse" 55 | -------------------------------------------------------------------------------- /docs/v2/examples/UsageWithVueJS.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using with VueJS 3 | --- 4 | # Using with VueJS 5 | ```js 6 | // VueJS data property 7 | data() { 8 | return { 9 | ...this.mapData({ 10 | settings: 'accounts/settings' 11 | }) 12 | }, 13 | }, 14 | // VueJS computed methods are like Pulse filters, they're cached until one of their dependencies change. Here we're just writing a shortcut to return a boolean if `DARK_THEME` exists based on the Pulse data for `accounts/settings`. 15 | computed: { 16 | darkTheme() { 17 | return this.settings.DARK_THEME || false 18 | } 19 | // as `settings` is defined in mapData(), it will trigger this computed function to re-render when it changes. 20 | }, 21 | watch: { 22 | settings() { 23 | this.$accounts.updateTheme() 24 | } 25 | } 26 | ``` 27 | 28 | You may notice in that watcher I used `this.$accounts`. This is possible as every Pulse collection can be accessed on the Vue instance with the prefix `$`. You can use this to set data, read data in methods, and call actions. 29 | 30 | **Do not use \$ collection references in your template or computed properties, Vue does not see them as reactive, and will not trigger a re-render when Pulse data updates. This is why we have mapData()** 31 | 32 | The \$ references are there to make it easy to interact with Pulse data from the component, like calling actions and setting new values. 33 | 34 | ```JS 35 | // VueJS mounted hook 36 | mounted() { 37 | this.$collection.doSomething() 38 | this.$accounts.someValue = true 39 | }, 40 | methods: { 41 | doSomething() { 42 | this.$collection.someAction() 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/v2/examples/authentication.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authentication 3 | --- 4 | 5 | ## Basic authentication 6 | 7 | Assuming you use a JWT as a Bearer token, this example shows simple login functionality in pulse 8 | 9 | ```js 10 | import Pulse from 'pulse-framework'; 11 | 12 | const core = new Pulse({ 13 | // settings for the request 14 | request: { 15 | baseURL: 'https://api.mysite.com', 16 | 17 | // do something before each request 18 | requestIntercept({ base }, options) { 19 | options.headers.token = `Bearer ${base.token}`; 20 | }, 21 | 22 | // do something after each request 23 | responseIntercept({ base }, response) { 24 | if (response.status === 401) base.isAuthenticated = false; 25 | } 26 | }, 27 | modules: { 28 | auth: { 29 | data: { 30 | token: null, 31 | isAuthenticated: false 32 | }, 33 | persist: ['token'], 34 | routes: { 35 | login: (request, creds) => request.post('login', creds) 36 | }, 37 | actions: { 38 | login({ routes, data }, creds) { 39 | return routes.login(creds).then(res => (data.token = res.token)); 40 | } 41 | } 42 | } 43 | } 44 | }); 45 | 46 | // Call login 47 | core.login({ username: 'jamie', password: 'jeff' }); 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/v2/getting-started/setup-with-vue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Vue 3 | --- 4 | 5 | ### Install 6 | 7 | ``` 8 | npm i pulse-framework 9 | ``` 10 | 11 | First we'll create a Pulse instance and export it from a file named `core.js`, but you can call it whatever you want. 12 | 13 | _core.js_ 14 | 15 | ```js 16 | import Pulse from 'pulse-framework'; 17 | import Vue from 'vue'; 18 | 19 | Pulse.use(Vue); 20 | 21 | export default new Pulse({ 22 | data: { 23 | something: true 24 | } 25 | }); 26 | ``` 27 | 28 | ::: tip 29 | You can use `framework: Vue` on the root or in the `config` instead of `Pulse.use(Vue)`, its up to you. 30 | ::: 31 | 32 | ### Usage in a Vue component using `mapData()` 33 | 34 | After install `mapData()` can now be found on the Vue instance. 35 | 36 | ```js 37 | export default { 38 | name: 'My Vue Component', 39 | data() { 40 | return { 41 | ...this.mapData(core => ({ 42 | something: core.something, 43 | somethingElse: core.somethingElse 44 | })) 45 | }; 46 | } 47 | }; 48 | ``` 49 | 50 | Within your template you can use Pulse data the same way you'd use normal Vue data. 51 | 52 | ```js 53 | this.thing; 54 | ``` 55 | 56 | But remember, this is **immutable**, so in order to mutate data you must access the pulse modules using the `this.$` prefix. 57 | 58 | ### Mutating data: 59 | 60 | From anywhere within your Vue component you can do as follows: 61 | 62 | ```js 63 | this.$myModule.something = true; 64 | ``` 65 | 66 | You can even do this in the template without `this.` 67 | 68 | Here's all the properties availible using the `$` prefix: 69 | 70 | ```js 71 | this.$base; // the root Pulse module 72 | this.$myModule; // a Pulse module 73 | this.$myCollection; // a Pulse collection 74 | this.$services.myService; // a Pulse service 75 | this.$utils.myUtil; // a Pulse util 76 | ``` 77 | 78 | ::: tip Summary 79 | The main thing to learn is that mapData() is reactive, `$` is not- though we need to use the `$` to make mutations and call actions. 80 | ::: 81 | -------------------------------------------------------------------------------- /docs/v2/introduction/what-is-pulse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is Pulse? 3 | --- 4 | 5 | Pulse is a global state and logic framework for reactive Javascript applications. Supporting frameworks like VueJS, React and React Native. Lightweight, modular and powerful, but most importantly friendly to beginners. 6 | 7 | Pulse replaces global state management solutions such as Redux, VueX and MobX, including HTTP libraries such as Axios, Fetch or Request.js. It makes your application more modular, ensuring you follow the best practices while writing significantly less code. Your Pulse code can be used in many different applications, such as a webapp in Vue and a mobile app in React native; if it uses Javascript, it can use Pulse. 8 | 9 | ## Why Pulse? 10 | 11 | After exploring the many options for Javascript state libraries, including the popular VueX and Redux, I felt like I needed a simpler solution. I wanted to get more out of a library than just state management― something that could provide solid structure for the **entire** application. It needed to be structured and simple, but also scalable. This framework provides everything needed to get a reactive javascript front-end application working fast, taking care to follow best practices and to employ simple terminology that makes sense even to beginners. 12 | 13 | I built Pulse reflective of the architecture in which we use at Notify.me, and as a replacement for VueX at Notify also, making sure it is also compatible with React and vanilla environments. The team at Notify love it and I think you will too. 14 | 15 | ## Features 16 | 17 | - :gear: Modular structure ([Modules](/v2/docs/modules.html)) 18 | - :zap: Reactive data ([Reactivity](/v2/docs/concepts.html#reactivity)) `pulse.something = somethingElse` 19 | - :robot: Computed data with automatic dependency tracking ([Computed](/v2/docs/computed.html)) 20 | - :first_quarter_moon: Lifesycle hooks [`watch()`]() / `onReady()` / `nextPulse()` 21 | - :gem: SSOT architecture (single source of truth) 22 | - :nerd_face: DB/ORM-like structure with [Collections](/v2/docs/collections.html#collection-basics) 23 | - :sparkles: Automatic data normalization using [Collect](/v2/docs/collections.html#what-is-data-normalization) `collection.collect()` 24 | - :lock: Model based [data validation](/v2/docs/collections.html#models) with Collections 25 | - :timer_clock: Mutation history tracking with [smart undo]() `collection.undo()` 26 | - :crystal_ball: Dynamic relations between collections using [Populate]() `populate()` 27 | - :wrench: Wrappers for utils and services 28 | - :construction: Task queuing for race condition prevention 29 | - :telephone_receiver: Promise based HTTP requests and websocket connections (web sockets coming soon) 30 | - :hourglass_flowing_sand: Timed interval task handler using [Jobs]() 31 | - :bus: Event bus `pulse.on() / pulse.emit()` 32 | - :floppy_disk: Persisted data API for localStorage and async storage 33 | - :closed_book: Error logging & snapshot bug reporting (WIP) 34 | - :leaves: Lightweight (only 100KB) with 0 dependencies 35 | - :fire: Supports Vue, React and React Native 36 | - :yellow_heart: Well documented (I'm getting there...) 37 | 38 | 41 | -------------------------------------------------------------------------------- /docs/v2/under-the-hood/runtime.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Runtime 3 | --- 4 | -------------------------------------------------------------------------------- /docs/v3/docs/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Actions 3 | --- 4 | 5 | ## Introduction 6 | 7 | # Actions 8 | 9 | Actions are functions, sometimes literally just functions. However there are some added benifits to using Pulse Actions. 10 | 11 | ::: tip Where do I put them? 12 | Actions are best inside an `actions.ts` file in a [Controller]() directory, exported individually. 13 | 14 | Then, you can use `import * as actions from './actions'` to register them to a Controller. 15 | ::: 16 | 17 | This is an example of a regular function used as an action. It has a try catch so that errors can be caught and processed within the core, not the UI code. 18 | 19 | ```js 20 | export async function MyAction() { 21 | try { 22 | // perform action 23 | } catch (e) { 24 | App.Error(e); 25 | } 26 | } 27 | ``` 28 | 29 | > App.Error() is a configurable global error handler. You might want to emit an event to trigger a UI error popup, for example. 30 | 31 | ## Wrapped Actions 32 | 33 | ### `App.Action()` 34 | ::: warning 35 | This feature is not currently functional. It is being worked on currently and should be released in the following few days. Check Discord for updates! 36 | ::: 37 | This is a wrapper function for actions, it contains a built in try/catch + error handler for cleaner syntax. It also provides helper functions as the first parameter, offsetting custom parameters by one. The second parameter in the declaration would be the first parameter when the action is called. 38 | 39 | ```js [WIP, Coming Soon] 40 | export const MyAction = App.Action(() => { 41 | // do something 42 | }); 43 | ``` 44 | 45 | ```js 46 | export const MyAction = App.Action(({ onError, undo, debounce }) => { 47 | onError(undo); // configure action to revert all state changes on error 48 | debounce(300); // configure action to debounce at a rate of 300ms 49 | 50 | // do something 51 | }); 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/v3/docs/computed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Computed State 3 | --- 4 | 5 | ## Introduction 6 | 7 | # Computed State 8 | 9 | Computed State is an extension of [State](./state.md). It computes a value from a function that you provide, and caches it to avoid unnecessary recomputation. 10 | 11 | - It will magically recompute when its dependencies change. 12 | - Computed can track dependencies automatically or manually. 13 | 14 | ::: tip Note: Some State features are disabled 15 | Unlike State you can not directly mutate it, so `.set()`, `.bind` are disabled. 16 | 17 | `.persist()` is also blocked as persisting a computed value isn't necessary. 18 | 19 | Other State methods are still useable! Refer to [State Methods](./state.md#methods). 20 | ::: 21 | 22 | ## Example 23 | 24 | ```ts 25 | const App = new Pulse(); 26 | 27 | const MY_COMPUTED = App.Computed(() => 1 + 2); 28 | 29 | MY_COMPUTED.value; // 3 30 | ``` 31 | 32 | The function provided here is a simple math equation. 33 | 34 | In this case there is no reason for this Computed State to ever recompute, we didn't define any dependencies and none were used during the compute function. 35 | 36 | Here is an example with a dependency: 37 | 38 | ```ts 39 | const MY_STATE = App.State(5); 40 | 41 | const MY_COMPUTED = App.Computed(() => MY_STATE.value + 2); 42 | 43 | MY_COMPUTED.value; // 7 44 | ``` 45 | 46 | Now when `MY_STATE` changes, `MY_COMPUTED` will recompute 47 | 48 | ```ts 49 | MY_STATE.set(2); 50 | 51 | MY_COMPUTED.value; // 4 52 | ``` 53 | 54 | ::: tip How does it work? 55 | The State class has a reactive getter `State.value`. When the computed function begins Pulse will listen for any State instances that have their value accessed, and will register them as a dependency of the Computed State. 56 | 57 | This works for Groups, Selectors, Collection Data and anything that extends the State class. 58 | ::: 59 | 60 | ## Methods 61 | 62 | ### `.recompute()` 63 | 64 | _Forces the Computed instance to recompute_ 65 | 66 | ```typescript 67 | MY_COMPUTED.recompute(); 68 | ``` 69 | 70 | ## Initial compute 71 | Computed functions need to compute their value initially, however doing this upon declaration can cause issues depending on data accessed within the compute function. 72 | 73 | Normally Pulse applications use `App.Core()` to export the [Core](), but this function also acts as a way to finalize Pulse. This is a perfect opportunity to compute the initial values for all Computed instances. 74 | 75 | ```ts 76 | const App = new Pulse(); 77 | 78 | const MY_COMPUTED = App.Computed(() => 1 + 1); 79 | 80 | MY_COMPUTED.value // undefined 81 | 82 | const core = App.Core(myCore); 83 | 84 | MY_COMPUTED.value // 2 85 | ``` 86 | 87 | 88 | If they were to compute immediately after declaration, other State / Computed values in your core might not be defined yet, throwing many angry errors. 89 | 90 | If this is not the behavior you want, or you are not using a core you can bypass this logic using the config option `noCore: true` 91 | ```ts 92 | const App = new Pulse({ noCore: true }); 93 | 94 | const MY_COMPUTED = App.Computed(() => 1 + 1); 95 | 96 | MY_COMPUTED.value // 2 97 | ``` -------------------------------------------------------------------------------- /docs/v3/docs/persisting-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persisting Data 3 | --- 4 | 5 | # Persisting Data 6 | 7 | It's common for applications to store data on the client browser, Pulse makes it easy to achieve this. On refresh the State value will magically load if present in storage. 8 | 9 | ```js 10 | const MY_STATE = App.State('hello').persist('storage-key-here'); 11 | ``` 12 | 13 | ## Configuration 14 | 15 | Configuration is optional. In browser environments Pulse automatically integrates with the local storage API and so calling `State.persist()` just works. 16 | 17 | ```js 18 | const App = new Pulse(); 19 | 20 | App.Storage({ 21 | prefix: 'my_app' // custom storage key prefix (optional) 22 | async: false, 23 | set: ... 24 | get: ... 25 | remove: ... 26 | }); 27 | ``` 28 | 29 | ::: tip Note 30 | Currently it is not possible to persist data Collection data. If you need this functionality consider helping design it, join our [Discord](https://discord.gg/KvuJva) 31 | ::: 32 | 33 | ::: warning React Native & non browser users: 34 | Some environments, such as React Native, do not have local storage. You must bind a custom storage solution as shown above, in React Native you can use `AsyncStorage`. 35 | ::: 36 | 37 | More features will be added to data persisting soon, such as persisting entire collection data, custom storage per collection and more configuration options. 38 | -------------------------------------------------------------------------------- /docs/v3/docs/pulse-instance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pulse Instance 3 | --- 4 | 5 | ## Introduction 6 | 7 | # Pulse Instance 8 | 9 | The Pulse instance is created with `new Pulse()`, it is unique to your application. With it, you can create [State](), [Computed State](), [Collections]() and more. 10 | 11 | The instance not only contains configuration for Pulse, but also a job queue system for managing State mutations. 12 | ```ts 13 | const App = new Pulse(); 14 | ``` 15 | 16 | The Pulse Instance provides helpful function to your application, and the way you write your [Core](). 17 | 18 | - Queueing [State]() changes and preventing race conditions. 19 | - Providing global awareness to [Computed State]() for automatic dependency tracking. 20 | - Integrating with persistent storage. 21 | - Initializing the [Core]() structure. 22 | - Issuing squashed updates to subscribed components via the [Pulse Runtime](). 23 | 24 | ## Configuration Options 25 | 26 | Pulse takes an optional configuration object as the only parameter 27 | 28 | ```ts 29 | const App = new Pulse({ 30 | storage: { 31 | prefix: 'CoolApp' 32 | } 33 | }); 34 | ``` 35 | 36 | > Here's a TypeScript interface for quick refrence, however each property will be explained in more detail below. 37 | 38 | ```ts 39 | interface PulseConfig { 40 | computedDefault?: any; 41 | waitForMount?: boolean; 42 | storage?: StorageConfig; 43 | logJobs?: boolean; 44 | noCore?: boolean; 45 | globalHistory?: boolean; 46 | } 47 | ``` 48 | 49 | ## Options Refrence 50 | 51 | ### `storage` 52 | 53 | This option is for state persistence with local storage or a custom storage API such as React Native's AsyncStorage 54 | 55 | ```ts 56 | interface StorageConfig { 57 | get: () => any; 58 | set: (key: string) => any; 59 | remove: (key: string) => any; 60 | async?: boolean; // Is the storage asynchronous 61 | prefix?: string; // a custom prefix for local storage keys 62 | } 63 | ``` 64 | 65 | ### `computedDefault` 66 | 67 | The value provided here will be the fallback used when the result of a computed function is `null` or `undefined`. 68 | 69 | ### `waitForMount` 70 | 71 | ### `computedDefault` 72 | 73 | ### `errors` [wip] 74 | 75 | ```ts 76 | interface ErrorConfig { 77 | key: string; 78 | message: string; 79 | code?: number; 80 | } 81 | ``` 82 | 83 | ## Error Handling [WIP] 84 | 85 | Pulse offers a global error handler best suited for use with [Actions]() on a try/catch and automatically used by [App.Action()](). 86 | 87 | ```ts 88 | App.onError((error: ErrorObject) => { 89 | if (error.code === 401) core.accounts.logout(); 90 | }); 91 | ``` 92 | 93 | Pulse will try to parse a caught error, and will always provide this object so you can safely process the error. 94 | 95 | ```ts 96 | interface ErrorObject { 97 | code: number; // if the error was because of a request, this will be the request error code 98 | message: string; 99 | action: Function; // reference to action in which the error occurred 100 | raw: any; // The raw error 101 | } 102 | ``` 103 | 104 | You can configure your [API]() instance to prepare response data for the error handler also. More about this on [API](); 105 | -------------------------------------------------------------------------------- /docs/v3/getting-started/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pulse Concepts 3 | --- 4 | 5 | ## Structure 6 | 7 | Pulse was designed to take all business logic out of components, meaning your React/Vue/Angular components are essencially puppets for Pulse to orchestrate. The benifit of keeping logic seperate to visual components is versitility, upgradablity and cleanliness. An example would be with Notify's codebase, the majority of the business logic is in a repository called `notify-core` and our components are purely React code with hooks into the core. This allows us the freedom to build different components for different platforms that all behave the same way. 8 | 9 | ## Reactivity 10 | 11 | Reactive data is state that will react to mutations in order to cause component re-renders. Components can subscribe to Pulse state and will be updated when the state changes. 12 | 13 | ## Thesaurus 14 | 15 | :::tip How we refer to our own classes 16 | In these docs we will refer to our classes with a capital first letter. When you see "state" we're referring to the programming concept `state`, but when you see `State` we're referring to our [State]() class. 17 | ::: 18 | 19 | - **Pulse Instance**: The result of initializing Pulse, a refrence to your Pulse application. 20 | - **The Core**: You library of State, Collections and Controllers in a single object. 21 | - **Reactivity**: The concept of state mutations causing component re-renders automatically. 22 | - **Declaration**: Refers to logic as it is declared in the code, the opposite of this would be runtime. 23 | - **Runtime**: When Pulse is fully initialized and the core is ready, runtime begins. Runtime handles queuing state changes. 24 | - **Primary Key**: Refers to the identifier used to index data items within a Collection, a `string` or `number`. Also known as "pk" or "id". 25 | -------------------------------------------------------------------------------- /docs/v3/getting-started/setup-with-next.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Next 3 | --- 4 | 5 | # NextJS Setup 6 | 7 | NextJS is a web framework for React. [Learn more.](https://nextjs.org/) 8 | 9 | ## Installation 10 | 11 | ``` 12 | yarn add @pulsejs/core @pulsejs/react @pulsejs/next 13 | ``` 14 | 15 | ## Initialization 16 | 17 | ```ts 18 | import Pulse from '@pulsejs/next'; 19 | 20 | export const App = new Pulse(); 21 | ``` 22 | 23 | NextJS is built on top of React, which means Pulse will work just the same as usual with just the [React]() setup. However if your application needs to make state changes server-side then this add-on for Pulse will help you out! 24 | 25 | ## What is SSR? 26 | 27 | The basic concept of sever side rendering entails your server rendering the page first, so the client doesn't have to. NextJS achieves this with the function `getSeverSideProps`. This means the JS in your app will run once on the server, then once again on the client. Because of this there would be an identical instance of Pulse created on either end. 28 | 29 | If any changes happen at runtime on the server, these changes must be sent with the HTML content back to the client. This integration for Pulse provides a function to make that extremely simple. 30 | 31 | # `preserveServerState()` 32 | 33 | This function will analyse your Pulse instance and extract all State and Collections, unpack the important data and sterilize it for injection into the rendered HTML. 34 | 35 | ```ts 36 | import { preserveServerState } from '@pulsejs/next'; 37 | 38 | // NextJS getServerSideProps function 39 | export async function getServerSideProps(context) { 40 | const data = { props: {} }; 41 | 42 | // server side changes happen here... 43 | 44 | return preserveServerState(data); 45 | } 46 | ``` 47 | 48 | There are some rules to remember for preserving State changes on the server... 49 | 50 | - State **must** have a name, you can set this with `.key()` (Controllers do this for you!) 51 | - State must have been changed from the initial value (`State.isSet` must be `true`) 52 | 53 | This function will return the same `data` object passed in, but with `PULSE_DATA` injected into `data.props` containing the sterilized Pulse data. 54 | -------------------------------------------------------------------------------- /docs/v3/getting-started/setup-with-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With React 3 | --- 4 | 5 | # React Setup 6 | 7 | ## Installation 8 | 9 | ``` 10 | yarn add @pulsejs/core @pulsejs/react 11 | ``` 12 | 13 | Think of `@pulsejs/react` as an extension of Pulse in the context of React. It provides access to all core functions + React only helpers such as the `usePulse` hook. We can't forget to install the `@pulsejs/core` as it is used by the React integration. 14 | 15 | ## Initialization 16 | 17 | ```ts 18 | import Pulse from '@pulsejs/react'; 19 | 20 | export const App = new Pulse(); 21 | ``` 22 | 23 | Unlike older versions you do not need to pass React into Pulse, as the React package lists React as a peer dependency. This allows for a much cleaner syntax for setup! 24 | 25 | Follow this [guide](../docs/core.html#definition) to learn how to set up your core. 26 | 27 | ## Functional Components: `usePulse()` 28 | 29 | `usePulse` is a React hook that _subscribes_ a React functional component to State instances. 30 | 31 | ```ts 32 | const myStateValue = usePulse(core.MY_STATE); 33 | ``` 34 | 35 | > Both the input and the return value are an array, allowing you to subscribe to more than one State. 36 | 37 | It also supports extensions of State, such as Computed, Groups, Selectors and even Collection Data, meaning you can also use functions that return State, such as `Collection.findById()` 38 | 39 | ::: tip NOTE: usePulse returns the value, not the instance. 40 | The return value is `State.value`, not the State instance. For Groups it's slightly different, you'll get the `Group.output`, which is the useful data for your component. 41 | ::: 42 | 43 | ### Example Component 44 | 45 | ```tsx 46 | import { usePulse } from 'pulse-framework'; 47 | import React from 'react'; 48 | import core from './core'; 49 | 50 | export default function Component(): React.FC { 51 | const [account] = usePulse([core.accounts.collection.selectors.CURRENT]); 52 | 53 | return <>{account.username}; 54 | } 55 | ``` 56 | 57 | ### State Arrays 58 | 59 | usePulse also supports **arrays** of State instances, returning values as an array that can be destructured. 60 | 61 | ```ts 62 | const [myState, anotherState] = usePulse([core.MY_STATE, core.ANOTHER_STATE]); 63 | ``` 64 | 65 | The names of the values can be anything, though we recommend they be the camel case counter-part to the State instance name. This is completely typesafe as of version `3.1` 66 | 67 | ## Class Components: `PulseHOC()` 68 | 69 | ```js 70 | import { PulseHoc } from 'pulse-framework'; 71 | 72 | class Component extends React.Component { 73 | render() { 74 | return

Hello, {this.props.name}

; 75 | } 76 | } 77 | 78 | export default PulseHoc(Component, [core.MY_STATE]); 79 | ``` 80 | 81 | ::: warning 82 | PulseHOC is a low priority WIP and has not been tested at the time of writing these docs, if you need this and it doesn't work, please let me know via Discord 83 | ::: 84 | 85 | ## Additional Hooks 86 | 87 | Pulse's React integration also provides some helpful hooks for functional React components, the documentation for these hooks can be found in the sections for the parent functionality. 88 | 89 | - [useWatcher()](/v3/docs/state.html#methods) - A hook to use State watchers with auto cleanup/ 90 | - [useEvent()](/v3/docs/events.html#useevent) - A hook to use a Pulse Event with a cleaner syntax and auto cleanup. -------------------------------------------------------------------------------- /docs/v3/getting-started/setup-with-vue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Vue 3 | --- 4 | 5 | # Vue Setup 6 | 7 | ::: danger NOT USABLE YET 8 | Vue was a large part of previous versions, so we're eager to add Vue support for the many people in the Pulse community that use Vue. We're still testing the best way to implement this below is the current plan, though this syntax is subject to change. 9 | ::: 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add @pulsejs/core @pulsejs/vue 15 | ``` 16 | 17 | ## Initialization 18 | 19 | ```ts 20 | import Pulse from '@pulsejs/vue'; 21 | 22 | export const App = new Pulse(); 23 | ``` 24 | 25 | ## Example 26 | 27 | ```ts 28 | import Vue from 'vue'; 29 | import Pulse from '@pulsejs/vue'; 30 | 31 | export const App = new Pulse(); 32 | 33 | const core = App.Core({ 34 | MY_STATE: App.State(true) 35 | }); 36 | 37 | export default new Vue({ 38 | el: '#vue', 39 | data: { 40 | ...this.mapCore(core => ({ 41 | localName: core.MY_STATE 42 | })) 43 | } 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/v3/getting-started/style-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Style Guide 3 | --- 4 | 5 | ### Style Guide 6 | 7 | You're free to design your application in whichever way suits your needs, but this is the Pulse way: 8 | 9 | 41 | -------------------------------------------------------------------------------- /docs/v3/resources/examples.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/docs/v3/resources/examples.md -------------------------------------------------------------------------------- /docs/v3/resources/snippets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Snippets 3 | --- 4 | 5 | ### Snippets for VSCode 6 | 7 | **Install:** bottom-right cog > user snippets > your_language.json 8 | 9 | ### API Route `TypeScript` 10 | 11 | ```json 12 | { 13 | "Pulse Route": { 14 | "prefix": "proute", 15 | "body": [ 16 | "export const ${1:name} = async (payload: any): Promise =>", 17 | " (await API.post('${1:name}', payload)).data;" 18 | ], 19 | "description": "Pulse Route" 20 | } 21 | } 22 | ``` 23 | 24 | This is a typesafe snippet for making an API call using the Pulse [API]() class. 25 | 26 | Assuming you import your API instance as `API`. 27 | 28 | ```js 29 | export const name = async (payload: Payload): Promise => 30 | (await API.post('name', payload)).data; 31 | ``` 32 | 33 | `Payload` and `ResponseData` should be interfaces defining what data you expect to send/recieve. 34 | -------------------------------------------------------------------------------- /docs/v4/docs/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Actions 3 | --- 4 | 5 | ## Introduction 6 | 7 | # Actions 8 | 9 | Actions are functions, sometimes literally just functions. However there are some added benifits to using Pulse Actions. Authomatic try/catch wraping, state tracking, undoing, and more! 10 | 11 | ### Basic Usage 12 | ```ts 13 | import { action } from '@pulsejs/core'; 14 | 15 | const doSomething = action(({}, num) => { 16 | 17 | const newnum = num + 1 18 | 19 | return `I Did Something ${newnum}` 20 | 21 | }) 22 | ``` 23 | 24 | ## Async Actions 25 | 26 | You can also use async functions as an action. This becomes escpecially powerful with the `onCatch()` modifier. 27 | 28 | ```ts 29 | const doSomethingAsync = action(async ({onCatch}) => { 30 | 31 | return await somethingAsync() 32 | 33 | }) 34 | ``` 35 | 36 | ## Modifiers 37 | 38 | Modifiers are helper functioned that are passed in an object as the first argument of your action function. We recommend deconstructing this object and only pulling the functions you need. 39 | 40 | # `onCatch()` 41 | 42 | Catches any errors and passes it to the first argument in the function it's given. 43 | 44 | ```ts 45 | const doSomethingBad = action(({onCatch}) => { 46 | 47 | onCatch((e) => console.error) // throws "a bad word was said" 48 | 49 | throw new Error('a bad word was said.') 50 | }) 51 | 52 | doSomethingBad() // returns: void 53 | ``` 54 | 55 | # `track()` 56 | 57 | Track any state changes within the action, then do something! 58 | 59 | ```ts 60 | const MY_STATE = state('a state') 61 | 62 | const changeState = action(({track}) => { 63 | track(() => 'lol') 64 | MY_STATE.set() 65 | }) 66 | ``` -------------------------------------------------------------------------------- /docs/v4/docs/persisting-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Persisting Data 3 | --- 4 | 5 | # Persisting Data 6 | 7 | It's common for applications to store data on the client browser, Pulse makes it easy to achieve this. On refresh the State value will magically load if present in storage. 8 | 9 | ```js 10 | const MY_STATE = App.State('hello').persist('storage-key-here'); 11 | ``` 12 | 13 | ## Configuration 14 | 15 | Configuration is optional. In browser environments Pulse automatically integrates with the local storage API and so calling `State.persist()` just works. 16 | 17 | ```js 18 | const App = new Pulse(); 19 | 20 | App.Storage({ 21 | prefix: 'my_app' // custom storage key prefix (optional) 22 | async: false, 23 | set: ... 24 | get: ... 25 | remove: ... 26 | }); 27 | ``` 28 | 29 | ::: tip Note 30 | Currently it is not possible to persist data Collection data. If you need this functionality consider helping design it, join our [Discord](https://discord.gg/KvuJva) 31 | ::: 32 | 33 | ::: warning React Native & non browser users: 34 | Some environments, such as React Native, do not have local storage. You must bind a custom storage solution as shown above, in React Native you can use `AsyncStorage`. 35 | ::: 36 | 37 | More features will be added to data persisting soon, such as persisting entire collection data, custom storage per collection and more configuration options. 38 | -------------------------------------------------------------------------------- /docs/v4/getting-started/concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pulse Concepts 3 | --- 4 | 5 | ## Structure 6 | 7 | Pulse was designed to take all business logic out of components, meaning your React/Vue/Angular components are essencially puppets for Pulse to orchestrate. The benifit of keeping logic seperate to visual components is versitility, upgradablity and cleanliness. An example would be with Notify's codebase, the majority of the business logic is in a repository called `notify-core` and our components are purely React code with hooks into the core. This allows us the freedom to build different components for different platforms that all behave the same way. 8 | 9 | ## Reactivity 10 | 11 | Reactive data is state that will react to mutations in order to cause component re-renders. Components can subscribe to Pulse state and will be updated when the state changes. 12 | 13 | ## Thesaurus 14 | 15 | :::tip How we refer to our own classes 16 | In these docs we will refer to our classes with a capital first letter. When you see "state" we're referring to the programming concept `state`, but when you see `State` we're referring to our [State]() class. 17 | ::: 18 | 19 | - **Pulse Instance**: The result of initializing Pulse, a refrence to your Pulse application. 20 | - **The Core**: You library of State, Collections and Controllers in a single object. 21 | - **Reactivity**: The concept of state mutations causing component re-renders automatically. 22 | - **Declaration**: Refers to logic as it is declared in the code, the opposite of this would be runtime. 23 | - **Runtime**: When Pulse is fully initialized and the core is ready, runtime begins. Runtime handles queuing state changes. 24 | - **Primary Key**: Refers to the identifier used to index data items within a Collection, a `string` or `number`. Also known as "pk" or "id". 25 | -------------------------------------------------------------------------------- /docs/v4/getting-started/setup-with-next.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Next 3 | --- 4 | 5 | # NextJS Setup 6 | 7 | NextJS is a web framework for React. [Learn more.](https://nextjs.org/) 8 | 9 | ## Installation 10 | 11 | ``` 12 | yarn add @pulsejs/core @pulsejs/react @pulsejs/next 13 | ``` 14 | 15 | ## Initialization 16 | 17 | ```ts 18 | import Pulse from '@pulsejs/next'; 19 | 20 | export const App = new Pulse(); 21 | ``` 22 | 23 | NextJS is built on top of React, which means Pulse will work just the same as usual with just the [React]() setup. However if your application needs to make state changes server-side then this add-on for Pulse will help you out! 24 | 25 | ## What is SSR? 26 | 27 | The basic concept of sever side rendering entails your server rendering the page first, so the client doesn't have to. NextJS achieves this with the function `getSeverSideProps`. This means the JS in your app will run once on the server, then once again on the client. Because of this there would be an identical instance of Pulse created on either end. 28 | 29 | If any changes happen at runtime on the server, these changes must be sent with the HTML content back to the client. This integration for Pulse provides a function to make that extremely simple. 30 | 31 | # `preserveServerState()` 32 | 33 | This function will analyse your Pulse instance and extract all State and Collections, unpack the important data and sterilize it for injection into the rendered HTML. 34 | 35 | ```ts 36 | import { preserveServerState } from '@pulsejs/next'; 37 | 38 | // NextJS getServerSideProps function 39 | export async function getServerSideProps(context) { 40 | const data = { props: {} }; 41 | 42 | // server side changes happen here... 43 | 44 | return preserveServerState(data); 45 | } 46 | ``` 47 | 48 | There are some rules to remember for preserving State changes on the server... 49 | 50 | - State **must** have a name, you can set this with `.key()` (Controllers do this for you!) 51 | - State must have been changed from the initial value (`State.isSet` must be `true`) 52 | 53 | This function will return the same `data` object passed in, but with `PULSE_DATA` injected into `data.props` containing the sterilized Pulse data. 54 | -------------------------------------------------------------------------------- /docs/v4/getting-started/setup-with-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With React 3 | --- 4 | 5 | # React Setup 6 | 7 | ## Installation 8 | 9 | ``` 10 | yarn add @pulsejs/core @pulsejs/react 11 | ``` 12 | 13 | Think of `@pulsejs/react` as an extension of Pulse in the context of React. It provides access to all core functions + React only helpers such as the `usePulse` hook. We can't forget to install the `@pulsejs/core` as it is used by the React integration. 14 | 15 | ## Initialization 16 | 17 | As of version 4.0, Pulse no longer requires you to initialize an instance. 18 | 19 | If you wish to setup your core the "official" way, see [this guide](../docs/core.html#definition) 20 | 21 | ## Functional Components: `usePulse()` 22 | 23 | `usePulse` is a React hook that _subscribes_ a React functional component to State instances. 24 | 25 | ```ts 26 | const myStateValue = usePulse(core.MY_STATE); 27 | ``` 28 | 29 | > Both the input and the return value are an array, allowing you to subscribe to more than one State. 30 | 31 | It also supports extensions of State, such as Computed, Groups, Selectors and even Collection Data, meaning you can also use functions that return State, such as `Collection.getData()` 32 | 33 | ::: tip NOTE: usePulse returns the value, not the instance. 34 | The return value is `State.value`, not the State instance. For Groups it's slightly different, you'll get the `Group.output`, which is the useful data for your component. 35 | ::: 36 | 37 | ### Example Component 38 | 39 | ```tsx 40 | import { usePulse } from '@pulsejs/react'; 41 | import React from 'react'; 42 | import core from './core'; 43 | 44 | export default function Component(): React.FC { 45 | const [account] = usePulse([core.accounts.collection.selectors.CURRENT]); 46 | 47 | return <>{account.username}; 48 | } 49 | ``` 50 | 51 | ### State Arrays 52 | 53 | usePulse also supports **arrays** of State instances, returning values as an array that can be destructured. 54 | 55 | ```ts 56 | const [myState, anotherState] = usePulse([core.MY_STATE, core.ANOTHER_STATE]); 57 | ``` 58 | 59 | The names of the values can be anything, though we recommend they be the camel case counter-part to the State instance name. This is completely typesafe as of version `3.1` 60 | 61 | ## Class Components: `PulseHOC()` 62 | 63 | ```js 64 | import { PulseHoc } from '@pulsejs/react'; 65 | 66 | class Component extends React.Component { 67 | render() { 68 | return

Hello, {this.props.name}

; 69 | } 70 | } 71 | 72 | export default PulseHoc(Component, [core.MY_STATE]); 73 | ``` 74 | 75 | ::: warning 76 | PulseHOC is a low priority WIP and has not been tested at the time of writing these docs, if you need this and it doesn't work, please let us know via Discord 77 | ::: 78 | 79 | ## Additional Hooks 80 | 81 | Pulse's React integration also provides some helpful hooks for functional React components, the documentation for these hooks can be found in the sections for the parent functionality. 82 | 83 | - [useWatcher()](/v4/docs/state.html#methods) - A hook to use State watchers with auto cleanup/ 84 | - [useEvent()](/v4/docs/events.html#useevent) - A hook to use a Pulse Event with a cleaner syntax and auto cleanup. -------------------------------------------------------------------------------- /docs/v4/getting-started/setup-with-vue.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Setup With Vue 3 | --- 4 | 5 | # Vue Setup 6 | 7 | ::: danger NOT USABLE YET 8 | Vue was a large part of previous versions, so we're eager to add Vue support for the many people in the Pulse community that use Vue. We're still testing the best way to implement this below is the current plan, though this syntax is subject to change. 9 | ::: 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add @pulsejs/core @pulsejs/vue 15 | ``` 16 | 17 | ## Initialization 18 | 19 | ```ts 20 | import Pulse from '@pulsejs/vue'; 21 | 22 | export const App = new Pulse(); 23 | ``` 24 | 25 | ## Example 26 | 27 | ```ts 28 | import Vue from 'vue'; 29 | import Pulse from '@pulsejs/vue'; 30 | 31 | export const App = new Pulse(); 32 | 33 | const core = App.Core({ 34 | MY_STATE: App.State(true) 35 | }); 36 | 37 | export default new Vue({ 38 | el: '#vue', 39 | data: { 40 | ...this.mapCore(core => ({ 41 | localName: core.MY_STATE 42 | })) 43 | } 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/v4/getting-started/style-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Style Guide 3 | --- 4 | 5 | ### Style Guide 6 | 7 | You're free to design your application in whichever way suits your needs, but this is the Pulse way: 8 | 9 | Index files `index.ts` should only be used for imports and exports of your modules 10 | 11 | - File names should be formatted as [module]().[type]().[extention](). `(eg: auth.routes.ts)` 12 | - Your Pulse code is the core of your application, so would sit in a directory or repository named "core". 13 | - Your core file structure would consist of the following directories: 14 | ::: vue 15 | ├── **core** 16 | │ ├── .**index.ts** _Export core object_ 17 | │ │ ├── `modules` 18 | │ │ │ ├── **accounts** 19 | │ │ │ │ ├── **index.ts** _Import actions, states, routes and export as a default object_ 20 | │ │ │ │ ├── **account.actions.ts** _Methods_ 21 | │ │ │ │ ├── **account.state.ts** _State, Computed State & Collections_ 22 | │ │ │ │ ├── **account.types.ts** _for TypeScript users (Optional)_ 23 | │ │ │ │ ├── **account.routes.ts** _for all your routes_ 24 | │ │ │ └── **index.ts** _Export all the modules with the correct name (ex: `export { default as accounts } from './accounts`)_ 25 | │ │ ├── `utils` _(Optional)_ 26 | │ │ │ └── **index.ts** 27 | │ │ ├── `data` _(Optional)_ 28 | │ │ │ ├── **lists.json** 29 | └── **package.json** 30 | ::: 31 | 32 | * Controller actions should never directly return route response 33 | * Controller actions should recieve prameters instead of just one object to assemple request payloads 34 | * Controller functions should use async / await when calling routes 35 | -------------------------------------------------------------------------------- /docs/v4/resources/examples.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/docs/v4/resources/examples.md -------------------------------------------------------------------------------- /docs/v4/resources/ideas.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ideas 3 | --- 4 | 5 | # Ideas 6 | 7 | This is a dump for things we could implement in the future 8 | 9 | ## Data Model 10 | 11 | ```js 12 | App.Model((model, data) => { 13 | return { 14 | id: model.index(), 15 | thumbnail_hash: model.string().max(100).min(100).hidden().optional() 16 | thumbnail: model.compute(() => AppState.URL.value + data.thumbnail_hash).if(data.thumbnail_hash) 17 | connections: model.relate(Connections) 18 | settings: model.level('owner'), 19 | } 20 | }) 21 | ``` 22 | 23 | 24 | 25 | # Collection Relations 26 | ```ts 27 | Users.relate(Posts, 'posts') 28 | ``` 29 | - posts property will be deleted and auto collected into posts 30 | - group will be created automatically on Posts as the user id 31 | - users output will contain posts -------------------------------------------------------------------------------- /docs/v4/resources/snippets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Snippets 3 | --- 4 | 5 | ### Snippets for VSCode 6 | 7 | **Install:** bottom-right cog > user snippets > your_language.json 8 | 9 | ### API Route `TypeScript` 10 | 11 | ```json 12 | { 13 | "Pulse Route": { 14 | "prefix": "proute", 15 | "body": [ 16 | "export const ${1:name} = async (payload: any): Promise =>", 17 | " (await API.post('${1:name}', payload)).data;" 18 | ], 19 | "description": "Pulse Route" 20 | } 21 | } 22 | ``` 23 | 24 | This is a typesafe snippet for making an API call using the Pulse [API]() class. 25 | 26 | Assuming you import your API instance as `API`. 27 | 28 | ```js 29 | export const name = async (payload: Payload): Promise => 30 | (await API.post('name', payload)).data; 31 | ``` 32 | 33 | `Payload` and `ResponseData` should be interfaces defining what data you expect to send/recieve. 34 | -------------------------------------------------------------------------------- /examples/typescript/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulse-example-core", 3 | "version": "1.0.0", 4 | "author": "Jamie Pine", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "build": "tsc", 10 | "dev": "tsc --watch" 11 | }, 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/pulse-framework/pulse" 18 | }, 19 | "dependencies": { 20 | "@pulsejs/core": "latest" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/typescript/core/src/accounts/controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | action, 3 | Controller, 4 | // log, callback, 5 | state, 6 | // form, 7 | model, 8 | event, 9 | collection 10 | } from '@pulsejs/core'; 11 | 12 | class Accounts extends Controller { 13 | public state = { 14 | authToken: state(null).persist() 15 | // createAccount: form(data => ({ 16 | // username: model.string().min(3).max(50).required(), 17 | // password: model.string().min(8).max(50).required(), 18 | // passwordVerify: model.equalTo(data.password).required().hidden() 19 | // })) 20 | }; 21 | 22 | public collection = collection().createGroups(['jeff', 'lol']); 23 | } 24 | 25 | export const accounts = new Accounts(); 26 | 27 | // accounts.state.createAccount.getProperty('username').hasError; 28 | // accounts.state.createAccount.getProperty('username').errorMessage; 29 | 30 | function Test(items: T[]) { 31 | const obj: Record = {} as Record; 32 | // items.forEach(item => obj[item] = true); 33 | return obj; 34 | } 35 | 36 | const test = Test(['jeff', 'haha']); 37 | -------------------------------------------------------------------------------- /examples/typescript/core/src/accounts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /examples/typescript/core/src/accounts/types.ts: -------------------------------------------------------------------------------- 1 | export interface Accounts {} 2 | -------------------------------------------------------------------------------- /examples/typescript/core/src/app/api.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/core/src/app/api.ts -------------------------------------------------------------------------------- /examples/typescript/core/src/app/controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, state } from '@pulsejs/core'; 2 | 3 | class App extends Controller { 4 | public state = { 5 | baseURL: state('https://api.notify.me'), 6 | assetURL: state('https://media.notify.me') 7 | }; 8 | } 9 | 10 | export const app = new App(); 11 | -------------------------------------------------------------------------------- /examples/typescript/core/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /examples/typescript/core/src/app/persistance.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/core/src/app/persistance.ts -------------------------------------------------------------------------------- /examples/typescript/core/src/app/types.ts: -------------------------------------------------------------------------------- 1 | export interface App {} 2 | -------------------------------------------------------------------------------- /examples/typescript/core/src/channels/controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, collection, action, state, model, route } from '@pulsejs/core'; 2 | // the core module is a file that exports all controllers 3 | import { app, ui } from '../core'; 4 | import { Channel, Subscription, NotificationOptions } from './types'; 5 | 6 | /** 7 | * @Controller Channels 8 | */ 9 | class Channels extends Controller { 10 | public state = { 11 | searchValue: state(null) 12 | }; 13 | 14 | public collection = collection() 15 | .createGroups(['subscribed', 'muted', 'previous']) 16 | .createSelector('current') 17 | .model( 18 | data => ({ 19 | id: model.index().string(), 20 | username: model.index().string().max(30).min(3), 21 | avatar_hash: model.string().max(100).min(100).hidden().optional(), 22 | avatar: model.if(data.avatar_hash).compute(() => app.state.assetURL.value + data.thumbnail_hash) 23 | }), 24 | { 25 | allowUnknown: true 26 | } 27 | ) 28 | .persist({ db: 'sqlite', table: 'channels' }); 29 | 30 | private routes = { 31 | get: route({ method: 'GET', endpoint: '/channel' }), 32 | subscribe: route({ method: 'GET', endpoint: '/channel/subscribe/:channel_id' }), 33 | unsubscribe: route({ method: 'DELETE', endpoint: '/channel/subscribe/:channel_id' }) 34 | }; 35 | 36 | /** 37 | * @action Subscribe to channel 38 | * @param {string} channelId - The channel to subscribe to 39 | * @param {NotificationOption} notificationOption - The notification settings for this subscription 40 | */ 41 | public subscribe = action(async ({ onCatch, undo, batch }, channelId: string, notificationOption: NotificationOptions) => { 42 | onCatch(undo, ui.alert, false); 43 | // batch state changes to group side-effects & revert changes with undo onCatch 44 | batch(() => { 45 | this.collection.put(channelId, ['subscribed']); 46 | this.collection.update(channelId, { subscription: { notification_options: notificationOption } }); 47 | }); 48 | // create the subscription on the api, passing params & query 49 | const subscription = await this.routes.subscribe({ 50 | params: { 51 | channelId: this.collection.selectors.current.id 52 | }, 53 | query: { limit: 30 } 54 | }); 55 | if (!subscription.active) throw 'subscription_not_active'; 56 | 57 | this.collection.update(channelId, { subscription }); 58 | return true; 59 | }); 60 | 61 | /** 62 | * @action Get a channel by username 63 | * @param {string} username - The channel username to get 64 | */ 65 | public getChannel = action( 66 | async ({ onCatch }, username: string): Promise => { 67 | onCatch(ui.alert, false); 68 | 69 | const channel = await this.routes.get({ query: { username, includeConnections: true, includeModules: true } }); 70 | 71 | this.collection.collect(channel, [], { patch: true }); 72 | return this.collection.getDataValueByIndex('username', username); 73 | } 74 | ); 75 | } 76 | 77 | export const channels = new Channels(); 78 | 79 | channels.collection.selectors.current; 80 | channels.collection.groups.subscribed; 81 | -------------------------------------------------------------------------------- /examples/typescript/core/src/channels/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /examples/typescript/core/src/channels/types.ts: -------------------------------------------------------------------------------- 1 | export interface Channel { 2 | id: string; 3 | username: string; 4 | avatar: string; 5 | subscription?: Subscription; 6 | } 7 | 8 | export interface Subscription { 9 | id?: string; 10 | active?: boolean; 11 | notification_options: NotificationOptions; 12 | } 13 | 14 | export enum NotificationOptions { 15 | EVERYTHING, 16 | MUTED 17 | } 18 | -------------------------------------------------------------------------------- /examples/typescript/core/src/core.ts: -------------------------------------------------------------------------------- 1 | export * from './ui'; 2 | export * from './app'; 3 | export * from './accounts'; 4 | export * from './channels'; 5 | -------------------------------------------------------------------------------- /examples/typescript/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from './core'; 2 | 3 | export default core; 4 | -------------------------------------------------------------------------------- /examples/typescript/core/src/ui/controller.ts: -------------------------------------------------------------------------------- 1 | import { action, Controller, state, event } from '@pulsejs/core'; 2 | import { AlertType, Theme, ThemeKey } from './types'; 3 | import { themes } from './themes'; 4 | 5 | class UI extends Controller { 6 | public state = { 7 | themeKey: state(ThemeKey.DARK).persist(), 8 | theme: state(() => themes[this.state.themeKey]) 9 | }; 10 | // public callbacks = { 11 | // onAlert: callback(), 12 | // onAppNotification: callback() 13 | // }; 14 | public events = { 15 | tabViewMounted: event() 16 | }; 17 | public alert = action(({}, type: AlertType, title?: string, message?: string) => { 18 | // log.info(type, title, message); 19 | // this.callbacks.onAlert.call(type, title, message); 20 | }); 21 | } 22 | 23 | export const ui = new UI(); 24 | -------------------------------------------------------------------------------- /examples/typescript/core/src/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './controller'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /examples/typescript/core/src/ui/themes.ts: -------------------------------------------------------------------------------- 1 | import { Theme, ThemeKey } from './types'; 2 | 3 | export const themes: Record = { 4 | // dark theme 5 | [ThemeKey.DARK]: { 6 | highlight: '#efefef' 7 | }, 8 | // light theme 9 | [ThemeKey.LIGHT]: { 10 | highlight: '#efefef' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /examples/typescript/core/src/ui/types.ts: -------------------------------------------------------------------------------- 1 | export interface UI {} 2 | 3 | export type AlertType = 'success' | 'error' | 'info'; 4 | 5 | export enum ThemeKey { 6 | DARK, 7 | LIGHT 8 | } 9 | 10 | export interface Theme { 11 | highlight: string; 12 | } 13 | -------------------------------------------------------------------------------- /examples/typescript/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "target": "ES2016", 6 | "lib": ["es5", "es6", "es2017", "dom"], 7 | "module": "CommonJS", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "strict": false, 12 | "composite": true, 13 | "noImplicitAny": false, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/typescript/core/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@pulsejs/core@latest": 6 | version "3.5.8" 7 | resolved "https://registry.yarnpkg.com/@pulsejs/core/-/core-3.5.8.tgz#63a2212d3d5dd37d72e8296ceefe2a5242fb7832" 8 | integrity sha512-EClwlMR/1xEUHMlHpF+QS8+4CKd+ZLzxNxpWL/r/02LvgrRlLexOsWvQ+ji7G7ygDCRNc5gLQXBf1K3f8PHWYA== 9 | -------------------------------------------------------------------------------- /examples/typescript/react/.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/react/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /examples/typescript/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-scripts": "3.4.3", 16 | "typescript": "~3.7.2", 17 | "pulse-example-core": "file:../core", 18 | "@pulsejs/core": "^4.0.0-beta.1", 19 | "@pulsejs/react": "^4.0.0-beta.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/typescript/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/react/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/typescript/react/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/react/public/logo192.png -------------------------------------------------------------------------------- /examples/typescript/react/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/react/public/logo512.png -------------------------------------------------------------------------------- /examples/typescript/react/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/typescript/react/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/typescript/react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/typescript/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | import { usePulse } from '@pulsejs/react'; 6 | import { resetState } from '@pulsejs/core'; 7 | 8 | import core from 'pulse-example-core'; 9 | 10 | //@ts-ignore 11 | globalThis['core'] = core; 12 | 13 | function MyApp() { 14 | const jeff = usePulse(core.app.state.assetURL); 15 | 16 | return ( 17 |
18 |
19 | logo 20 |

21 | Edit src/App.tsx and save to reload. 22 |

23 | 24 | {jeff} 25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default MyApp; 32 | -------------------------------------------------------------------------------- /examples/typescript/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /examples/typescript/react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/typescript/react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/typescript/react/src/setupTests.ts: -------------------------------------------------------------------------------- 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/extend-expect'; 6 | -------------------------------------------------------------------------------- /examples/typescript/react/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 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/typescript/vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /examples/typescript/vue/README.md: -------------------------------------------------------------------------------- 1 | # typescript-app 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /examples/typescript/vue/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/typescript/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11", 13 | "@pulsejs/core": "file:../../../packages/pulse-core", 14 | "@pulsejs/vue": "file:../../../packages/pulse-vue" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.5.0", 18 | "@vue/cli-plugin-eslint": "~4.5.0", 19 | "@vue/cli-service": "~4.5.0", 20 | "babel-eslint": "^10.1.0", 21 | "eslint": "^6.7.2", 22 | "eslint-plugin-vue": "^6.2.2", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "eslintConfig": { 26 | "root": true, 27 | "env": { 28 | "node": true 29 | }, 30 | "extends": [ 31 | "plugin:vue/essential", 32 | "eslint:recommended" 33 | ], 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | }, 37 | "rules": {} 38 | }, 39 | "browserslist": [ 40 | "> 1%", 41 | "last 2 versions", 42 | "not dead" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/typescript/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/vue/public/favicon.ico -------------------------------------------------------------------------------- /examples/typescript/vue/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/typescript/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | 34 | 44 | -------------------------------------------------------------------------------- /examples/typescript/vue/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pulse-framework/pulse/c2e56774567a5bda9c637c71ab7292f51c4e3231/examples/typescript/vue/src/assets/logo.png -------------------------------------------------------------------------------- /examples/typescript/vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /examples/typescript/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /examples/typescript/vue/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: config => config.resolve.symlinks(false) 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | rootDir: 'packages', 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: 'tsconfig.test.json' 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.2.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "npmClient": "yarn", 7 | "useWorkspaces": true, 8 | "version": "4.1.3" 9 | } 10 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "pulse", 4 | "public": true, 5 | "builds": [ 6 | { 7 | "src": "package.json", 8 | "use": "@now/static-build", 9 | "config": { 10 | "distDir": "./docs/dist" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulse-framework", 3 | "version": "4.0.0", 4 | "description": "Global state and logic framework for reactive JavaScript & TypeScript applications.", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Jamie Pine", 8 | "url": "https://github.com/jamiepine" 9 | }, 10 | "files": [ 11 | "dist", 12 | "package.json", 13 | "LICENCE" 14 | ], 15 | "private": true, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/pulse-framework/pulse" 19 | }, 20 | "scripts": { 21 | "bootstrap": "lerna bootstrap", 22 | "remove:build": "rimraf packages/**/dist packages/**/tsconfig.tsbuildinfo", 23 | "remove:install": "lerna clean && rimraf node_modules", 24 | "setup:react": "yarn build && (cd packages/pulse-react && yarn link) && (cd packages/pulse-core && yarn link) && (cd examples/typescript/core && yarn link @pulsejs/core && yarn link) && (cd examples/typescript/react && yarn link @pulsejs/react && yarn link @pulsejs/core && yarn link pulse-example-core)", 25 | "test:react": "(cd examples/typescript/react && yarn start)", 26 | "test:core": "(cd examples/typescript/core && tsc -W)", 27 | "build": "yarn remove:build && tsc -b packages/pulse-core packages/pulse-react packages/pulse-vue packages/pulse-next", 28 | "dev": "yarn remove:build && tsc -b --watch packages/pulse-core packages/pulse-react packages/pulse-vue packages/pulse-next", 29 | "test": "jest", 30 | "release": "yarn build && lerna publish --force-publish", 31 | "docs:dev": "vuepress dev docs", 32 | "docs:build": "vuepress build docs --dest ./docs/dist/", 33 | "now-build": "yarn run docs:build", 34 | "postinstall": "git config --local core.hooksPath .githooks" 35 | }, 36 | "devDependencies": { 37 | "@types/jest": "^26.0.13", 38 | "@types/node": "^8.0.28", 39 | "eslint-config-prettier": "^6.11.0", 40 | "jest": "^26.4.2", 41 | "lerna": "^2.9.0", 42 | "prettier": "2.0.5", 43 | "rimraf": "^2.6.2", 44 | "ts-jest": "^26.3.0", 45 | "typescript": "^3.9.7", 46 | "vuepress": "^1.5.4" 47 | }, 48 | "workspaces": [ 49 | "packages/*" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/pulse-core/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | "node_modules" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/pulse-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jamie Pine 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 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/collection/data.ts: -------------------------------------------------------------------------------- 1 | import { State, Collection, DefaultDataItem } from '../internal'; 2 | 3 | export class Data extends State { 4 | public output: DataType | DefaultDataItem; 5 | constructor(private collection: () => Collection, data: DataType) { 6 | super(collection().instance, data); 7 | this.type(Object); 8 | this.name = data && data[collection().config?.primaryKey]; 9 | } 10 | } 11 | export default Data; 12 | 13 | // collection should detect if computed data dependency is own group, if so handle efficiently 14 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/collection/model.ts: -------------------------------------------------------------------------------- 1 | import { DefaultDataItem } from '../internal'; 2 | 3 | interface DataPropertyConfig { 4 | index?: boolean; 5 | type?: 'string' | 'array' | 'boolean' | 'object' | 'number'; 6 | maxLength?: number; 7 | minLength?: number; 8 | required?: boolean; 9 | optional?: boolean; 10 | computed?: (data: DataType) => DataType; 11 | } 12 | 13 | export class Model { 14 | public string(): this { 15 | return this; 16 | } 17 | public max(amount: number): this { 18 | return this; 19 | } 20 | public min(amount: number): this { 21 | return this; 22 | } 23 | public required(): this { 24 | return this; 25 | } 26 | public index(): this { 27 | return this; 28 | } 29 | public optional(): this { 30 | return this; 31 | } 32 | public hidden(): this { 33 | return this; 34 | } 35 | public if(condition: any): this { 36 | return this; 37 | } 38 | public compute(func: (...args: any) => any): this { 39 | return this; 40 | } 41 | } 42 | 43 | export const model = new Model(); 44 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/collection/selector.ts: -------------------------------------------------------------------------------- 1 | import { Computed, Collection, DefaultDataItem, Data, GroupObj, SelectorObj, PrimaryKey } from '../internal'; 2 | 3 | export type SelectorName = string | number; 4 | 5 | export class Selector< 6 | // Generics 7 | DataType extends DefaultDataItem = DefaultDataItem 8 | // 9 | > extends Computed { 10 | protected collection: () => Collection; 11 | // this is the selected primary key 12 | private _id: PrimaryKey = 0; 13 | 14 | // getter and setter for primary key 15 | public set id(val: PrimaryKey) { 16 | this._id = val; 17 | this.recompute(); 18 | } 19 | public get id() { 20 | if (this.instance().runtime.trackState) this.instance().runtime.foundState.add(this); 21 | return this._id; 22 | } 23 | 24 | constructor(collection: () => Collection, key: PrimaryKey) { 25 | // initialize computed constructor with initial compute state 26 | super(collection().instance, () => Selector.findData(collection(), key)); 27 | 28 | // computed function that returns a given item from collection 29 | this.func = () => Selector.findData(collection(), this._id); 30 | 31 | // alias collection function 32 | this.collection = collection; 33 | 34 | this.type(Object); 35 | 36 | this._id = key; 37 | } 38 | 39 | public select(key: PrimaryKey) { 40 | this.id = key; 41 | } 42 | 43 | // custom override for the State persist function 44 | public persist(key?: string) { 45 | this.persistState = true; 46 | this.instance().storage.handleStatePersist(this, key); 47 | return this; 48 | } 49 | 50 | public getPersistableValue() { 51 | return this.id; 52 | } 53 | 54 | static findData(collection: Collection, key: PrimaryKey) { 55 | if (key == undefined) return null; 56 | 57 | let data = collection.getData(key).value; 58 | // if data is not found, create placeholder data, so that when real data is collected it maintains connection 59 | if (!data) { 60 | // this could be improved by storing temp references outside data object in collection 61 | collection.data[key] = new Data(() => collection, { id: key } as any); 62 | data = collection.getData(key).value; 63 | } else { 64 | // If we have a computed function, run it before returning the data. 65 | data = collection._computedFunc ? collection._computedFunc(data) : data; 66 | } 67 | return data; 68 | } 69 | 70 | public reset(): this { 71 | super.reset(); 72 | this._id = 0; 73 | return this; 74 | } 75 | } 76 | 77 | export default Selector; 78 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/computed.ts: -------------------------------------------------------------------------------- 1 | import { Pulse } from './pulse'; 2 | import { State, SetFunc } from './internal'; 3 | 4 | export class Computed extends State { 5 | // private cleanup: Set = new Set(); 6 | public set value(val: ComputedValueType) { 7 | console.error('Error: Can not mutate Computed value, please use recompute()'); 8 | } 9 | 10 | public get value(): ComputedValueType { 11 | return this._value; 12 | } 13 | 14 | public set bind(val: ComputedValueType) { 15 | console.error('Error: Can not bind Computed value'); 16 | } 17 | 18 | constructor(public instance: () => Pulse, public func: () => ComputedValueType, public deps?: Array) { 19 | super(instance, instance().config.computedDefault || null); 20 | 21 | instance()._computed.add(this); 22 | 23 | if (typeof func !== 'function') throw new TypeError('A compute function must be provided to Computed.'); 24 | 25 | if (deps) deps.forEach(state => state.dep.depend(this)); 26 | 27 | // if Core will not be used, or Pulse in a post-core state (ready), compute immediately 28 | if (instance().config.noCore === true || instance().ready) this.recompute(); 29 | } 30 | 31 | public computeValue(): ComputedValueType | SetFunc { 32 | if (this.deps) return this.func(); 33 | 34 | this.instance().runtime.trackState = true; 35 | 36 | const computed = this.func(); 37 | let dependents = this.instance().runtime.getFoundState(); 38 | dependents.forEach(state => state.dep.depend(this)); 39 | return computed; 40 | } 41 | 42 | public recompute(): void { 43 | this.set(this.computeValue()); 44 | } 45 | 46 | public reset(): this { 47 | super.reset(); 48 | this.recompute(); 49 | return this; 50 | } 51 | 52 | public patch() { 53 | console.error('Error, can not use patch method on Computed since the value is dynamic.'); 54 | return this; 55 | } 56 | 57 | public persist(key?: string): this { 58 | console.error('Computed state can not be persisted, remove call to .persist()', key); 59 | return this; 60 | } 61 | } 62 | 63 | export default Computed; 64 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/controller.ts: -------------------------------------------------------------------------------- 1 | export class Controller { 2 | public name?: string; 3 | public reset(): void {} 4 | } 5 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/dep.ts: -------------------------------------------------------------------------------- 1 | import { State, SubscriptionContainer } from './internal'; 2 | 3 | export class Dep { 4 | public deps: Set = new Set(); 5 | public subs: Set = new Set(); 6 | 7 | constructor(initialDeps?: Array, private instance?: () => State) { 8 | if (initialDeps) initialDeps.forEach(dep => this.deps.add(dep)); 9 | } 10 | 11 | public depend(instance: State) { 12 | if (instance.dep === this) return; 13 | this.deps.add(instance); 14 | } 15 | } 16 | 17 | export default Dep; 18 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/helpers/debounce.ts: -------------------------------------------------------------------------------- 1 | interface DebouncedFunction { 2 | (): any; 3 | cancel: () => void; 4 | } 5 | 6 | export const debounce = ReturnType>(func: F, wait: number, immediate?: boolean) => { 7 | let timeout: number = 0; 8 | 9 | const debounced: DebouncedFunction = function (this: void) { 10 | const context: any = this; 11 | const args = arguments; 12 | 13 | const later = function () { 14 | timeout = 0; 15 | if (!immediate) func.call(context, ...args); 16 | }; 17 | const callNow = immediate && !timeout; 18 | clearTimeout(timeout); 19 | timeout = window.setTimeout(later, wait); 20 | if (callNow) func.call(context, ...args); 21 | }; 22 | 23 | debounced.cancel = function () { 24 | clearTimeout(timeout); 25 | timeout = 0; 26 | }; 27 | 28 | return debounced as (...args: Parameters) => ReturnType; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/helpers/isWatchableObj.ts: -------------------------------------------------------------------------------- 1 | export function isWatchableObject(value) { 2 | function isHTMLElement(obj) { 3 | try { 4 | return obj instanceof HTMLElement; 5 | } catch (e) { 6 | return typeof obj === 'object' && obj.nodeType === 1 && typeof obj.style === 'object' && typeof obj.ownerDocument === 'object'; 7 | } 8 | } 9 | let type = typeof value; 10 | return value != null && type == 'object' && !isHTMLElement(value) && !Array.isArray(value); 11 | } 12 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './internal'; 2 | import { Pulse } from './pulse'; 3 | export default Pulse; 4 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/integrate.ts: -------------------------------------------------------------------------------- 1 | import { Pulse } from './pulse'; 2 | 3 | export interface IntegrationConfig { 4 | name?: any; 5 | foreignInstance?: T; 6 | updateMethod?: (componentInstance: any, updateProperties: any) => void; 7 | onPulseReady?: (pulseInstance: Pulse) => void; 8 | onCoreReady?: (pulseInstance: Pulse) => void; 9 | } 10 | 11 | // import this into integration modules to create a 12 | export class Integration { 13 | public ready?: boolean; 14 | constructor(public config: IntegrationConfig) {} 15 | } 16 | 17 | export class Integrations { 18 | public loaded: { [key: string]: Integration } = {}; 19 | public loadedSet: Set = new Set(); 20 | constructor(public instance: () => Pulse) { 21 | if (Pulse.initialIntegrations) Pulse.initialIntegrations.forEach(int => this.use(int)); 22 | } 23 | public use(integration: Integration) { 24 | if (!(integration instanceof Integration) || !integration.config.name) throw 'Pulse Error: Not a valid integration object'; 25 | this.loaded[integration.config.name] = integration; 26 | this.loadedSet.add(integration); 27 | } 28 | // Event runners 29 | public pulseReady() { 30 | this.loadedSet.forEach(integration => integration.config.onPulseReady && integration.config.onPulseReady(this.instance())); 31 | } 32 | public coreReady() { 33 | this.loadedSet.forEach(integration => integration.config.onCoreReady && integration.config.onCoreReady(this.instance())); 34 | } 35 | public update(componentInstance: any, updateProperties: any) { 36 | this.loadedSet.forEach(integration => integration.config.updateMethod && integration.config.updateMethod(componentInstance, updateProperties)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/internal.ts: -------------------------------------------------------------------------------- 1 | // This file exposes Pulse functions and types to the outside world. 2 | // It also serves as a cyclic dependency workaround. 3 | // All internal Pulse modules must import from here 4 | 5 | // Internal Classes 6 | export { Integration, Integrations } from './integrate'; 7 | export { Runtime } from './runtime'; 8 | export { Tracker } from './tracker'; 9 | export { Storage } from './storage'; 10 | export { Dep } from './dep'; 11 | export { SubController, ComponentContainer, CallbackContainer } from './sub'; 12 | // export { StatusTracker } from './status'; 13 | 14 | // State 15 | export { State, StateGroup, HistoryItem } from './state'; 16 | export { Computed } from './computed'; 17 | 18 | // Collections 19 | export { Collection, CollectionConfig } from './collection/collection'; 20 | export { Group } from './collection/group'; 21 | export { Selector, SelectorName } from './collection/selector'; 22 | export { Data } from './collection/data'; 23 | export * from './collection/model'; 24 | 25 | // Controllers 26 | export { Controller } from './controller'; 27 | 28 | // Events 29 | export { Event } from './event'; 30 | 31 | // Actions 32 | export { Action } from './action'; 33 | 34 | // API 35 | export { API } from './api'; 36 | 37 | // Helper functions 38 | export { cleanState, resetState, normalizeDeps, getPulseInstance } from './utils'; 39 | export { persist } from './storage'; 40 | export { isWatchableObject } from './helpers/isWatchableObj'; 41 | 42 | // Types 43 | export { SetFunc } from './state'; 44 | export { IJob } from './runtime'; 45 | export { FuncType } from './action'; 46 | export { SubscriptionContainer } from './sub'; 47 | export { APIConfig, PulseResponse } from './api'; 48 | export { PrimaryKey, GroupName, GroupAddOptions } from './collection/group'; 49 | export { StorageConfig } from './storage'; 50 | export { EventPayload, EventConfig, EventsObjFunc, EventCallbackFunc } from './event'; 51 | export { GroupObj, DefaultDataItem, SelectorObj, Config } from './collection/collection'; 52 | 53 | export * from './pulse'; 54 | 55 | export * from './instance'; 56 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/status.ts: -------------------------------------------------------------------------------- 1 | import { Pulse } from './pulse'; 2 | import { State } from './internal'; 3 | import { copy } from './utils'; 4 | 5 | interface StatusObjectData { 6 | message: string | null; 7 | status: 'invalid' | 'success' | 'error' | null; 8 | } 9 | 10 | const initialData: StatusObjectData = { 11 | message: null, 12 | status: null 13 | }; 14 | 15 | export class StatusTracker { 16 | //@ts-ignore 17 | public state: State<{ [key: string]: StatusObjectData }>; 18 | 19 | public get all(): { [key: string]: StatusObjectData } { 20 | return this.state.value; 21 | } 22 | 23 | constructor(private instance: () => Pulse) { 24 | this.state = new State(instance, {}); 25 | } 26 | 27 | public get(key: string): StatusObjectData { 28 | return this?.state?.value[key]; 29 | } 30 | 31 | public set(key: string): StatusObject { 32 | if (!this.state.value[key]) { 33 | this.state.set(Object.assign(copy(this.state.value), { [key]: initialData })); 34 | } 35 | 36 | return new StatusObject(this.state, key); 37 | } 38 | 39 | public remove(key: string): void { 40 | if (!this.state.value[key]) return; 41 | 42 | const copiedState: { [key: string]: StatusObjectData } = copy(this.state.value); 43 | 44 | copiedState[key] = undefined; 45 | delete copiedState[key]; 46 | 47 | this.state.set(copiedState); 48 | } 49 | 50 | public clear(key?: string): void { 51 | // clearing a specific value 52 | if (key) { 53 | if (!this.state.value[key]) return; 54 | 55 | const copiedState: { [key: string]: StatusObjectData } = copy(this.state.value); 56 | 57 | copiedState[key] = initialData; 58 | 59 | this.state.set(copiedState); 60 | 61 | return; 62 | } 63 | 64 | this.state.reset(); 65 | } 66 | } 67 | 68 | export class StatusObject { 69 | constructor(private state: State<{ [key: string]: StatusObjectData }>, private key: string) {} 70 | 71 | public status(newStatus: 'invalid' | 'success' | 'error' | 'none'): StatusObject { 72 | this.state.nextState[this.key].status = newStatus === 'none' ? null : newStatus; 73 | this.state.set(); 74 | return this; 75 | } 76 | public message(messageText: string): StatusObject { 77 | this.state.nextState[this.key].message = messageText; 78 | this.state.set(); 79 | return this; 80 | } 81 | } 82 | 83 | export default StatusTracker; 84 | -------------------------------------------------------------------------------- /packages/pulse-core/lib/tracker.ts: -------------------------------------------------------------------------------- 1 | import { Pulse } from './pulse'; 2 | import { IJob } from './internal'; 3 | 4 | export class Tracker { 5 | jobs: Set = new Set(); 6 | 7 | constructor(public instance: () => Pulse, changeFunc: () => void) { 8 | this.instance().runtime.trackers.add(this); 9 | changeFunc(); 10 | this.instance().runtime.trackers.delete(this); 11 | } 12 | 13 | public ingest(job: IJob) { 14 | this.jobs.add(job); 15 | } 16 | 17 | public undo() {} 18 | 19 | public destroy() {} 20 | } 21 | -------------------------------------------------------------------------------- /packages/pulse-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pulsejs/core", 3 | "version": "4.1.3", 4 | "author": "Jamie Pine", 5 | "license": "MIT", 6 | "description": "Global state and logic framework for reactive JavaScript & TypeScript applications.", 7 | "keywords": [], 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc", 12 | "dev": "tsc --watch" 13 | }, 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/pulse-framework/pulse" 20 | }, 21 | "files": [ 22 | "dist/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/collection.oldtest.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Collection, Group, Data, Selector } from '../lib'; 2 | 3 | interface ExampleData { 4 | id: string; 5 | } 6 | 7 | let // 8 | App: Pulse, 9 | MyCollection: Collection; 10 | 11 | const dataCount = 5; 12 | 13 | function createExampleDataArray() { 14 | let i: number = 0, 15 | arr: ExampleData[] = []; 16 | while (i <= dataCount - 1) { 17 | arr.push({ id: (Math.floor(Math.random() * 10000) + 1 + Math.random()).toString() }); 18 | i++; 19 | } 20 | return arr; 21 | } 22 | 23 | beforeAll(() => { 24 | App = new Pulse({ noCore: true }); 25 | }); 26 | 27 | beforeEach(() => { 28 | MyCollection = App.Collection()(collection => ({ 29 | defaultGroup: true, 30 | groups: { explicitlyDefinedGroup: collection.Group() }, 31 | selectors: { mySelector: collection.Selector() } 32 | })); 33 | }); 34 | 35 | describe('Collections', () => { 36 | test('Collection has reference to the Pulse instance', () => { 37 | expect(MyCollection.instance() instanceof Pulse).toBe(true); 38 | }); 39 | 40 | test('Collection is configured correctly', () => { 41 | expect(MyCollection.groups['default'] instanceof Group).toBe(true); 42 | expect(MyCollection.groups['explicitlyDefinedGroup'] instanceof Group).toBe(true); 43 | expect(MyCollection.selectors['mySelector'] instanceof Selector).toBe(true); 44 | }); 45 | 46 | function testGroupValues(groupName: string) { 47 | MyCollection.collect(createExampleDataArray(), groupName); 48 | expect(MyCollection.getGroup(groupName).value.length).toBe(dataCount); 49 | expect(MyCollection.getGroup(groupName).output.length).toBe(dataCount); 50 | // index is only set after output is accessed once 51 | expect(MyCollection.getGroup(groupName).index.length).toBe(dataCount); 52 | } 53 | 54 | test('Data is present in default group', () => testGroupValues('default')); 55 | 56 | test('Data is present in a dynamic group', () => testGroupValues('haha')); 57 | 58 | test('Data is present in a explicitly defined group', () => testGroupValues('explicitlyDefinedGroup')); 59 | 60 | test('Provisional data works correctly', () => { 61 | const exampleData = createExampleDataArray(); 62 | const chosenId = exampleData[3].id; 63 | // get the data, creating a provisional data instance 64 | const data = MyCollection.getData(chosenId); 65 | 66 | const watcherId = data.watch(() => {}); 67 | 68 | expect(MyCollection._provisionalData[chosenId]).toBe(data); 69 | // collect data, one of these items is key 3 70 | MyCollection.collect(exampleData); 71 | 72 | expect(MyCollection._provisionalData[chosenId]).toBe(undefined); 73 | expect(MyCollection.data[chosenId]).toBe(data); 74 | expect(MyCollection.data[chosenId].watchers[watcherId]).toBeDefined(); 75 | }); 76 | 77 | test('getDataValue returns null if data does not exist', () => { 78 | expect(MyCollection.getDataValue('myNameJeff')).toBe(null); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/collection.test.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Collection, Group, Data, Selector, collection } from '../lib'; 2 | 3 | interface ExampleData { 4 | id: string; 5 | } 6 | 7 | let // 8 | App: Pulse, 9 | MyCollection: Collection; 10 | 11 | const dataCount = 5; 12 | 13 | function createExampleDataArray() { 14 | let i: number = 0, 15 | arr: ExampleData[] = []; 16 | while (i <= dataCount - 1) { 17 | arr.push({ id: (Math.floor(Math.random() * 10000) + 1 + Math.random()).toString() }); 18 | i++; 19 | } 20 | return arr; 21 | } 22 | 23 | beforeEach(() => { 24 | MyCollection = collection().createGroup('explicitlyDefinedGroup').createSelector('mySelector'); 25 | 26 | MyCollection; 27 | }); 28 | 29 | describe('Collections', () => { 30 | test('Collection has reference to the Pulse instance', () => { 31 | expect(MyCollection.instance() instanceof Pulse).toBe(true); 32 | }); 33 | 34 | test('Collection is configured correctly', () => { 35 | expect(MyCollection.groups['default'] instanceof Group).toBe(true); 36 | expect(MyCollection.groups['explicitlyDefinedGroup'] instanceof Group).toBe(true); 37 | expect(MyCollection.selectors['mySelector'] instanceof Selector).toBe(true); 38 | }); 39 | 40 | function testGroupValues(groupName: string) { 41 | MyCollection.collect(createExampleDataArray(), groupName); 42 | expect(MyCollection.getGroup(groupName).value.length).toBe(dataCount); 43 | expect(MyCollection.getGroup(groupName).output.length).toBe(dataCount); 44 | // index is only set after output is accessed once 45 | expect(MyCollection.getGroup(groupName).index.length).toBe(dataCount); 46 | } 47 | 48 | test('Data is present in default group', () => testGroupValues('default')); 49 | 50 | test('Data is present in a dynamic group', () => testGroupValues('haha')); 51 | 52 | test('Data is present in a explicitly defined group', () => testGroupValues('explicitlyDefinedGroup')); 53 | 54 | test('Provisional data works correctly', () => { 55 | const exampleData = createExampleDataArray(); 56 | const chosenId = exampleData[3].id; 57 | // get the data, creating a provisional data instance 58 | const data = MyCollection.getData(chosenId); 59 | 60 | const watcherId = data.watch(() => {}); 61 | 62 | expect(MyCollection._provisionalData[chosenId]).toBe(data); 63 | // collect data, one of these items is key 3 64 | MyCollection.collect(exampleData); 65 | 66 | expect(MyCollection._provisionalData[chosenId]).toBe(undefined); 67 | expect(MyCollection.data[chosenId]).toBe(data); 68 | expect(MyCollection.data[chosenId].watchers[watcherId]).toBeDefined(); 69 | }); 70 | 71 | test('getDataValue returns null if data does not exist', () => { 72 | expect(MyCollection.getDataValue('myNameJeff')).toBe(null); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/computed.oldtest.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Computed, State } from '../lib'; 2 | 3 | let // 4 | App: Pulse, 5 | NumberState: State, 6 | ComputedNumber: Computed; 7 | 8 | beforeAll(() => { 9 | App = new Pulse({ noCore: true }); 10 | }); 11 | 12 | beforeEach(() => { 13 | NumberState = App.State(1); 14 | ComputedNumber = App.Computed(() => NumberState.value + 2); 15 | }); 16 | 17 | describe('Computed State', () => { 18 | describe('.value | Check that computation occurred properly', () => { 19 | test('Computed State can be initializes successfully', () => { 20 | //Verify state was created and can be retrieved 21 | expect(ComputedNumber.value).toBe(3); 22 | }); 23 | 24 | test('Computed State auto recomputes', () => { 25 | //Mutate number state to (2) 26 | NumberState.set(2); 27 | //Verify state was created and can be retrieved 28 | expect(ComputedNumber.value).toBe(4); 29 | }); 30 | }); 31 | 32 | test('Does not error with no function', () => { 33 | const makeComputed = jest.fn(() => App.Computed(null)); 34 | 35 | expect(makeComputed).toThrowError(TypeError); // does not throw error 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/computed.test.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Computed, State, state } from '../lib'; 2 | 3 | let // 4 | App: Pulse, 5 | NumberState: State, 6 | ComputedNumber: Computed; 7 | 8 | beforeEach(() => { 9 | NumberState = state(1); 10 | ComputedNumber = state(() => NumberState.value + 2); 11 | }); 12 | 13 | describe('Computed State', () => { 14 | describe('.value | Check that computation occurred properly', () => { 15 | test('Computed State can be initializes successfully', () => { 16 | //Verify state was created and can be retrieved 17 | expect(ComputedNumber.value).toBe(3); 18 | }); 19 | 20 | test('Computed State auto recomputes', () => { 21 | //Mutate number state to (2) 22 | NumberState.set(2); 23 | //Verify state was created and can be retrieved 24 | expect(ComputedNumber.value).toBe(4); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/pulse.oldtest.ts: -------------------------------------------------------------------------------- 1 | import Pulse from '../lib'; 2 | 3 | let App: Pulse; 4 | 5 | beforeAll(() => { 6 | App = new Pulse({ noCore: true }); 7 | }); 8 | 9 | describe('Pulse Generic', () => { 10 | test('Pulse instance is created', () => { 11 | expect(App).toHaveProperty('runtime'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/pulse.test.ts: -------------------------------------------------------------------------------- 1 | import { instance } from '../lib'; 2 | 3 | describe('Pulse Generic', () => { 4 | test('Pulse instance is created', () => { 5 | expect(instance).toHaveProperty('runtime'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/pulse-core/tests/util.ts: -------------------------------------------------------------------------------- 1 | export interface Days { 2 | monday: any; 3 | tuesday: any; 4 | wednesday: any; 5 | thursday: any; 6 | friday: any; 7 | saturday: any; 8 | sunday: any; 9 | } 10 | 11 | export const DefaultLoggers = { 12 | info: console.info, 13 | warn: console.warn, 14 | error: console.error, 15 | log: console.log 16 | }; 17 | 18 | export function restoreDefaultLoggers(): void { 19 | for (const name in DefaultLoggers) // 20 | console[name] = DefaultLoggers[name]; 21 | } 22 | 23 | export function makeMockLoggers(): void { 24 | for (const name in DefaultLoggers) // 25 | console[name] = jest.fn(); 26 | } 27 | -------------------------------------------------------------------------------- /packages/pulse-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/pulse-next/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jamie Pine 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 | -------------------------------------------------------------------------------- /packages/pulse-next/lib/index.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Integration } from '@pulsejs/core'; 2 | import { loadServerState } from './loader'; 3 | 4 | export { preserveServerState, loadServerState } from './loader'; 5 | 6 | export * from '@pulsejs/react'; 7 | 8 | // declare pulse plugin 9 | export const PulseNext = new Integration({ 10 | name: 'next', 11 | onCoreReady: loadServerState 12 | }); 13 | 14 | Pulse.initialIntegrations.push(PulseNext); 15 | 16 | export default Pulse; 17 | -------------------------------------------------------------------------------- /packages/pulse-next/lib/loader.ts: -------------------------------------------------------------------------------- 1 | import { Pulse, Computed, PrimaryKey } from '@pulsejs/core'; 2 | 3 | interface PulseData { 4 | state: { [key: string]: any }; 5 | collections: Array<{ 6 | data: any; 7 | groups: { 8 | [key: string]: Array; 9 | }; 10 | selectors: { 11 | [key: string]: any; 12 | }; 13 | name: string; 14 | }>; 15 | } 16 | 17 | export function preserveServerState(nextProps: { [key: string]: any }, instance?: Pulse): any { 18 | if (!instance) instance = getPulseInstance(); 19 | const { _collections: collections, _state: state } = instance; 20 | 21 | const PULSE_DATA: PulseData = { 22 | collections: [], 23 | state: {} 24 | }; 25 | if (state) 26 | state.forEach(stateItem => { 27 | if (stateItem.name && stateItem.isSet && !(stateItem instanceof Computed)) PULSE_DATA.state[stateItem.name] = stateItem._value; 28 | }); 29 | if (collections) { 30 | for (const collection of collections) { 31 | if (collection.config?.name) { 32 | const collectionData = { data: [], groups: {}, selectors: {}, name: collection.config.name }; 33 | 34 | for (let key in collection.data) if (collection.data[key]._value !== undefined) collectionData.data.push(collection.data[key]._value); 35 | 36 | for (let key in collection.groups as any) 37 | if (collection.groups[key]._value.length > 0) collectionData.groups[key] = collection.groups[key]._value; 38 | 39 | for (let key in collection.selectors) if (collection.selectors[key].isSet) collectionData.selectors[key] = collection.selectors[key]._value; 40 | 41 | PULSE_DATA.collections.push(collectionData); 42 | } 43 | } 44 | } 45 | 46 | nextProps.props.PULSE_DATA = PULSE_DATA; 47 | 48 | return nextProps; 49 | } 50 | 51 | export function loadServerState(pulse?: Pulse, pulseData: PulseData = globalThis?.__NEXT_DATA__?.props?.pageProps?.PULSE_DATA) { 52 | if (isServer()) return; 53 | 54 | if (!pulse) pulse = getPulseInstance(); 55 | 56 | if (pulseData) { 57 | 58 | for (const state of pulse._state.values()) { 59 | if (state.name && pulseData.state[state.name] && !(state instanceof Computed)) state.set(pulseData.state[state.name]); 60 | } 61 | 62 | for (const collection of pulse._collections.values()) { 63 | if (collection && collection.config.name) { 64 | const fromSSR = pulseData.collections.find(c => c.name === collection.config.name); 65 | if (fromSSR) { 66 | if (fromSSR.groups) { 67 | for (const key in fromSSR.groups) { 68 | const groupKeys = fromSSR.groups[key]; 69 | if (groupKeys && groupKeys.length > 0) { 70 | if (!collection.groups[key]) collection.createGroup(key, groupKeys); 71 | else collection.groups[key].add(groupKeys); 72 | const toCol = fromSSR.data.filter(d => groupKeys.includes(d[collection.config.primaryKey])); 73 | for (const data of toCol) collection.collect(data, key); 74 | } 75 | } 76 | } 77 | 78 | if (fromSSR.data?.length > 0) collection.collect(fromSSR.data); 79 | 80 | for (const key in fromSSR.selectors) if (collection.selectors[key].name) collection.selectors[key].set(fromSSR.selectors[key]); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | export function isServer() { 88 | return typeof process !== 'undefined' && process?.release?.name === 'node'; 89 | } 90 | export function getPulseInstance(): Pulse { 91 | return globalThis['__pulse__app'] || false; 92 | } 93 | -------------------------------------------------------------------------------- /packages/pulse-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pulsejs/next", 3 | "version": "4.1.3", 4 | "author": "Jamie Pine", 5 | "license": "MIT", 6 | "description": "Next.js integration for PulseJS", 7 | "keywords": [], 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc" 12 | }, 13 | "peerDependencies": { 14 | "@pulsejs/core": "^4.0.0-beta.1", 15 | "@pulsejs/react": "^4.0.0-beta.1", 16 | "react": "^16.13.1" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.49" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/pulse-framework/pulse" 27 | }, 28 | "files": [ 29 | "dist/**/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/pulse-next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/pulse-react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jamie Pine 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 | -------------------------------------------------------------------------------- /packages/pulse-react/lib/index.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Pulse, { Integration } from '@pulsejs/core'; 3 | 4 | export { PulseHOC } from './pulseHOC'; 5 | export { usePulse } from './usePulse'; 6 | export { useEvent } from './useEvent'; 7 | export { useWatcher } from './useWatcher'; 8 | 9 | export * from '@pulsejs/core'; 10 | export default Pulse; 11 | 12 | // declare pulse plugin 13 | export const PulseReact = new Integration({ 14 | name: 'react', 15 | foreignInstance: React, 16 | // used by the pulseHOC 17 | updateMethod(component, payload) { 18 | // UpdatedData will be empty if the PulseHOC doesn't get an object as deps 19 | if (Object.keys(payload).length !== 0) { 20 | // Update Props 21 | component.updatedProps = { ...component.updatedProps, ...payload }; 22 | 23 | // Set State (Rerender) 24 | component.setState(payload); 25 | } else { 26 | // Force Update (Rerender) 27 | component.forceUpdate(); 28 | } 29 | } 30 | }); 31 | 32 | Pulse.initialIntegrations.push(PulseReact); 33 | -------------------------------------------------------------------------------- /packages/pulse-react/lib/pulseHOC.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { State, Pulse, normalizeDeps, getPulseInstance, SubscriptionContainer } from '@pulsejs/core'; 3 | 4 | export function PulseHOC(ReactComponent: any, deps?: Array | { [key: string]: State } | State, pulseInstance?: Pulse) { 5 | let depsArray: Array; 6 | let depsObject: { [key: string]: State }; 7 | 8 | if (deps instanceof State || Array.isArray(deps)) { 9 | // Normalize Dependencies 10 | depsArray = normalizeDeps(deps || []); 11 | 12 | // Get Pulse Instance 13 | if (!pulseInstance) { 14 | if (depsArray.length > 0) { 15 | const tempPulseInstance = getPulseInstance(depsArray[0]); 16 | pulseInstance = tempPulseInstance || undefined; 17 | } else { 18 | console.warn("Pulse: Please don't pass an empty array!"); 19 | } 20 | } 21 | } else if (typeof deps === 'object') { 22 | depsObject = deps; 23 | 24 | // Get Pulse Instance 25 | if (!pulseInstance) { 26 | const objectKeys = Object.keys(depsObject); 27 | if (objectKeys.length > 0) { 28 | const tempPulseInstance = getPulseInstance(depsObject[objectKeys[0]]); 29 | pulseInstance = tempPulseInstance || undefined; 30 | } else { 31 | console.warn("Pulse: Please don't pass an empty object!"); 32 | } 33 | } 34 | } else { 35 | console.error('Pulse: No Valid PulseHOC properties'); 36 | return ReactComponent; 37 | } 38 | // Check if pulse Instance exists 39 | if (!pulseInstance) { 40 | console.error('Pulse: Failed to get Pulse Instance'); 41 | return ReactComponent; 42 | } 43 | 44 | return class extends React.Component { 45 | public componentContainer: SubscriptionContainer | null = null; // Will be set in registerSubscription (sub.ts) 46 | public updatedProps = this.props; 47 | 48 | constructor(props: any) { 49 | super(props); 50 | 51 | // Create HOC based Subscription with Array (Rerenders will here be caused via force Update) 52 | if (depsArray) pulseInstance?.subController.subscribeWithSubsArray(this, depsArray); 53 | 54 | // Create HOC based Subscription with Object 55 | if (depsObject) { 56 | const response = pulseInstance?.subController.subscribeWithSubsObject(this, depsObject); 57 | this.updatedProps = { 58 | ...props, 59 | ...response?.props 60 | }; 61 | 62 | // Defines State for causing rerender (will be called in updateMethod) 63 | this.state = depsObject; 64 | } 65 | } 66 | componentDidMount() { 67 | if (pulseInstance?.config.waitForMount) pulseInstance?.subController.mount(this); 68 | } 69 | componentWillUnmount() { 70 | pulseInstance?.subController.unsubscribe(this); 71 | } 72 | render() { 73 | return React.createElement(ReactComponent, this.updatedProps); 74 | } 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /packages/pulse-react/lib/useEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event, EventCallbackFunc, Pulse } from '@pulsejs/core'; 2 | import * as React from 'react'; 3 | // useEvent helper for using Events inside React components as hooks 4 | export function useEvent(event: E, callback: EventCallbackFunc, pulseInstance?: Pulse) { 5 | // get the instance of Pulse 6 | if (!pulseInstance) pulseInstance = event.instance(); 7 | React.useEffect(() => { 8 | // call the event on component mount 9 | const unsub = event.on(callback); 10 | // remove the event on component unmount 11 | return () => unsub(); 12 | }, []); 13 | } 14 | -------------------------------------------------------------------------------- /packages/pulse-react/lib/usePulse.ts: -------------------------------------------------------------------------------- 1 | import { Pulse, State, normalizeDeps, getPulseInstance, Group } from '@pulsejs/core'; 2 | import React from 'react'; 3 | 4 | globalThis.React1 = React; 5 | 6 | // usePulse returns the State value, or an array of State values, not the instances themselves. 7 | // This type will extract the inferred value of State 8 | // We use a TypeScript ternary to detect which type of Pulse class we're working with. 9 | export type PulseValue = T extends Group ? U[] : T extends State ? U : never; 10 | export type PulseValueArray = { [K in keyof T]: T[K] extends Group ? U[] : T[K] extends State ? U : never }; 11 | 12 | // We use function overloads to describe specific use cases of usePulse, that have different return formats 13 | 14 | // single-argument syntax 15 | export function usePulse>(deps: X, pulseInstance?: Pulse): PulseValue; 16 | // array-argument syntax 17 | export function usePulse[]>(deps: X | [], pulseInstance?: Pulse): PulseValueArray; 18 | 19 | export function usePulse>>(deps: X | [] | State, pulseInstance?: Pulse): PulseValueArray | PulseValue { 20 | const depsArray = normalizeDeps(deps) as PulseValueArray; 21 | 22 | // Get Pulse Instance 23 | if (!pulseInstance) { 24 | const extractedPulseInstance = getPulseInstance(depsArray[0]); 25 | if (!extractedPulseInstance) { 26 | console.error('Pulse: Failed to get Pulse Instance. It is likely you provided a value that is not a valid State instance to usePulse().'); 27 | return undefined; 28 | } 29 | pulseInstance = extractedPulseInstance; 30 | } 31 | 32 | // This is a trigger state used to force the component to re-render 33 | const [_, set_] = React.useState({}); 34 | 35 | React.useEffect(function () { 36 | // Create a callback base subscription, Callback invokes re-render Trigger 37 | const subscriptionContainer = pulseInstance?.subController.subscribeWithSubsArray(() => { 38 | set_({}); 39 | }, depsArray); 40 | 41 | // Unsubscribe on Unmount 42 | return () => pulseInstance?.subController.unsubscribe(subscriptionContainer); 43 | }, []); 44 | 45 | // Return public value of state 46 | if (!Array.isArray(deps) && depsArray.length === 1) return depsArray[0].getPublicValue(); 47 | 48 | // Return public value of state 49 | return depsArray.map(dep => dep.getPublicValue()) as PulseValueArray; 50 | } 51 | -------------------------------------------------------------------------------- /packages/pulse-react/lib/useWatcher.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { State } from '@pulsejs/core'; 3 | 4 | export function useWatcher(state: State, callback: (value: T) => void) { 5 | React.useEffect(() => { 6 | const cleanup = state.watch(callback); 7 | return () => void state.removeWatcher(cleanup); 8 | }, []); 9 | } 10 | -------------------------------------------------------------------------------- /packages/pulse-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pulsejs/react", 3 | "version": "4.1.3", 4 | "author": "Jamie Pine", 5 | "license": "MIT", 6 | "description": "React integration for PulseJS", 7 | "keywords": [], 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc" 12 | }, 13 | "peerDependencies": { 14 | "@pulsejs/core": "^4.0.8", 15 | "react": "^16.13.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^16.9.49", 19 | "@types/react-test-renderer": "^16.9.3", 20 | "react-test-renderer": "^16.13.1" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/pulse-framework/pulse" 28 | }, 29 | "files": [ 30 | "dist/**/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/pulse-react/tests/react.oldtest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Pulse, { State, usePulse } from '../lib'; 3 | import * as renderer from 'react-test-renderer'; 4 | 5 | let // 6 | App: Pulse, 7 | StringState: State, 8 | BooleanState: State, // TODO: use this somewhere, anywhere! 9 | TestComponent: React.FC<{}>; 10 | 11 | const TestString = 'This is a string! Wow, so glorious!'; 12 | 13 | beforeAll(() => { 14 | App = new Pulse({ noCore: true }); 15 | }); 16 | 17 | describe('usePulse() | Typings', () => { 18 | beforeEach(() => { 19 | StringState = App.State(TestString); 20 | BooleanState = App.State(false); 21 | }); 22 | 23 | test('State typings', () => { 24 | TestComponent = () => { 25 | const [myStringDestructured] = usePulse([StringState]); 26 | const myStringSingle = usePulse(StringState); 27 | const myHookArray: [string] = usePulse([StringState]); 28 | 29 | expect(typeof myStringDestructured).toBe('string'); 30 | expect(myStringDestructured.length > 1).toBeTruthy(); 31 | 32 | expect(typeof myStringSingle).toBe('string'); 33 | expect(myStringSingle.length > 1).toBeTruthy(); 34 | 35 | expect(myHookArray).toHaveLength(1); 36 | expect(typeof myHookArray).toBe('object'); 37 | expect(typeof myHookArray[0]).toBe('string'); 38 | 39 | return ( 40 |
41 |

{myStringSingle}

42 |

{myStringDestructured}

43 |

{myHookArray[0]}

44 |
45 | ); 46 | }; 47 | 48 | const testRender = renderer.create(); 49 | let tree = testRender.toJSON(); 50 | expect(tree).toMatchSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/pulse-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/pulse-vue/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Jamie Pine 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 | -------------------------------------------------------------------------------- /packages/pulse-vue/lib/index.ts: -------------------------------------------------------------------------------- 1 | import Pulse, { Integration, State } from '@pulsejs/core'; 2 | import Vue from 'vue'; 3 | 4 | 5 | declare module 'vue/types/vue' { 6 | interface VueConstructor { 7 | mapCore: (...args: any) => any; 8 | ($core: C): C; 9 | } 10 | } 11 | 12 | let PulseIntegrationConfig; 13 | if (Vue.version.startsWith('2.')) { 14 | PulseIntegrationConfig = { 15 | name: 'vue', 16 | foreignInstance: Vue, 17 | onPulseReady: pulse => { 18 | Vue.use({ 19 | install: vue => { 20 | vue.mixin({ 21 | created: function () { 22 | pulse.subController.registerSubscription(this); 23 | this.$core = pulse.core; 24 | this.mapCore = (mapObj: (core: ReturnType) => T) => { 25 | const stateObj = mapObj(pulse.core); 26 | return pulse.subController.subscribeWithSubsObject(this, stateObj).props; 27 | }; 28 | } 29 | }); 30 | } 31 | }); 32 | } 33 | }; 34 | } else if (Vue.version.startsWith('3.')) { 35 | // for pulse 3 36 | PulseIntegrationConfig = { 37 | name: 'vue', 38 | foreignInstance: Vue, 39 | onPulseReady: pulse => { 40 | Vue.use({ 41 | install: vue => { 42 | vue.mixin({ 43 | created: function () { 44 | pulse.subController.registerSubscription(this); 45 | this.$core = pulse.core; 46 | this.mapCore = (mapObj: (core: ReturnType) => T) => { 47 | const stateObj = mapObj(pulse.core); 48 | return pulse.subController.subscribeWithSubsObject(this, stateObj).props; 49 | }; 50 | } 51 | }); 52 | } 53 | }); 54 | } 55 | }; 56 | } else { 57 | console.log('%cPulse Does Not Support Current Vue Version!', 'background: #41B883; color: white;'); 58 | } 59 | const VuePulse = new Integration(PulseIntegrationConfig); 60 | 61 | Pulse.initialIntegrations.push(VuePulse); 62 | 63 | export * from '@pulsejs/core'; 64 | export default Pulse; 65 | -------------------------------------------------------------------------------- /packages/pulse-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pulsejs/vue", 3 | "version": "4.1.3", 4 | "author": "Jamie Pine", 5 | "license": "MIT", 6 | "description": "Vue integration for PulseJS", 7 | "keywords": [], 8 | "main": "dist/index.js", 9 | "typings": "dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc" 12 | }, 13 | "files": [ 14 | "dist/**/*" 15 | ], 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/pulse-framework/pulse" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli": "^4.5.4", 25 | "vue": "^2.6.12" 26 | }, 27 | "peerDependencies": { 28 | "@pulsejs/core": "^4.0.0-beta.4", 29 | "vue": "^2.6.12" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/pulse-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "rootDir": "lib", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "./lib/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "lib": ["es5", "es6", "es2017", "dom"], 5 | "module": "CommonJS", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "strict": false, 10 | "composite": true, 11 | "noImplicitAny": false, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./packages/tsconfig.settings.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "sourceMap": true, 8 | } 9 | } 10 | --------------------------------------------------------------------------------