├── .gitattributes ├── jest.setup.js ├── .gitignore ├── images └── favicon-with-angularjs.png ├── typings ├── angular.d.ts └── globals.d.ts ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── src ├── models │ ├── __snapshots__ │ │ └── state-holder.test.ts.snap │ ├── hook-link.ts │ ├── state-holder.ts │ ├── hook-link.test.ts │ ├── hook.ts │ ├── hook.test.ts │ └── state-holder.test.ts ├── __snapshots__ │ └── angular-store.test.ts.snap ├── angularjs-store.ts └── angular-store.test.ts ├── tslint.json ├── tsconfig.json ├── .npmignore ├── gulpfile.js ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── jest.config.js ├── LICENSE ├── tasks ├── build-esm-task.js ├── index.js ├── build-umd-task.js └── build-cjs-task.js ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | require('angular'); 2 | require('angular-mocks'); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ts_cache* 2 | coverage 3 | dist 4 | node_modules 5 | yarn-error.log* 6 | -------------------------------------------------------------------------------- /images/favicon-with-angularjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranndev/angularjs-store/HEAD/images/favicon-with-angularjs.png -------------------------------------------------------------------------------- /typings/angular.d.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | 3 | declare global { 4 | const angular: typeof angular; 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "endOfLine": "lf", 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "ms-vscode.vscode-typescript-tslint-plugin"], 3 | "unwantedRecommendations": ["eg2.tslint"] 4 | } 5 | -------------------------------------------------------------------------------- /src/models/__snapshots__/state-holder.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`StateHolder should match the initial state to snapshot 1`] = ` 4 | Object { 5 | "get": [Function], 6 | "set": [Function], 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constant variable that determines if the code is currently running on development environment. 3 | * Any code inside `if (__DEV__) { ... }` block will be eliminited in production. 4 | */ 5 | declare var __DEV__: boolean; 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "rules": { 5 | "interface-name": [true, "never-prefix"], 6 | "unified-signatures": [false], 7 | "no-console": [true, "log", "error"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__snapshots__/angular-store.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NgStore should match the initial state to snapshot 1`] = ` 4 | NgStore { 5 | "$$hooks": Array [], 6 | "$$stateHolder": Object { 7 | "get": [Function], 8 | "set": [Function], 9 | }, 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "noImplicitAny": true, 5 | "removeComments": true, 6 | "sourceMap": true, 7 | "strictNullChecks": true, 8 | "target": "es3", 9 | "lib": ["es2017", "dom"] 10 | }, 11 | "include": ["./src", "./typings"] 12 | } 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | .ts_cache* 4 | coverage 5 | images 6 | node_modules 7 | src 8 | tasks 9 | typings 10 | .gitattributes 11 | .gitignore 12 | .prettierrc 13 | CODE_OF_CONDUCT.md 14 | CONTRIBUTING.md 15 | gulpfile.js 16 | jest.config.js 17 | jest.setup.js 18 | tsconfig.json 19 | tslint.json 20 | yarn-error.log 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('path'); 4 | const registerTasks = require('./tasks'); 5 | 6 | /** @typedef {typeof config} Config */ 7 | 8 | const config = { 9 | name: 'angularjs-store', 10 | src: path.resolve(__dirname, 'src'), 11 | dist: path.resolve(__dirname, 'dist'), 12 | watchDirs: ['./src/**/*.ts', './typings/**/*.ts'], 13 | getCommonRollupOptions: () => /** @type {import('rollup').RollupOptions} */ ({ 14 | input: './src/angularjs-store.ts', 15 | external: ['angular'], 16 | }), 17 | }; 18 | 19 | registerTasks(config); 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: setup node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - name: npm ci, build, and test 19 | run: | 20 | npm ci 21 | npm run lint 22 | npm run build 23 | npm run test 24 | npx codecov --file=./coverage/coverage-final.json --disable=gcov 25 | env: 26 | CI: true 27 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 28 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | collectCoverage: true, 6 | collectCoverageFrom: ['/src/**/*.ts'], 7 | coverageDirectory: 'coverage', 8 | setupFilesAfterEnv: ['/jest.setup.js'], 9 | testMatch: ['/src/**/*.test.ts'], 10 | testPathIgnorePatterns: [ 11 | '/.github', 12 | '/.vscode', 13 | '/coverage', 14 | '/dist', 15 | '/images', 16 | '/node_modules', 17 | ], 18 | transform: { 19 | '^.+\\.ts$': 'ts-jest', 20 | }, 21 | globals: { 22 | __DEV__: true, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/models/hook-link.ts: -------------------------------------------------------------------------------- 1 | import { IScope } from 'angular'; 2 | 3 | export default class HookLink { 4 | private $$destroyer: () => void; 5 | 6 | /** 7 | * Create a HookLink. 8 | * @param destroyer Destroyer function. 9 | */ 10 | constructor(destroyer: () => void) { 11 | this.$$destroyer = destroyer; 12 | } 13 | 14 | /** 15 | * Invoke the destroyer function. 16 | */ 17 | public destroy() { 18 | this.$$destroyer(); 19 | } 20 | 21 | /** 22 | * Bind hook to scope. Automatically destroy the hook link when scope destroyed. 23 | * @param scope The scope where to bound the HookLink. 24 | */ 25 | public destroyOn(scope: IScope) { 26 | scope.$on('$destroy', () => { 27 | this.$$destroyer(); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/models/state-holder.ts: -------------------------------------------------------------------------------- 1 | export interface StateHolder { 2 | /** 3 | * Get a new copy of state. 4 | */ 5 | get(): State; 6 | 7 | /** 8 | * Update the current state. 9 | * @param partialState New partial state. 10 | */ 11 | set(partialState: Partial): void; 12 | } 13 | 14 | export default function holdState(state: State): StateHolder { 15 | const $$state = angular.copy(state); 16 | 17 | const get = () => { 18 | return angular.copy($$state); 19 | }; 20 | 21 | const set = (partialState: Partial) => { 22 | for (const key in partialState) { 23 | if (partialState.hasOwnProperty(key) && key in $$state) { 24 | $$state[key] = angular.copy(partialState[key])!; 25 | } 26 | } 27 | }; 28 | 29 | return { get, set }; 30 | } 31 | -------------------------------------------------------------------------------- /src/models/hook-link.test.ts: -------------------------------------------------------------------------------- 1 | import { IRootScopeService, IScope } from 'angular'; 2 | import HookLink from './hook-link'; 3 | 4 | let hookLink: HookLink; 5 | let destroyer: () => void; 6 | let scope: IScope; 7 | 8 | beforeEach(() => { 9 | destroyer = jest.fn(); 10 | hookLink = new HookLink(destroyer); 11 | }); 12 | 13 | beforeEach(inject(($rootScope: IRootScopeService) => { 14 | scope = $rootScope.$new(); 15 | })); 16 | 17 | describe('HookLink', () => { 18 | describe('destroy', () => { 19 | it('should call the destroyer function', () => { 20 | hookLink.destroy(); 21 | expect(destroyer).toBeCalledTimes(1); 22 | }); 23 | }); 24 | 25 | describe('destroyOn', () => { 26 | it('should automatically call the destroyer function when bounded scope destroyed', () => { 27 | hookLink.destroyOn(scope); 28 | scope.$destroy(); 29 | expect(destroyer).toHaveBeenCalledTimes(1); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.rulers": [120], 4 | 5 | "[jsonc]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[json]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[javascript]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | 18 | "files.exclude": { 19 | "**/.git": true, 20 | "**/.svn": true, 21 | "**/.hg": true, 22 | "**/CVS": true, 23 | "**/.DS_Store": true, 24 | ".ts_cache*": true, 25 | "coverage": true 26 | }, 27 | "files.watcherExclude": { 28 | "**/.git/objects/**": true, 29 | "**/.git/subtree-cache/**": true, 30 | "**/node_modules/*/**": true, 31 | ".ts_cache*": true, 32 | "coverage": true 33 | }, 34 | "search.exclude": { 35 | "**/node_modules": true, 36 | "**/bower_components": true, 37 | ".ts_cache*": true, 38 | "coverage": true, 39 | "dist": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rannie Peralta 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 | -------------------------------------------------------------------------------- /src/models/hook.ts: -------------------------------------------------------------------------------- 1 | export type HookMatcher = (action: string) => boolean; 2 | 3 | export type HookCallback = (state: Readonly, initialRun: boolean) => void; 4 | 5 | export default class Hook { 6 | private $$match: HookMatcher; 7 | private $$callback: HookCallback; 8 | private $$called = false; 9 | 10 | /** 11 | * Create a Hook. 12 | * @param matcher Function that test the dispatched action. 13 | * @param callback Callback function that trigger when action passed to matcher. 14 | */ 15 | constructor(matcher: HookMatcher, callback: HookCallback) { 16 | this.$$match = matcher; 17 | this.$$callback = callback; 18 | } 19 | 20 | /** 21 | * Run the registered callback when action passed to matcher. 22 | * @param action Action name. 23 | * @param state A state to pass in callback. 24 | * @param force Ignore the action checking and run the callback forcely. Disabled by default. 25 | */ 26 | public run(action: string, state: Readonly, force = false) { 27 | if (!force && !this.$$match(action)) { 28 | return; 29 | } 30 | 31 | this.$$callback(state, !this.$$called); 32 | this.$$called = true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tasks/build-esm-task.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { task } = require('gulp'); 4 | const rollup = require('rollup'); 5 | const typescript = require('rollup-plugin-typescript2'); 6 | const replace = require('rollup-plugin-replace'); 7 | 8 | /** 9 | * Bundle package to ESM format. 10 | * @param {import('../gulpfile').Config} config 11 | */ 12 | async function bundleESM(config) { 13 | const options = config.getCommonRollupOptions(); 14 | 15 | options.plugins = options.plugins || []; 16 | 17 | options.plugins.push( 18 | // @ts-ignore 19 | replace({ __DEV__: 'process.env.NODE_ENV !== "production"' }), 20 | ); 21 | 22 | options.plugins.push( 23 | // @ts-ignore 24 | typescript({ 25 | cacheRoot: '.ts_cache_esm', 26 | tsconfigOverride: { 27 | compilerOptions: { declaration: true }, 28 | }, 29 | }), 30 | ); 31 | 32 | const bundle = await rollup.rollup(options); 33 | 34 | await bundle.write({ 35 | file: `${config.dist}/esm/${config.name}.js`, 36 | format: 'esm', 37 | sourcemap: true, 38 | }); 39 | } 40 | 41 | /** 42 | * @param {import('../gulpfile').Config} config 43 | */ 44 | module.exports = function registerESMTasks(config) { 45 | /** 46 | * A task that bundles the package to ESM format. 47 | */ 48 | task('build-esm', () => bundleESM(config)); 49 | }; 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | prebuild: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: setup node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | - name: npm ci, lint, and test 18 | run: | 19 | npm ci 20 | npm run lint 21 | npm run test 22 | npx codecov --file=./coverage/coverage-final.json --disable=gcov 23 | env: 24 | CI: true 25 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 26 | 27 | publish-npm: 28 | needs: prebuild 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v1 32 | - uses: actions/setup-node@v1 33 | with: 34 | node-version: 12 35 | registry-url: https://registry.npmjs.org/ 36 | - run: npm ci 37 | - run: npm publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 40 | 41 | publish-gpr: 42 | needs: prebuild 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v1 46 | - uses: actions/setup-node@v1 47 | with: 48 | node-version: 12 49 | registry-url: https://npm.pkg.github.com/ 50 | scope: '@ranndev' 51 | - run: npm ci 52 | - run: npm publish 53 | env: 54 | NODE_AUTH_TOKEN: ${{secrets.GPR_TOKEN}} 55 | -------------------------------------------------------------------------------- /tasks/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { task, src, series, parallel, watch } = require('gulp'); 4 | const clean = require('gulp-clean'); 5 | const registerCJSTasks = require('./build-cjs-task'); 6 | const registerESMTasks = require('./build-esm-task'); 7 | const registerUMDTasks = require('./build-umd-task'); 8 | 9 | /** 10 | * Register the base tasks (`clean`, `watch`, and `build`) and all of its subtasks. 11 | * @param {import('../gulpfile').Config} config 12 | */ 13 | module.exports = function registerTasks(config) { 14 | registerCJSTasks(config); 15 | registerESMTasks(config); 16 | registerUMDTasks(config); 17 | 18 | /** 19 | * A task assigned to clean up the `dist` folder. 20 | */ 21 | task('clean', () => { 22 | return src(config.dist + '/*', { allowEmpty: true, read: false }).pipe(clean()); 23 | }); 24 | 25 | /** 26 | * A task that bundles the package into 3 different format (`cjs`, `umd`, and `esm`). The package will also build into 27 | * minified (for production) and non-minified (for development) version per format except for `esm`. 28 | */ 29 | task('build', series('clean', parallel('build-production-cjs', 'build-production-umd', 'build-esm'))); 30 | 31 | /** 32 | * A task that watches the designated paths and rebundle the package once there's a file change detected. Note that 33 | * the package is only bundled into non-minified version or only for development use. 34 | */ 35 | task( 36 | 'watch', 37 | series('clean', parallel('build-development-cjs', 'build-development-umd', 'build-esm'), function watcher() { 38 | watch(config.watchDirs, parallel('build-development-cjs', 'build-development-umd', 'build-esm')); 39 | }), 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/models/hook.test.ts: -------------------------------------------------------------------------------- 1 | import Hook, { HookCallback, HookMatcher } from './hook'; 2 | 3 | let hook: Hook; 4 | let matcher: HookMatcher; 5 | let callback: HookCallback; 6 | const state = { foo: '', bar: 1, baz: false }; 7 | 8 | beforeEach(() => { 9 | matcher = jest.fn((action: string) => action === 'TEST_ACTION'); 10 | callback = jest.fn(); 11 | hook = new Hook(matcher, callback); 12 | }); 13 | 14 | describe('Hook', () => { 15 | describe('run', () => { 16 | it('should call the callback when action passed to matcher', () => { 17 | hook.run('TEST_ACTION', state); 18 | expect(callback).toHaveBeenCalledTimes(1); 19 | }); 20 | 21 | it('should not call the callback when action doesn\'t passed in matcher', () => { 22 | hook.run('FOO_ACTION', state); 23 | hook.run('BAR_ACTION', state); 24 | hook.run('BAZ_ACTION', state); 25 | expect(callback).not.toHaveBeenCalled(); 26 | }); 27 | 28 | it('should call the callback when force option enabled even when action doesn\'t passed to matcher', () => { 29 | hook.run('', state, true); 30 | expect(callback).toHaveBeenCalledTimes(1); 31 | }); 32 | 33 | it('should not call the matcher when force option enabled', () => { 34 | hook.run('', state, true); 35 | expect(matcher).not.toHaveBeenCalled(); 36 | }); 37 | 38 | it('should call the callback with state and true on the first run', () => { 39 | hook.run('TEST_ACTION', state); 40 | expect(callback).toHaveBeenCalledWith(state, true); 41 | }); 42 | 43 | it('should call the callback with state and false on the second run and so forth', () => { 44 | hook.run('TEST_ACTION', state); 45 | for (let i = 0; i < 9; i++) { 46 | hook.run('TEST_ACTION', state); 47 | expect(callback).toHaveBeenCalledWith(state, false); 48 | } 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | First off, thank you for considering contributing to AngularJS Store. It’s people like you that make AngularJS Store such a great tool. 2 | 3 | ## How to Contribute? 4 | 5 | **Found an issue or want to submit a new feature?** 6 | 7 | 1. Make sure to [file an issue](https://github.com/ranndev/angularjs-store/issues/new) first. 8 | 2. Before [you create your pull request](https://github.com/ranndev/angularjs-store/pulls). 9 | 3. Let's discuss there what we can do. 10 | 11 | **Documentation correction** 12 | 13 | 1. [File an issue](https://github.com/ranndev/angularjs-store/issues/new) for your spotted incorrect documentation. 14 | 2. After we confirmed that it was really an error, 15 | - you either be granted to have an access to correct it on [GitBook](https://www.gitbook.com/) by your own 16 | - or other contributor that already have an access will update the documentation for you. 17 | 18 | ## Git Guidelines 19 | 20 | ### Branch Naming 21 | 22 | **Format:** 23 | 24 | ``` 25 | {story type}-{2-3 word summary}-{optional tracker id} 26 | ``` 27 | 28 | **Story Types:** 29 | 30 | - `test` - Unit/Integration Test 31 | - `bugfix` - Bug Fix 32 | - `feature` - New Feature 33 | - `docs` - Update documentation 34 | - `chore` - If none of the obove fits 35 | 36 | **Examples:** 37 | 38 | - `bugfix-unexpected-error-27` 39 | - `feature-automate-process-5` 40 | 41 | ### Commit Message Formatting 42 | 43 | **Format:** 44 | 45 | ``` 46 | {applicable emojis} {message} 47 | ``` 48 | 49 | **Emojis:** 50 | 51 | - `:hammer:` or `:wrench:` - Fixes bug 52 | - `:art:` Improves format/structure of the code 53 | - `:memo:` Updates documentation 54 | - `:green_heart:` Fixes CI build 55 | - `:white_check_mark:` Writes tests 56 | - `:lock:` Deals with security 57 | - `:zap:` Improves performance 58 | 59 | **Message Restrictions:** 60 | 61 | - Should starts with capital letter. 62 | - Use present tense ("Add feature" not "Added feature"). 63 | 64 | **Examples:** 65 | 66 | - `:hammer: Fix unhandled error` 67 | - `:white_check_mark: Add unit tests for new service` 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ranndev/angularjs-store", 3 | "version": "4.0.5", 4 | "description": "A tool to easily manage your state in AngularJS Application", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/angularjs-store.js", 7 | "types": "dist/esm/angularjs-store.d.ts", 8 | "scripts": { 9 | "prepublishOnly": "npm run build", 10 | "start": "webpack", 11 | "build": "gulp build", 12 | "watch": "gulp watch", 13 | "test:watch": "jest --watch --config=./jest.config.js", 14 | "test": "jest", 15 | "lint:fix": "tslint --fix --project tsconfig.json", 16 | "lint": "tslint --project tsconfig.json" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/ranndev/angularjs-store.git" 21 | }, 22 | "keywords": [ 23 | "angularjs", 24 | "store", 25 | "state", 26 | "predictable", 27 | "manager", 28 | "management", 29 | "observable", 30 | "emitter", 31 | "flux", 32 | "redux", 33 | "reactive" 34 | ], 35 | "author": "Rannie Peralta", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/ranndev/angularjs-store/issues" 39 | }, 40 | "homepage": "https://github.com/ranndev/angularjs-store#readme", 41 | "devDependencies": { 42 | "@types/angular-mocks": "1.7.0", 43 | "@types/jest": "^24.0.23", 44 | "angular": "^1.7.9", 45 | "angular-mocks": "1.7.9", 46 | "codecov": "^3.6.1", 47 | "gulp": "^4.0.2", 48 | "gulp-clean": "^0.4.0", 49 | "husky": "^3.1.0", 50 | "jest": "^24.9.0", 51 | "mkdirp": "^0.5.1", 52 | "prettier": "^1.19.1", 53 | "pretty-quick": "^2.0.1", 54 | "rollup": "^1.27.4", 55 | "rollup-plugin-replace": "^2.2.0", 56 | "rollup-plugin-terser": "^5.1.2", 57 | "rollup-plugin-typescript2": "^0.25.2", 58 | "ts-jest": "^24.2.0", 59 | "tslint": "^5.20.1", 60 | "tslint-config-prettier": "^1.18.0", 61 | "typescript": "^3.7.2" 62 | }, 63 | "dependencies": { 64 | "@types/angular": "^1.6.56" 65 | }, 66 | "peerDependencies": { 67 | "angular": "^1.7.8" 68 | }, 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "pretty-quick --staged" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tasks/build-umd-task.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { task, parallel } = require('gulp'); 4 | const { rollup } = require('rollup'); 5 | const typescript = require('rollup-plugin-typescript2'); 6 | const { terser } = require('rollup-plugin-terser'); 7 | 8 | /** 9 | * Bundle package to UMD format. 10 | * @param {import('../gulpfile').Config} config 11 | * @param {boolean=} minified 12 | */ 13 | async function bundleUMD(config, minified = false) { 14 | const options = config.getCommonRollupOptions(); 15 | 16 | options.plugins = options.plugins = []; 17 | 18 | if (minified) { 19 | options.plugins.push( 20 | terser({ 21 | compress: { 22 | dead_code: true, 23 | global_defs: { __DEV__: false }, 24 | }, 25 | output: { comments: false }, 26 | sourcemap: true, 27 | mangle: true, 28 | }), 29 | ); 30 | } 31 | 32 | options.plugins.push( 33 | // @ts-ignore 34 | typescript({ 35 | cacheRoot: '.ts_cache_umd' + (minified ? '_min' : ''), 36 | }), 37 | ); 38 | 39 | const bundle = await rollup(options); 40 | const fileBase = `${config.dist}/umd/${config.name}`; 41 | 42 | await bundle.write({ 43 | file: fileBase + (minified ? '.min.js' : '.js'), 44 | format: 'umd', 45 | intro: minified ? '' : 'var __DEV__ = true;', 46 | name: 'NgStore', 47 | sourcemap: true, 48 | }); 49 | } 50 | 51 | /** 52 | * @param {import('../gulpfile').Config} config 53 | */ 54 | module.exports = function registerUMDTasks(config) { 55 | /** 56 | * A task that bundles the package to a not minified UMD format. 57 | */ 58 | task('build-development-umd-inner', () => bundleUMD(config)); 59 | 60 | /** 61 | * A task that bundles the package to minified UMD format. 62 | */ 63 | task('build-production-umd-inner', () => bundleUMD(config, true)); 64 | 65 | /** 66 | * A task wrapper for `build-development-umd-inner` task. 67 | */ 68 | task('build-development-umd', parallel('build-development-umd-inner')); 69 | 70 | /** 71 | * A task wrapper for `build-development-umd-inner` and `build-production-umd-inner` tasks. 72 | */ 73 | task('build-production-umd', parallel('build-development-umd-inner', 'build-production-umd-inner')); 74 | }; 75 | -------------------------------------------------------------------------------- /src/models/state-holder.test.ts: -------------------------------------------------------------------------------- 1 | import createStateHolder, { StateHolder } from './state-holder'; 2 | 3 | describe('StateHolder', () => { 4 | const state = { foo: '', bar: 1, baz: [''] }; 5 | let stateHolder: StateHolder; 6 | 7 | beforeEach(() => { 8 | stateHolder = createStateHolder(state); 9 | }); 10 | 11 | it('should match the initial state to snapshot', () => { 12 | expect(stateHolder).toMatchSnapshot(); 13 | }); 14 | 15 | describe('get', () => { 16 | it('should always return a new copy of state', () => { 17 | const copies: Array = []; 18 | for (let i = 0; i < 9; i++) { 19 | const copy = stateHolder.get(); 20 | expect(copies).not.toContain(copy); 21 | copies.push(copy); 22 | } 23 | }); 24 | }); 25 | 26 | describe('set', () => { 27 | it('should support updating state partially', () => { 28 | const partialStates: Array> = [ 29 | { foo: 'bar' }, 30 | { foo: 'baz', bar: 100 }, 31 | { foo: 'fuz', bar: 200, baz: [] }, 32 | ]; 33 | 34 | partialStates.forEach((partialState) => { 35 | stateHolder.set(partialState); 36 | expect(stateHolder.get()).toEqual({ ...state, ...partialState }); 37 | }); 38 | }); 39 | 40 | it('should not add excess property', () => { 41 | stateHolder.set({ excessProperty: true } as Partial); 42 | expect(stateHolder.get()).not.toHaveProperty('excessProperty'); 43 | }); 44 | 45 | it('should not merge an array property', () => { 46 | stateHolder.set({ baz: ['a', 'b', 'c'] }); 47 | expect(stateHolder.get()).toEqual(expect.objectContaining({ baz: ['a', 'b', 'c'] })); 48 | 49 | stateHolder.set({ baz: ['a', 'b'] }); 50 | expect(stateHolder.get()).toEqual(expect.objectContaining({ baz: ['a', 'b'] })); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('StateHolder', () => { 56 | interface State { 57 | levelTwoData: { 58 | levelThreeData: { 59 | levelFourData: {}; 60 | }; 61 | }; 62 | } 63 | 64 | let stateHolder: StateHolder; 65 | 66 | beforeEach(() => { 67 | const levelFourData = {}; 68 | const levelThreeData = { levelFourData }; 69 | const levelTwoData = { levelThreeData }; 70 | 71 | stateHolder = createStateHolder({ levelTwoData }); 72 | }); 73 | 74 | describe('get (default state copier)', () => { 75 | it('should get a new copy of level two propery', () => { 76 | const copyOne = stateHolder.get(); 77 | const copyTwo = stateHolder.get(); 78 | expect(copyOne.levelTwoData).not.toBe(copyTwo.levelTwoData); 79 | }); 80 | 81 | it('should get a new copy of level three propery', () => { 82 | const copyOne = stateHolder.get(); 83 | const copyTwo = stateHolder.get(); 84 | expect(copyOne.levelTwoData.levelThreeData).not.toBe(copyTwo.levelTwoData.levelThreeData); 85 | }); 86 | 87 | it('should get a new copy of level three propery', () => { 88 | const copyOne = stateHolder.get(); 89 | const copyTwo = stateHolder.get(); 90 | expect(copyOne.levelTwoData.levelThreeData.levelFourData).not.toBe( 91 | copyTwo.levelTwoData.levelThreeData.levelFourData, 92 | ); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /tasks/build-cjs-task.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const fs = require('fs'); 4 | const mkdirp = require('mkdirp'); 5 | const { task, parallel } = require('gulp'); 6 | const { rollup } = require('rollup'); 7 | const typescript = require('rollup-plugin-typescript2'); 8 | const { terser } = require('rollup-plugin-terser'); 9 | 10 | /** 11 | * Bundle package to CJS format. 12 | * @param {import('../gulpfile').Config} config 13 | * @param {boolean=} minified 14 | */ 15 | async function bundleCJS(config, minified = false) { 16 | const options = config.getCommonRollupOptions(); 17 | 18 | options.plugins = options.plugins || []; 19 | 20 | if (minified) { 21 | options.plugins.push( 22 | terser({ 23 | compress: { 24 | dead_code: true, 25 | global_defs: { __DEV__: false }, 26 | }, 27 | output: { comments: false }, 28 | sourcemap: true, 29 | mangle: true, 30 | }), 31 | ); 32 | } 33 | 34 | options.plugins.push( 35 | // @ts-ignore 36 | typescript({ cacheRoot: '.ts_cache_cjs' + (minified ? '_min' : '') }), 37 | ); 38 | 39 | const bundle = await rollup(options); 40 | const fileBase = `${config.dist}/cjs/${config.name}`; 41 | 42 | await bundle.write({ 43 | file: fileBase + (minified ? '.min.js' : '.js'), 44 | format: 'cjs', 45 | intro: minified ? '' : 'var __DEV__ = true;', 46 | sourcemap: true, 47 | }); 48 | } 49 | 50 | /** 51 | * @param {import('../gulpfile').Config} config 52 | */ 53 | module.exports = function registerBuildCJSTasks(config) { 54 | /** 55 | * A task that writes the CJS index for development. 56 | */ 57 | task('build-development-cjs-index', (done) => { 58 | mkdirp.sync(`${config.dist}/cjs`); 59 | 60 | const content = `module.exports = require('./${config.name}.js')`; 61 | 62 | fs.writeFile(`${config.dist}/cjs/index.js`, content, done); 63 | }); 64 | 65 | /** 66 | * A task that writes the CJS index for production. 67 | */ 68 | task('build-production-cjs-index', (done) => { 69 | mkdirp.sync(`${config.dist}/cjs`); 70 | 71 | const content = [ 72 | `if (process.env.NODE_ENV === 'production') {`, 73 | ` module.exports = require('./${config.name}.min.js')`, 74 | '} else {', 75 | ` module.exports = require('./${config.name}.js')`, 76 | '}', 77 | ]; 78 | 79 | fs.writeFile(`${config.dist}/cjs/index.js`, content.join('\n'), done); 80 | }); 81 | 82 | /** 83 | * A task that bundles the package to a not minified CJS format. 84 | */ 85 | task('build-development-cjs-inner', () => bundleCJS(config)); 86 | 87 | /** 88 | * A task that bundles the package to minified CJS format. 89 | */ 90 | task('build-production-cjs-inner', () => bundleCJS(config, true)); 91 | 92 | /** 93 | * A task that builds the package to CJS format for development. 94 | */ 95 | task('build-development-cjs', parallel('build-development-cjs-inner', 'build-development-cjs-index')); 96 | 97 | /** 98 | * A task that builds the package to CJS format for production. 99 | */ 100 | task( 101 | 'build-production-cjs', 102 | parallel('build-development-cjs-inner', 'build-production-cjs-inner', 'build-production-cjs-index'), 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rannieperalta092796@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/angularjs-store.ts: -------------------------------------------------------------------------------- 1 | import Hook, { HookCallback, HookMatcher } from './models/hook'; 2 | import HookLink from './models/hook-link'; 3 | import holdState, { StateHolder } from './models/state-holder'; 4 | 5 | /* istanbul ignore next */ 6 | if (__DEV__) { 7 | if (!angular) { 8 | console.warn('Seems like you forgot to load angular. Make sure to load it first before the angularjs-store.'); 9 | } 10 | } 11 | 12 | export type HookActionQuery = 13 | | '*' 14 | | Actions[number] 15 | | Array 16 | | RegExp; 17 | 18 | export default class NgStore { 19 | private $$stateHolder: StateHolder; 20 | 21 | /** 22 | * All registered hooks from the store. 23 | */ 24 | private $$hooks: Array> = []; 25 | 26 | /** 27 | * Create a Store. 28 | * @param initialState Initial state value. 29 | */ 30 | constructor(initialState: State) { 31 | /* istanbul ignore next */ 32 | if (__DEV__) { 33 | if (Object.prototype.toString.call(initialState) !== '[object Object]') { 34 | console.warn( 35 | 'Initializing the store with a non-object state is not recommended.\n', 36 | "If you're trying to create a store with primitive type of state, try to wrap it with object.", 37 | ); 38 | } 39 | } 40 | 41 | this.$$stateHolder = holdState(initialState); 42 | } 43 | 44 | /** 45 | * Get a new copy of state. 46 | */ 47 | public copy(): State { 48 | return this.$$stateHolder.get(); 49 | } 50 | 51 | /** 52 | * Attach a hook to the store and get notified everytime the given query matched to dispatched action. 53 | * @param query A query for the dispatched action. 54 | * @param callback Invoke when query match to dispatched action. 55 | */ 56 | public hook(query: HookActionQuery, callback: HookCallback) { 57 | let matcher: HookMatcher; 58 | 59 | if (typeof query === 'string') { 60 | matcher = query === '*' ? () => true : (action) => action === query; 61 | } else if (Array.isArray(query)) { 62 | /* istanbul ignore next */ 63 | if (__DEV__) { 64 | const nonStringQueryItem = query.find((queryItem) => typeof queryItem !== 'string'); 65 | 66 | if (nonStringQueryItem) { 67 | console.warn( 68 | `Hook action query contains non-string value (${nonStringQueryItem}).\n`, 69 | 'Using array as query must only contains string.', 70 | ); 71 | } 72 | } 73 | 74 | matcher = (action) => query.includes(action); 75 | } else if (query instanceof RegExp) { 76 | matcher = (action) => query.test(action); 77 | } else { 78 | /* istanbul ignore next */ 79 | if (__DEV__) { 80 | throw new Error('Hook action query must be a either string, array of string, or regular expression.'); 81 | } 82 | 83 | /* istanbul ignore next */ 84 | throw new TypeError('Invalid hook query.'); 85 | } 86 | 87 | if (!angular.isFunction(callback)) { 88 | /* istanbul ignore next */ 89 | if (__DEV__) { 90 | throw new Error('Hook callback must be a function.'); 91 | } 92 | 93 | /* istanbul ignore next */ 94 | throw new TypeError('Invalid hook callback.'); 95 | } 96 | 97 | const hook = new Hook(matcher, callback); 98 | 99 | this.$$hooks.push(hook); 100 | 101 | // Initial run of hook. 102 | hook.run('', this.$$stateHolder.get(), true); 103 | 104 | return new HookLink(() => { 105 | this.$$hooks.splice(this.$$hooks.indexOf(hook), 1); 106 | }); 107 | } 108 | 109 | /** 110 | * Dispatch an action for updating state. 111 | * @param action Action name. 112 | * @param state New state of store. 113 | */ 114 | public dispatch(action: Actions[number], state: Partial): void; 115 | 116 | /** 117 | * Dispatch an action for updating state. 118 | * @param action Action name. 119 | * @param setState State setter. 120 | */ 121 | public dispatch(action: Actions[number], setState: (prevState: State) => Partial): void; 122 | 123 | /** 124 | * Implementation. 125 | */ 126 | public dispatch(action: Actions[number], state: Partial | ((prevState: State) => Partial)) { 127 | const partialState = angular.isFunction(state) ? state(this.$$stateHolder.get()) : state; 128 | 129 | /* istanbul ignore next */ 130 | if (__DEV__) { 131 | if (Object.prototype.toString.call(partialState) !== '[object Object]') { 132 | console.warn( 133 | "You're about to update the state using a non-object value.\n", 134 | 'Did you use non-object state?\n', 135 | "If yes, it's not recommended.\n", 136 | 'Primitive type state must wrap with object.', 137 | ); 138 | } 139 | } 140 | 141 | this.$$stateHolder.set(partialState); 142 | 143 | for (const hook of this.$$hooks) { 144 | hook.run(action, this.$$stateHolder.get()); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/angular-store.test.ts: -------------------------------------------------------------------------------- 1 | import NgStore, { HookActionQuery } from './angularjs-store'; 2 | import { HookCallback } from './models/hook'; 3 | import HookLink from './models/hook-link'; 4 | 5 | let store: NgStore; 6 | 7 | const state = { foo: '', bar: 1, baz: false }; 8 | const validQueries: HookActionQuery[] = ['TEST_ACTION', ['TEST_ACTION', 'SOME_ACTION'], /^TEST_ACTION$/]; 9 | 10 | describe('NgStore', () => { 11 | beforeEach(() => { 12 | store = new NgStore(state); 13 | }); 14 | 15 | it('should match the initial state to snapshot', () => { 16 | expect(store).toMatchSnapshot(); 17 | }); 18 | 19 | describe('copy', () => { 20 | it('should always return a new copy of state', () => { 21 | const copies: Array = []; 22 | for (let i = 0; i < 9; i++) { 23 | const copy = store.copy(); 24 | expect(copies).not.toContain(copy); 25 | copies.push(copy); 26 | } 27 | }); 28 | }); 29 | 30 | describe('hook', () => { 31 | it('should accept wild card (*) query', () => { 32 | expect(() => { 33 | store.hook('*', jest.fn()); 34 | }).not.toThrow(); 35 | }); 36 | 37 | it('should accept string query', () => { 38 | expect(() => { 39 | store.hook('FOO_BAR', jest.fn()); 40 | }).not.toThrow(); 41 | }); 42 | 43 | it('should accept array query', () => { 44 | expect(() => { 45 | store.hook(['FOO_BAR', 'FOO_BAZ'], jest.fn()); 46 | }).not.toThrow(); 47 | }); 48 | 49 | it('should accept regular expression query', () => { 50 | expect(() => { 51 | store.hook(/^FOO_BAR$/, jest.fn()); 52 | }).not.toThrow(); 53 | }); 54 | 55 | it('should throw on invalid type of query', () => { 56 | const invalidQueries = [0, false, null, undefined, {}, (arg: any) => arg]; 57 | invalidQueries.forEach((query: HookActionQuery) => { 58 | expect(() => { 59 | store.hook(query, jest.fn()); 60 | }).toThrow(); 61 | }); 62 | }); 63 | 64 | it('should throw when passing a non-function callback', () => { 65 | expect(() => { 66 | store.hook('', (null as unknown) as () => void); 67 | }).toThrow(); 68 | }); 69 | 70 | it('should run the callback once after register the hook', () => { 71 | validQueries.forEach((query) => { 72 | const callback = jest.fn(); 73 | store.hook(query, callback); 74 | expect(callback).toHaveBeenCalledTimes(1); 75 | }); 76 | }); 77 | 78 | it('should return a HookLink instance', () => { 79 | validQueries.forEach((query) => { 80 | const callback = jest.fn(); 81 | expect(store.hook(query, callback)).toBeInstanceOf(HookLink); 82 | }); 83 | }); 84 | 85 | it('shoud destroy by using the returned hook link', () => { 86 | const callbacks: Array> = []; 87 | const hookLinks: HookLink[] = []; 88 | 89 | validQueries.forEach((query) => { 90 | const callback = jest.fn(); 91 | const hookLink = store.hook(query, callback); 92 | 93 | callbacks.push(callback); 94 | hookLinks.push(hookLink); 95 | }); 96 | 97 | store.dispatch('TEST_ACTION', state); 98 | 99 | callbacks.forEach((callback) => { 100 | expect(callback).toHaveBeenCalledTimes(2); 101 | }); 102 | 103 | hookLinks.forEach((hookLink) => { 104 | hookLink.destroy(); 105 | }); 106 | 107 | store.dispatch('TEST_ACTION', state); 108 | 109 | callbacks.forEach((callback) => { 110 | expect(callback).toHaveBeenCalledTimes(2); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('dispatch', () => { 116 | it('should update the state', () => { 117 | store.dispatch('', { foo: 'bar' }); 118 | store.dispatch('', () => ({ bar: 2 })); 119 | store.dispatch('', { baz: true }); 120 | expect(store.copy()).toEqual({ foo: 'bar', bar: 2, baz: true }); 121 | }); 122 | 123 | it('should notify the hook when action match the query', () => { 124 | const hookCallback = jest.fn(); 125 | store.hook('TEST_ACTION', hookCallback); 126 | store.dispatch('TEST_ACTION', state); 127 | expect(hookCallback).toHaveBeenCalledTimes(2); 128 | }); 129 | 130 | it('should notify the hook with updated state', () => { 131 | const hookCallback = jest.fn(); 132 | store.hook('TEST_ACTION', hookCallback); 133 | store.dispatch('TEST_ACTION', { foo: 'bar' }); 134 | expect(hookCallback).toHaveBeenLastCalledWith({ ...state, foo: 'bar' }, false); 135 | }); 136 | }); 137 | }); 138 | 139 | describe('NgStore', () => { 140 | it('should call the hook callback on any dispatched action when using wild card action query', () => { 141 | const storeWithWildCard = new NgStore<{}, ['Action_A', 'Action_B', 'Action_C']>({}); 142 | const hookCallback = jest.fn(); 143 | 144 | storeWithWildCard.hook('*', hookCallback); 145 | storeWithWildCard.dispatch('Action_A', {}); 146 | storeWithWildCard.dispatch('Action_B', {}); 147 | storeWithWildCard.dispatch('Action_C', {}); 148 | 149 | // Should call four times. included the initialization call. 150 | expect(hookCallback).toHaveBeenCalledTimes(4); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularJS Store - NgStore 2 | 3 | ![AngularJS Store with AngularJS](./images/favicon-with-angularjs.png) 4 | 5 | AngularJS Store will guide you to create a one-way data flow in your application (Single Source of Truth). Manage your AngularJS application's state into a very predictable way. 6 | 7 | [![Build Status](https://github.com/ranndev/angularjs-store/workflows/build/badge.svg?branch=master)](https://github.com/ranndev/angularjs-store/actions) 8 | [![codecov](https://codecov.io/gh/ranndev/angularjs-store/branch/develop/graph/badge.svg)](https://codecov.io/gh/ranndev/angularjs-store) 9 | [![Greenkeeper badge](https://badges.greenkeeper.io/ranndev/angularjs-store.svg)](https://greenkeeper.io/) 10 | 11 | ## Installation 12 | 13 | **NPM** 14 | 15 | ``` 16 | npm install --save @ranndev/angularjs-store 17 | ``` 18 | 19 | **Yarn** 20 | 21 | ``` 22 | yarn add @ranndev/angularjs-store 23 | ``` 24 | 25 | **CDN** 26 | 27 | ```html 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | ## Quick Start 36 | 37 | This tutorial will quickly get you started for the basics of AngularJS Store. 38 | For more advanced tutorials, check out the [Tutorials with Javascript](https://angularjs-store.gitbook.io/docs/tutorials-with-javascript) or [Tutorials with Typescript](https://angularjs-store.gitbook.io/docs/tutorials-with-typescript) from the [official documentation](https://angularjs-store.gitbook.io/docs). 39 | 40 | **Creating a store** 41 | 42 | First, you need to import the `NgStore` class from `angularjs-store` or if you are using CDN, `NgStore` class is globally available. 43 | 44 | ```javascript 45 | const initialState = { count: 0 }; 46 | const counterStore = new NgStore(initialState); 47 | ``` 48 | 49 | **Making the store injectable** 50 | 51 | Wrapping the store by AngularJS service to make it injectable. 52 | 53 | ```javascript 54 | const app = angular.module('app', []); 55 | 56 | app.service('counterStore', function counterStore() { 57 | const initialState = { count: 0 }; 58 | const counterStore = new NgStore(initialState); 59 | 60 | return counterStore; 61 | }); 62 | ``` 63 | 64 | **Getting the current state** 65 | 66 | Using the `copy` method to get a copy of state. 67 | 68 | ```javascript 69 | const app = angular.module('app', []); 70 | 71 | app.controller('YourController', function YourController($scope, counterStore) { 72 | const counterState = counterStore.copy(); // { count: 0 } 73 | $scope.count = counterState.count; // 0 74 | }); 75 | ``` 76 | 77 | **Updating the state** 78 | 79 | Using the `dispatch` for updating the state. 80 | 81 | ```javascript 82 | const app = angular.module('app', []); 83 | 84 | app.controller('YourController', function YourController(counterStore) { 85 | // counterStore.copy() = { count: 0 } 86 | 87 | counterStore.dispatch('INCREMENT_COUNT', (currentState) => { 88 | return { count: currentState.count + 1 }; 89 | }); 90 | 91 | // counterStore.copy() = { count: 1 } 92 | 93 | counterStore.dispatch('DECREMENT_COUNT', (currentState) => { 94 | return { count: currentState.count - 1 }; 95 | }); 96 | 97 | // counterStore.copy() = { count: 0 } 98 | }); 99 | ``` 100 | 101 | **Listening on state changes** 102 | 103 | Using the `hook` method to listen on dispatched actions. 104 | 105 | ```javascript 106 | const app = angular.module('app', []); 107 | 108 | app.controller('YourController', function YourController($scope, counterStore) { 109 | counterStore.hook('INCREMENT_COUNT', (counterState) => { 110 | $scope.count = counterState.count; 111 | }); 112 | 113 | counterStore.hook('DECREMENT_COUNT', (counterState) => { 114 | $scope.count = counterState.count; 115 | }); 116 | }); 117 | ``` 118 | 119 | **Stop listening on dispatched actions** 120 | 121 | ```javascript 122 | const app = angular.module('app', []); 123 | 124 | app.controller('YourController', function YourController($scope, counterStore) { 125 | const hookLink = counterStore.hook('INCREMENT_COUNT', (state) => { 126 | $scope.count = state.count; 127 | 128 | // Destory the HookLink when count reaches 10. 129 | // After the HookLink gets destroyed, the hook will no longer receive any dispatched actions. 130 | if ($scope.count === 10) { 131 | hookLink.destroy(); 132 | } 133 | }); 134 | }); 135 | ``` 136 | 137 | ## Documentation 138 | 139 | - Official Documentation - https://angularjs-store.gitbook.io/docs 140 | 141 | ## Demo 142 | 143 | - Sample App - https://angularjs-store-demo.netlify.com 144 | - Source Code - https://github.com/ranndev/angularjs-store-demo 145 | 146 | ## Contributing 147 | 148 | AngularJS Store is an open source project and we love to receive contributions from our community — you! There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests. See the [guidelines](CONTRIBUTING). 149 | 150 | ## Collaborators 151 | 152 | - [Rannie Peralta](https://github.com/ranndev) 153 | 154 | ## License 155 | 156 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 157 | --------------------------------------------------------------------------------