├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── bower.json ├── dist.config.js ├── dist.min.config.js ├── dist ├── alt-with-runtime.js ├── alt.js └── alt.min.js ├── docs ├── actions.md ├── altInstances.md ├── async.md ├── bootstrap.md ├── components │ └── altContainer.md ├── createActions.md ├── createStore.md ├── errors.md ├── flush.md ├── index.md ├── lifecycleListeners.md ├── prepare.md ├── recycle.md ├── rollback.md ├── serialization.md ├── stores.md ├── takeSnapshot.md ├── testing │ ├── actions.md │ └── stores.md ├── typescript.md └── utils │ ├── hotReload.md │ └── immutable.md ├── guides ├── es5 │ └── index.md └── getting-started │ ├── actions.md │ ├── async.md │ ├── container-components.md │ ├── index.md │ ├── store.md │ ├── view.md │ └── wait-for.md ├── package.json ├── scripts └── this-dispatch-to-return.js ├── src ├── actions │ └── index.js ├── functions.js ├── index.js ├── store │ ├── AltStore.js │ ├── StoreMixin.js │ └── index.js └── utils │ ├── AltUtils.js │ └── StateFunctions.js ├── test ├── actions-dump-test.js ├── alt-config-object.js ├── async-action-test.js ├── async-test.js ├── babel │ └── index.js ├── batching-test.js ├── before-and-after-test.js ├── bound-listeners-test.js ├── browser │ ├── index.html │ └── index.js ├── config-set-get-state-test.js ├── debug-alt-test.js ├── es3-module-pattern.js ├── failed-dispatch-test.js ├── functional-test.js ├── functions-test.js ├── helpers │ ├── SaaM.js │ ├── SampleActions.js │ └── alt.js ├── index.js ├── setting-state.js ├── store-as-a-module.js ├── store-model-test.js ├── store-transforms-test.js ├── stores-get-alt.js ├── stores-with-colliding-names.js └── value-stores-test.js ├── typings └── alt │ ├── alt-tests.ts │ └── alt.d.ts └── web ├── .gitignore ├── CNAME ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── _config.yml ├── _includes ├── footer.html ├── guidenav.html ├── guidesnav.html ├── head.html ├── header.html └── sidenav.html ├── _layouts ├── default.html ├── docs.html ├── guide.html ├── guides.html └── post.html ├── _posts └── 2015-01-22-how-to-convert-flux-to-alt.md ├── assets ├── alt.png ├── bootstrap-navbar.css ├── bootstrap-navbar.min.css ├── bootstrap-navbar.min.js ├── favicon.ico ├── lotus.min.css ├── prism.min.css ├── search.js ├── search.json └── styles.css ├── blog.html ├── feed.xml ├── index.html ├── scripts ├── createIndex.js └── search.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["airbnb", "stage-0"], 3 | "plugins": [ 4 | "add-module-exports", 5 | "transform-class-properties", 6 | ["transform-es2015-classes", { loose: true }], 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/utils/TimeTravel.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "node": true, 5 | "browser": true, 6 | "es6": true 7 | }, 8 | "parser": "babel-eslint", 9 | "ecmaFeatures": { 10 | "modules": true, 11 | "jsx": true 12 | }, 13 | "rules": { 14 | "semi": [2, "never"], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | test/browser/tests.js 6 | lib 7 | utils/* 8 | flux.js 9 | flux-build.js 10 | flux-build.min.js 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | script: npm run lint && npm run coverage 4 | after_script: cat ./coverage/lcov.info | coveralls 5 | node_js: 6 | - "stable" 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | If you think there's room for improvement or see it broken feel free to submit a patch. 4 | We'll accept a patch through any medium: GitHub pull requests, gists, emails, 5 | gitter.im snippets, etc. 6 | 7 | Your patch should adhere to these guidelines: 8 | 9 | * The [coding style](#coding-style) is similar. 10 | * All tests should pass `npm test` and coverage should remain at 100% `npm run coverage`. 11 | * No linting errors are present `npm run lint`. 12 | * The commit history is clean (no merge commits). 13 | * We thank you for your patch. 14 | 15 | ## How to get set up 16 | 17 | Fork the project and clone it to your computer. Then you'll need npm to install 18 | the project's dependencies. Just run: 19 | 20 | ```bash 21 | npm install 22 | ``` 23 | 24 | To make sure everything is ok you should run the tests: 25 | 26 | ```bash 27 | npm test 28 | ``` 29 | 30 | ## Coding Style 31 | 32 | We use [EditorConfig](http://editorconfig.org/) for basics and encourage you 33 | to install its plugin on your text editor of choice. This will get you 25% of 34 | the way there. 35 | 36 | The only hard-line rule is that the code should look uniform. We loosely follow 37 | the [Airbnb JS style guide](https://github.com/airbnb/javascript) with a few 38 | notable exceptions. 39 | 40 | * You shouldn't have to use [semicolons](https://medium.com/@goatslacker/no-you-dont-need-semicolons-148d936b9cf2). The build file adds them in anyway. 41 | * Do not rely on any ES6 shim/sham features (Map, WeakMap, Proxy, etc). 42 | * Use `//` for comments. And comment anything you feel isn't obvious. 43 | 44 | ## License 45 | 46 | All of our code and files are licensed under MIT. 47 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alt", 3 | "version": "0.18.3", 4 | "homepage": "https://github.com/goatslacker/alt", 5 | "authors": [ 6 | "Josh Perez " 7 | ], 8 | "description": "Alt is a flux implementation that is small (~4.3kb & 400 LOC), well tested, terse, insanely flexible, and forward thinking.", 9 | "main": "dist/alt.js", 10 | "devDependencies": { 11 | "babel": "^4.0.1", 12 | "coveralls": "^2.11.2", 13 | "istanbul": "^0.3.5", 14 | "mocha": "^2.1.0" 15 | }, 16 | "keywords": [ 17 | "alt", 18 | "es6", 19 | "flow", 20 | "flux", 21 | "react", 22 | "unidirectional" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests", 31 | "examples", 32 | "src", 33 | "coverage-test.js", 34 | "coverage", 35 | "TODO" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /dist.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname + '/src', 3 | entry: { 4 | 'alt': ['./index.js'], 5 | }, 6 | output: { 7 | path: __dirname + '/dist', 8 | filename: '[name].js', 9 | library: 'Alt', 10 | libraryTarget: 'umd' 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /\.js$/, 15 | loader: 'babel', 16 | exclude: /node_modules/ 17 | }] 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /dist.min.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname + '/src', 3 | entry: { 4 | 'alt': ['./index.js'], 5 | }, 6 | output: { 7 | path: __dirname + '/dist', 8 | filename: '[name].min.js', 9 | library: 'Alt', 10 | libraryTarget: 'umd' 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /\.js$/, 15 | loader: 'babel', 16 | exclude: /node_modules/ 17 | }] 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /dist/alt-with-runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../'); 2 | -------------------------------------------------------------------------------- /docs/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Actions 4 | description: The Actions returned by createActions 5 | permalink: /docs/actions/ 6 | --- 7 | 8 | # Actions 9 | 10 | Each action returned by [`alt.createActions`](createActions.md) comes with a few different properties. 11 | 12 | ## action 13 | 14 | > (...data: mixed): mixed 15 | 16 | The action itself is a reference to the function that handles the action. The actions are fire and forget like in flux. One solution to know when an action has completed is to return a promise from the action so these calls can later be aggregated. This is a convenient approach if you're attempting to use actions on the server so you can be notified when all actions have completed and it is safe to render. 17 | 18 | ```js 19 | MyActions.updateName('Zack'); 20 | ``` 21 | 22 | ## action.defer 23 | 24 | > (data: mixed): undefined 25 | 26 | This is a method that faciliates calling multiple actions in another action. Since multiple actions cannot be fired until the dispatch loop has finished this helper function waits for the dispatch loop to finish and then fires off the action. It is not recommended but it is available anyway. 27 | 28 | ```js 29 | MyActions.updateName.defer('Zack'); 30 | ``` 31 | 32 | ## action.CONSTANT 33 | 34 | A constant is automatically available at creation time. This is a unique identifier for the constant that can be used for dispatching and listening. 35 | 36 | ```js 37 | MyActions.prototype.updateName = function (name) { }; 38 | ``` 39 | 40 | will become 41 | 42 | ```js 43 | myActions.UPDATE_NAME; 44 | ``` 45 | 46 | ## action.methodName 47 | 48 | Similar to the constant. 49 | 50 | ```js 51 | MyActions.prototype.updateName = function (name) { }; 52 | ``` 53 | 54 | is 55 | 56 | ```js 57 | myActions.updateName; 58 | ``` 59 | 60 | This allows flexibility giving you choice between using the constant form or the method form. 61 | 62 | ## action.id 63 | 64 | Is the unique id given to the action, you can use this id to identify which dispatch is what. 65 | 66 | ## action.data 67 | 68 | Some meta data about the action including which action group it belongs to, the name of the action, and the id. 69 | -------------------------------------------------------------------------------- /docs/altInstances.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Alt Instances 4 | description: Generating instances of your flux universe 5 | permalink: /docs/altInstances/ 6 | --- 7 | 8 | # Creating instances of your Flux 9 | 10 | If using a singleton is scary to you, or if you believe dependency injection is the way to go then this is the feature for you. You don't have to settle for traditional flux and its singleton stores. You can create separate instances of the Alt universe and then inject these into your view via dependency injection or if you're using React, contexts. With this approach you get isomorphism for free, without having to worry about flushing your stores, since each request will generate its own instance. 11 | 12 | Using instances of alt is fairly straightforward. 13 | 14 | ```js 15 | class MyAlt extends Alt { 16 | constructor(config = {}) { 17 | super(config); 18 | 19 | this.addActions('myActions', ActionCreators); 20 | this.addStore('storeName', Store); 21 | } 22 | } 23 | 24 | const flux = new MyAlt(); 25 | ``` 26 | 27 | You can set your entire app context by wrapping your root component with `withAltContext`. 28 | 29 | As a decorator: 30 | 31 | ```js 32 | import withAltContext from 'alt/utils/withAltContext' 33 | 34 | @withAltContext(alt) 35 | export default class App extends React.Component { 36 | render() { 37 | return
{this.context.flux}
38 | } 39 | } 40 | ``` 41 | 42 | As a function: 43 | 44 | ```js 45 | import withAltContext from 'alt/utils/withAltContext' 46 | 47 | export default withAltContext(alt)(App); 48 | ``` 49 | 50 | # AltClass 51 | 52 | ## AltClass#constructor 53 | 54 | > constructor(config: object) : Alt 55 | 56 | ## AltClass#addActions 57 | 58 | > addActions(actionsName: string, actions: [ActionsClass](createActions.md)): undefined 59 | 60 | Creates the actions for this instance. 61 | 62 | ## AltClass#addStore 63 | 64 | > addStore(name: string, store: [StoreModel](createStore.md), saveStore: boolean): undefined 65 | 66 | Creates the store for this instance. 67 | 68 | ## AltClass#getActions 69 | 70 | > getActions(): [Actions](actions.md) 71 | 72 | Retrieves the created actions. This becomes useful when you need to bind the actions in the store. 73 | 74 | ```js 75 | class MyStore { 76 | constructor(alt) { 77 | this.bindActions(this.alt.getActions('myActions')); 78 | } 79 | } 80 | ``` 81 | 82 | ## AltClass#getStore 83 | 84 | > getStore(): [AltStore](stores.md) 85 | 86 | Retrieves the store instance that was created. 87 | 88 | ```js 89 | const state = alt.getStore('myStore').getState(); 90 | ``` 91 | 92 | # Integrating with React 93 | 94 | You have two options on passing your alt instance through to your React components: manually through props, or using context. 95 | 96 | We won't go over manually through props except for this one small code sample: 97 | 98 | ```js 99 | const flux = new Flux() 100 | 101 | class MyApplicationComponent extends React.Component { 102 | render() { 103 | return ( 104 |
105 | 106 |
107 | ) 108 | } 109 | } 110 | 111 | 112 | 113 | ``` 114 | 115 | Now, using context, which is a more common approach. There's a [util you can use](../src/utils/withAltContext) which will automatically create the context for you and then make it available for every immediate child of AltContainer. 116 | 117 | ```js 118 | const flux = new Flux() 119 | 120 | class MyApplicationComponent extends React.Component { 121 | render() { 122 | // MySuperCoolComponent would automatically get this.props.flux 123 | return ( 124 |
125 | 126 | 127 | 128 |
129 | ) 130 | } 131 | } 132 | 133 | const App = withAltContext(flux, MyApplicationComponent) 134 | 135 | ``` 136 | 137 | Now you're setup to do things with each instance of Alt: 138 | 139 | ```js 140 | class MySuperCoolComponent extends React.Component { 141 | componentDidMount() { 142 | this.props.flux.actions.CoolActions.goFetchData() 143 | } 144 | 145 | render() { 146 | return ( 147 |
{this.props.flux.stores.CoolStore.getMyCoolName()}
148 | ) 149 | } 150 | } 151 | ``` 152 | 153 | You can read more about [AltContainer here](components/altContainer.md). 154 | -------------------------------------------------------------------------------- /docs/async.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Handling Async 4 | description: Data Sources for async data in Alt Stores 5 | permalink: /docs/async/ 6 | --- 7 | 8 | # Data Sources 9 | 10 | A common question for newcomers to flux is "where do I put my async code?". While kicking off the request in your actions, stores, or a util class is ok, the most important thing is how you handle your request. Handling a request should only be done via actions, this ensures that it goes through the dispatcher and that the data flow remains uni-directional. 11 | 12 | Alt has this concept called data sources which set up an easy way for you to handle your CRUD operations while keeping best practices. We prefer that you tie these pieces of logic to your stores. Why? Because sometimes you need partial state data in order to compose a request. The data source enforces that you handle the asynchronous responses through actions, and gives you access to store state so you can compose your request. 13 | 14 | Important note: data sources work with Promises. Make sure you have a Promise polyfill loaded in the browser if you plan on shipping to browsers that don't natively support Promises. 15 | 16 | ### What it looks like 17 | 18 | ```js 19 | // sources/SearchSource.js 20 | 21 | const SearchSource = { 22 | performSearch: { 23 | // remotely fetch something (required) 24 | remote(state) { 25 | return axios.get(`/search/q/${state.value}`); 26 | }, 27 | 28 | // this function checks in our local cache first 29 | // if the value is present it'll use that instead (optional). 30 | local(state) { 31 | return state.results[state.value] ? state.results : null; 32 | }, 33 | 34 | // here we setup some actions to handle our response 35 | loading: SearchActions.loadingResults, // (optional) 36 | success: SearchActions.receivedResults, // (required) 37 | error: SearchActions.fetchingResultsFailed, // (required) 38 | 39 | // should fetch has precedence over the value returned by local in determining whether remote should be called 40 | // in this particular example if the value is present locally it would return but still fire off the remote request (optional) 41 | shouldFetch(state) { 42 | return true 43 | } 44 | } 45 | }; 46 | ``` 47 | 48 | You then tie this to a store using the `registerAsync` function in the constructor. 49 | 50 | ```js 51 | class SearchStore { 52 | constructor() { 53 | this.state = { value: '' }; 54 | 55 | this.registerAsync(SearchSource); 56 | } 57 | } 58 | ``` 59 | 60 | Now we'll have a few methods available for use: `SearchStore.performSearch()`, and `SearchStore.isLoading()`. We can use them in `SearchStore` by `this.getInstance().performSearch()` and `this.getInstance().isLoading()` 61 | 62 | ``` js 63 | class SearchStore { 64 | constructor() { 65 | this.state = { value: '' }; 66 | 67 | this.registerAsync(SearchSource); 68 | } 69 | onSearch() { 70 | if (!this.getInstance().isLoading()) { 71 | this.getInstance().performSearch(); 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ## API 78 | 79 | The data source is an object or a function that returns an object where the keys correspond to methods that will be available to the supplied Store. The values of the keys are an object that describes the behavior of calling that method. 80 | 81 | If you are using the no-singletons approach then you'd use the function form of data sources and use the first and only parameter `alt` that is passed to retrieve the actions to listen to them. 82 | 83 | ```js 84 | const SearchSource = (alt) => { 85 | return { 86 | performSearch: { 87 | return { 88 | loading: alt.actions.SearchActions.loadingResults 89 | }; 90 | } 91 | } 92 | }; 93 | ``` 94 | 95 | Each function must return an object. The object may optionally implement `local` and `loading` but it must implement `remote`, `success`, and `error`. 96 | 97 | ### local(state: object, ...args: any) 98 | _Optional_ 99 | 100 | This function is called first. If a value is returned then a change event will be emitted from the store. Omit this function to always fetch a value remotely; 101 | 102 | ### remote(state: object, ...args: any) 103 | ##### Required 104 | 105 | This function is called whenever we need to fetch a value remotely. `remote` is only called if `local` returns null or undefined as its value, or if `shouldFetch` returns true. 106 | 107 | Any arguments passed to your public method will be passed through to both local and remote: 108 | 109 | ```js 110 | remote(state, one, two, three) { 111 | } 112 | 113 | SearchStore.performSearch(1, 2, 3); 114 | ``` 115 | 116 | ### success 117 | ##### Required 118 | 119 | Must be an action. Called whenever a value resolves. 120 | 121 | ### error 122 | ##### Required 123 | 124 | Must be an action. Called whenever a value rejects. 125 | 126 | ### shouldFetch(state: object, ...args: any) 127 | _Optional_ 128 | 129 | This function determines whether or not remote needs to be called, despite the value returned by `local`. If `shouldFetch` returns true, it will always get the data from `remote` and if it returns false, it will always use the value from `local`. You can omit both `shouldFetch` and `local` if you wish to always fetch remotely. 130 | 131 | ### interceptResponse(response, action, args) 132 | _Optional_ 133 | 134 | This function overrides the value passed to the action. Response is the value returned from the promise in `remote` or `local` and null in the case of loading action, action is the action to be called, args are the arguments (as an array) passed to the data source method. 135 | 136 | ```js 137 | interceptResponse(data, action, args) { 138 | return 12; // always returns 12 to loading/success/failed 139 | } 140 | ``` 141 | 142 | ### loading 143 | _Optional_ 144 | 145 | Must be an action. This function will be called immediately prior to the `remote` function. This call is a synchronous and is not related to the `isLoading()` method of the store. 146 | 147 | ## Decorator 148 | 149 | There is also a decorator available for convenience for you ES7 folk out there. 150 | 151 | ```js 152 | import { datasource } from 'alt-utils/lib/decorators'; 153 | 154 | @datasource(SearchSource) 155 | class SearchStore { 156 | constructor() { 157 | this.state = { value: '' }; 158 | } 159 | } 160 | ``` 161 | -------------------------------------------------------------------------------- /docs/bootstrap.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Bootstrap 4 | description: Bootstrapping all your application state 5 | permalink: /docs/bootstrap/ 6 | --- 7 | 8 | # bootstrap 9 | 10 | > (data: string): undefined 11 | 12 | The `alt.bootstrap()` function takes in a snapshot you've saved and reloads every store's state with the data provided in that snapshot. 13 | 14 | Bootstrap is great if you're running an isomorphic app, or if you're persisting state to localstorage and then retrieving it on init later on. You can save a snapshot on the server side, send it down, and then bootstrap it back on the client. 15 | 16 | If you're bootstrapping then it is recommended you pass in a [unique Identifier to createStore](createStore.md#createstore), name of the class is good enough, to createStore so that it can be referenced later for bootstrapping. 17 | 18 | ```js 19 | alt.bootstrap(JSON.stringify({ 20 | MyStore: { 21 | key: 'value', 22 | key2: 'value2' 23 | } 24 | })); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/createActions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Creating Actions 4 | description: Create your actions which will act as dispatchers 5 | permalink: /docs/createActions/ 6 | --- 7 | 8 | # createActions 9 | 10 | > (ActionsClass: function, exportObj: ?object, ...constructorArgs): [Actions](actions.md) 11 | 12 | This is a function that takes in a class of actions and returns back an object with those actions defined. The second argument `exportObj` is optional and provides a way to export to a specific object. This is useful when you have circular dependencies you can either export to an app managed global or straight to `exports`. `constructorArgs` are passed to the `ActionClass` constructor. 13 | 14 | # generateActions 15 | 16 | > (...actions: string): [Actions](actions.md) 17 | 18 | If all of your actions are just straight through dispatches you can shorthand generate them using this function. 19 | 20 | ```js 21 | const MyActions = alt.generateActions('foo', 'bar', 'baz'); 22 | ``` 23 | 24 | Which could then be used like this: 25 | 26 | ```js 27 | MyActions.foo(); 28 | MyActions.bar(); 29 | MyActions.baz(); 30 | ``` 31 | 32 | # ActionsClass 33 | 34 | ## ActionsClass#constructor 35 | 36 | > (...args): ActionsClass 37 | 38 | The constructor of your actions receives any constructor arguments passed through the [`createActions`](#createActions) function. Inside the constructor any instance properties you define will be available as actions. 39 | 40 | ## ActionsClass#generateActions 41 | 42 | > (...actions: string): undefined 43 | 44 | This is a method that can be used in the ActionsClass constructor to set up any actions that just pass data straight-through. 45 | 46 | Actions like these: 47 | 48 | ```js 49 | ActionsClass.prototype.myAction = function (data) { 50 | return data; 51 | }; 52 | ``` 53 | 54 | can be converted to: 55 | 56 | ```js 57 | function ActionsClass { 58 | this.generateActions('myAction'); 59 | } 60 | ``` 61 | 62 | There is also a shorthand for this shorthand [available](#generateactions) on the alt instance. 63 | 64 | ## ActionsClass.prototype 65 | 66 | This is an object which contains a reference to all the other actions created in your ActionsClass. You can use this for calling multiple actions within an action: 67 | 68 | ```js 69 | ActionsClass.prototype.myActionFail = function (data) { 70 | return data; 71 | }; 72 | 73 | ActionsClass.prototype.myAction = function (data) { 74 | if (someValidationFunction(data)) { 75 | return data; // returning dispatches the action 76 | } else { 77 | this.myActionFail(); // calling an action dispatches this action 78 | // returning nothing does not dispatch the action 79 | } 80 | }; 81 | ``` 82 | 83 | ### Dispatching 84 | 85 | You can also simply return a value from an action to dispatch: 86 | 87 | ```js 88 | ActionsClass.prototype.myActionFail = function (data) { 89 | return data; 90 | }; 91 | ``` 92 | 93 | There are two exceptions to this, however: 94 | 95 | 1. Returning `undefined` (or omitting `return` altogether) will **not** dispatch the action 96 | 2. Returning a Promise will **not** dispatch the action 97 | 98 | The special treatment of Promises allows the caller of the action to track its progress, and subsequent success/failure. This is useful when rendering on the server, for instance: 99 | 100 | ```js 101 | alt.createActions({ 102 | fetchUser(id) { 103 | return function (dispatch) { 104 | dispatch(id); 105 | return http.get('/api/users/' + id) 106 | .then(this.fetchUserSuccess.bind(this)) 107 | .catch(this.fetchUserFailure.bind(this)); 108 | }; 109 | }, 110 | 111 | fetchUserSuccess: x => x, 112 | fetchUserFailure: x => x, 113 | }); 114 | 115 | alt.actions.fetchUser(123).then(() => { 116 | renderToHtml(alt); // we know user fetching has completed successfully, and it's safe to render the app 117 | }); 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Errors and How to Fix 4 | description: Errors encountered when developing a flux application with Alt 5 | permalink: /docs/errors/ 6 | --- 7 | 8 | # Typical Alt Errors 9 | 10 | * throw new ReferenceError('Store provided does not have a name') 11 | 12 | If you're trying to prepare a store for bootstrap and it doesn't have a displayName property or it is blank then this error will be thrown. A store name is required in order to bootstrap. If you encounter this error then you're probably trying to prepare a non-store, or your store was missing a name in the first place. 13 | 14 | * throw new ReferenceError('Dispatch tokens not provided') 15 | 16 | waitFor expects some dispatch tokens (returned by the dispatcher/attached to your stores) otherwise it won't know what to wait for. 17 | 18 | * throw new Error(`${handler} handler must be an action function`) 19 | 20 | This is called whenever you're trying to add a non-action to success/loading/error in your data source. An action is required. This doesn't accept callbacks. 21 | 22 | * throw new TypeError('exportPublicMethods expects a function') 23 | 24 | Called whenever exportPublicMethods encounters a non-function passed in. It's called exportPublic**Methods** not exportPublicWhateverYouWant. 25 | 26 | * throw new ReferenceError('Invalid action reference passed in') 27 | 28 | Whenever you're trying to bind an action that isn't really an action. Check where you're binding the actions, you probably passed in the wrong object. 29 | 30 | * throw new TypeError('bindAction expects a function') 31 | 32 | If you're binding an action to anything other than a function. A function is what handles the action. 33 | 34 | * throw new TypeError( 35 | `Action handler in store ${this.displayName} for ` + 36 | `${(symbol.id || symbol).toString()} was defined with ` + 37 | `two parameters. Only a single parameter is passed through the ` + 38 | `dispatcher, did you mean to pass in an Object instead?` 39 | ) 40 | 41 | Called whenever you define an action handler with an arity > 1. Dispatch only dispatches a single argument through, so this is a check to make sure you're not expecting to handle multiple args in your stores. If you need to pass in more than 1 item then put it in a data structure. 42 | 43 | * throw new ReferenceError( 44 | `You have multiple action handlers bound to an action: ` + 45 | `${action} and ${assumedEventHandler}` 46 | ) 47 | 48 | If you are mistakenly handling the same action twice within your store. Double check your store for methods with the same name or methods that could be handling the same action like for example: onFoo and foo. 49 | 50 | * throw new ReferenceError( 51 | `${methodName} defined but does not exist in ${this.displayName}` 52 | ) 53 | 54 | If you define a listener but it doesn't actually exist in your store. Add the method to your store and this will go away. 55 | 56 | * throw new ReferenceError(`${storeName} is not a valid store`) 57 | 58 | Called whenever you're trying to recycle a store that does not exist. Check the recycle call. 59 | -------------------------------------------------------------------------------- /docs/flush.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Flush 4 | description: Reset stores back to initial state 5 | permalink: /docs/flush/ 6 | --- 7 | 8 | # flush 9 | 10 | > (): string 11 | 12 | Flush takes a snapshot of the current application state and then resets all the stores back to their original initial state. This is useful if you're using alt stores as singletons and doing server side rendering. 13 | 14 | In this particular scenario you would load the data in via `bootstrap` and then use `flush` once the view markup has been created. This makes sure that your components have the proper data and then resets your stores so they are ready for the next request. This is all not effected by concurrency since bootstrap, flush, and render are all synchronous processes. 15 | 16 | ```js 17 | const applicationState = alt.flush(); 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: API Documentation 4 | description: Complete API docs for alt 5 | permalink: /docs/ 6 | --- 7 | 8 | # Api Documentation 9 | 10 | Welcome to alt API docs page. These pages contain the reference material for the latest version of alt. The pages are organized by various features that alt has. 11 | 12 | ## Alt 13 | 14 | Creating a new instance of alt. 15 | 16 | ```js 17 | const Alt = require('alt'); 18 | const alt = new Alt(); 19 | ``` 20 | 21 | ## AltClass#constructor 22 | 23 | > constructor(config: object) : Alt 24 | 25 | The alt constructor takes an optional configuration object. This is where you can configure your alt instance. 26 | 27 | ```js 28 | const alt = new Alt({ 29 | dispatcher: new MyDispatcher() 30 | }); 31 | ``` 32 | 33 | ### Config Object 34 | 35 | The following properties can be defined on the config object: 36 | 37 | #### dispatcher 38 | 39 | By default alt uses Facebook's Flux [dispatcher](https://github.com/facebook/flux/blob/master/src/Dispatcher.js), but you can provide your own dispatcher implementation for alt to use. Your dispatcher must have a similar interface to the Facebook dispatcher including, `waitFor`, `register`, and `dispatch`. 40 | 41 | One easy way to provide your own dispatcher is to extend the Facebook dispatcher. The following example shows a dispatcher that extends Facebook's dispatcher, but modifies it such that all dispatched payloads are logged to the console and `register` has a custom implementation. 42 | 43 | ```js 44 | class MyDispatcher extends Dispatcher { 45 | constructor() { 46 | super(); 47 | } 48 | 49 | dispatch(payload) { 50 | console.log(payload); 51 | super.dispatch(payload); 52 | } 53 | 54 | register(callback) { 55 | // custom register implementation 56 | } 57 | } 58 | ``` 59 | 60 | #### serialize 61 | 62 | This controls how store data is serialized in snapshots. By default alt uses `JSON.stringify`, but you can provide your own function to serialize data. 63 | 64 | #### deserialize 65 | 66 | This controls how store data is deserialized from snapshot/bootstrap data. By default alt uses `JSON.parse`, but you can provide your own function to deserialize data. 67 | 68 | #### stateTransforms 69 | 70 | This is an array of functions you can provide which will be executed every time `createStore` or `createUnsavedStore` is ran. It will iterate through the array applying each function to your store. This can be useful if you wish to perform any pre-processing or transformations to your store before it's created. 71 | 72 | ## Instances 73 | 74 | Alternatively, you can [create instances](altInstances.md) of the entire alt universe including stores. 75 | 76 | ```js 77 | class Flux extends Alt { 78 | constructor() { 79 | // add your actions and stores here 80 | } 81 | } 82 | 83 | const flux = new Flux(); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/lifecycleListeners.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Lifecycle Listeners 4 | description: Event listeners for various stages of your store's lifecycle 5 | permalink: /docs/lifecycleListeners/ 6 | --- 7 | 8 | # Lifecycle Listener Methods 9 | 10 | When bootstrapping, snapshotting, or recycling there are special methods you can assign to your store to ensure any bookkeeping that needs to be done. You would place these in your store's constructor. 11 | 12 | ## Bootstrap 13 | 14 | `bootstrap` is called after the store has been bootstrapped. Here you can add some logic to take your bootstrapped data and manipulate it. 15 | 16 | ```js 17 | class Store { 18 | constructor() { 19 | this.on('bootstrap', () => { 20 | // do something here 21 | }); 22 | } 23 | } 24 | ``` 25 | 26 | ## Snapshot 27 | 28 | `snapshot` is called before the store's state is serialized. Here you can perform any final tasks you need to before the state is saved. 29 | 30 | ```js 31 | class Store { 32 | constructor() { 33 | this.on('snapshot', () => { 34 | // do something here 35 | }); 36 | } 37 | } 38 | ``` 39 | 40 | ## Init 41 | 42 | `init` is called when the store is initialized as well as whenever a store is recycled. 43 | 44 | ```js 45 | class Store { 46 | constructor() { 47 | this.on('init', () => { 48 | // do something here 49 | }): 50 | } 51 | } 52 | ``` 53 | 54 | ## Rollback 55 | 56 | `rollback` is called whenever all the stores are rolled back. 57 | 58 | ```js 59 | class Store { 60 | constructor() { 61 | this.on('rollback', () => { 62 | // do something here 63 | }); 64 | } 65 | } 66 | ``` 67 | 68 | ## Error 69 | 70 | `error` is called whenever an error occurs in your store during a dispatch. You can use this listener to catch errors and perform any cleanup tasks. 71 | 72 | ```js 73 | class Store { 74 | constructor() { 75 | this.on('error', (err, payload, currentState) => { 76 | if (payload.action === MyActions.fire) { 77 | logError(err, payload.data); 78 | } 79 | }); 80 | 81 | this.bindListeners({ 82 | handleFire: MyActions.fire 83 | }); 84 | } 85 | 86 | handleFire() { 87 | throw new Error('Something is broken'); 88 | } 89 | } 90 | ``` 91 | 92 | ## beforeEach 93 | 94 | > (payload: object, state: object): undefined 95 | 96 | This method gets called, if defined, before the payload hits the action. You can use this method to `waitFor` other stores, save previous state, or perform any bookeeping. The state passed in to `beforeEach` is the current state pre-action. 97 | 98 | `payload` is an object which contains the keys `action` for the action name and `data` for the data that you're dispatching; `state` is the current store's state. 99 | 100 | ## afterEach 101 | 102 | > (payload: object, state: object): undefined 103 | 104 | This method gets called, if defined, after the payload hits the action and the store emits a change. You can use this method for bookeeping and as a companion to `beforeEach`. The state passed in to `afterEach` is the current state post-action. 105 | 106 | ## unlisten 107 | 108 | > (): undefined 109 | 110 | `unlisten` is called when you call unlisten on your store subscription. You can use this method to perform any teardown type tasks. 111 | -------------------------------------------------------------------------------- /docs/prepare.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Prepare 4 | description: Prepare a payload for bootstrapping 5 | permalink: /docs/prepare/ 6 | --- 7 | 8 | # prepare 9 | 10 | > (Store: AltStore, payload: mixed): string 11 | 12 | Given a store and a payload this function returns a serialized string you can use to bootstrap that particular store. 13 | 14 | ```js 15 | const data = alt.prepare(TodoStore, { 16 | todos: [{ 17 | text: 'Buy some milk' 18 | ]} 19 | }); 20 | 21 | alt.bootstrap(data); 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/recycle.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Recycle 4 | description: Reset stores back to original state 5 | permalink: /docs/recycle/ 6 | --- 7 | 8 | # recycle 9 | 10 | > (...storeNames: ?string|AltStore): undefined 11 | 12 | If you wish to reset a particular, or all, store's state back to their original initial state you would call `recycle`. Recycle takes a splat of stores you would like reset. If no argument is provided then all stores are reset. 13 | 14 | ```js 15 | // recycle just MyStore 16 | alt.recycle(MyStore); 17 | 18 | // recycle all stores 19 | alt.recycle(); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/rollback.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Rollback 4 | description: Reset stores back to previous state 5 | permalink: /docs/rollback/ 6 | --- 7 | 8 | # rollback 9 | 10 | > (): undefined 11 | 12 | If you've screwed up the state, or you just feel like rolling back you can call `alt.rollback()`. Rollback is pretty dumb in the sense that it's not automatic in case of errors, and it only rolls back to the last saved snapshot, meaning you have to save a snapshot first in order to roll back. 13 | 14 | ```js 15 | // reset state to last saved snapshot 16 | alt.rollback(); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/serialization.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Serialization 4 | description: Serialization example 5 | permalink: /docs/serialization/ 6 | --- 7 | 8 | # Serialization 9 | 10 | The [onSerialize](createStore.md#onSerialize) and [onDeserialize](createStore.md#onDeserialize) store config methods can be utilized separately or together to transform the store data being saved in a snapshot or the snapshot/bootstrap data being set to a store. Though they do not have to be used together, you can picture the parity between the two methods. You can transform the shape of your store data created in the snapshot with `onSerialize`, which might be to get your data in a format ready to be consumed by other services and use `onDeserialize` to transform this data back into the format that your store recognizes. 11 | 12 | In the example below, we will show how this technique can be used to `onSerialize` complex model data being used by the store into a simpler structure for the store's snapshot, and repopulate our store models with the simple structure from the bootstrap/snapshot data with `onDeserialize`. 13 | 14 | ## onSerialize 15 | 16 | `onSerialize` provides a hook to transform the store data (via an optional return value) to be saved to an alt snapshot. If a return value is provided than it becomes the value of the store in the snapshot. For example, if the store name was `MyStore` and `onSerialize` returned `{firstName: 'Cereal', lastName: 'Eyes'}`, the snapshot would contain the data `{...'MyStore': {'firstName': 'Cereal', 'lastName': 'Eyes'}...}`. If there is no return value, the default, [`MyStore#getState()`](stores.md#storegetstate) is used for the snapshot data. 17 | 18 | ## onDeserialize 19 | 20 | `onDeserialize` provides a hook to transform snapshot/bootstrap data into a form acceptable to the store for use within the application. The return value of this function becomes the state of the store. If there is no return value, the state of the store from the snapshot is used verbatim. For example, if the `onDeserialize` received the data `{queryParams: 'val=2&val2=23'}`, we might transform the data into `{val: 2, val2: 23}` and return it to set the store data such that `myStore.val === 2` and `myStore.val2 === 23`. onDeserialize can be useful for converting data from an external source such as a JSON API into the format the store expects. 21 | 22 | ## Example 23 | 24 | ```js 25 | // this is a model that our store uses to organize/manage data 26 | class MyModel { 27 | constructor({x, y}) { 28 | this.x = x 29 | this.y = y 30 | } 31 | 32 | get sum() { 33 | return this.x + this.y 34 | } 35 | 36 | get product() { 37 | return this.x * this.y 38 | } 39 | 40 | get data() { 41 | return { 42 | x: this.x, 43 | y: this.y, 44 | sum: this.sum, 45 | product: this.product 46 | } 47 | } 48 | } 49 | 50 | // our store 51 | class MyStore { 52 | static config = { 53 | onSerialize: (state) => { 54 | return { 55 | // provides product and sum data from the model getters in addition to x and y 56 | // this data would not be included by the default serialization (getState) 57 | myModel: state.myModel.data, 58 | // change property name in snapshot 59 | newValKeyName: state.anotherVal 60 | } 61 | }, 62 | onDeserialize: (data) => { 63 | const modifiedData = { 64 | // need to take POJO and move data into the model our store expects 65 | myModel: new MyModel({x: data.myModel.x, y: data.myModel.y}), 66 | // change the property name back to what our store expects 67 | anotherVal: data.newValKeyName 68 | } 69 | return modifiedData 70 | } 71 | } 72 | 73 | constructor() { 74 | 75 | // our model is being used by the store 76 | this.myModel = new Model({x: 2, y: 3}) 77 | // we want to change the property name of this value in the snapshot 78 | this.anotherVal = 5 79 | // we don't want to save this data in the snapshot 80 | this.semiPrivateVal = 10 81 | } 82 | 83 | static getMyModelData() { 84 | return this.getState().myModel.data 85 | } 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Stores 4 | description: The stores returned by createStore 5 | permalink: /docs/stores/ 6 | --- 7 | 8 | # Alt Stores 9 | 10 | These are the stores returned by [`alt.createStore`](createStore.md), they will not have the methods defined in your StoreModel because flux stores do not have direct setters. However, any `static` methods defined in your StoreModel will be transferred to this object. 11 | 12 | **Please note:** Static methods defined on a store model are nothing more than syntactic sugar for exporting the method as a public method of your alt instance. This means that `this` will be bound to the store instance. It is recommended to explicitly export the methods in the constructor using [`StoreModel#exportPublicMethods`](createStore.md#storemodelexportpublicmethods). 13 | 14 | ## Store#getState 15 | 16 | > (): object 17 | 18 | The bread and butter method of your store. This method is used to get your state out of the store. Once it is called it performs a shallow copy of your store's state, this is so you don't accidentally overwrite/mutate any of your store's state. The state is pulled from the [StoreModel](createStore.md)'s instance variables. 19 | 20 | ```js 21 | MyStore.getState(); 22 | ``` 23 | 24 | ## Store#listen 25 | 26 | > (handler: function): function 27 | 28 | The listen method takes a function which will be called when the store emits a change. A change event is emitted automatically whenever a dispatch completes unless you return `false` from the action handler method defined in the StoreModel. The `listen` method returns a function that you can use to unsubscribe to store updates. 29 | 30 | ```js 31 | MyStore.listen(function (state) { 32 | assert.deepEqual(MyStore.getState(), state); 33 | }); 34 | ``` 35 | 36 | ## Store#unlisten 37 | 38 | > (handler: function): undefined 39 | 40 | This can be used to unbind the store listener when you do not need it any more. 41 | 42 | ```js 43 | MyStore.unlisten(referenceToTheFunctionYouUsedForListen); 44 | ``` 45 | 46 | ## Store#emitChange 47 | 48 | > (): undefined 49 | 50 | When you manually need to emit a change event you can use this method. Useful if you're doing asynchronous operations in your store and need to emit the change event at a later time. Or if you want to emit changes from across different stores. 51 | 52 | ```js 53 | MyStore.emitChange(); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/takeSnapshot.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Snapshots 4 | description: Take a snapshot of your entire application's state 5 | permalink: /docs/takeSnapshot/ 6 | --- 7 | 8 | # takeSnapshot 9 | 10 | > (...storeNames: ?string|AltStore): string 11 | 12 | Take snapshot provides you with the entire application's state serialized to JSON, by default, but you may also pass in stores to take a snapshot of a subset of the application's state. 13 | 14 | Snapshots are a core component of alt. The idea is that at any given point in time you can `takeSnapshot` and have your entire application's state serialized for persistence, transferring, logging, or debugging. 15 | 16 | ```js 17 | var snapshot = alt.takeSnapshot(); 18 | var partialSnapshot = alt.takeSnapshot(Store1, Store3); 19 | ``` 20 | 21 | You can bootstrap a snapshot into your application using [`alt.bootstrap`](bootstrap.md) 22 | -------------------------------------------------------------------------------- /docs/testing/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Testing Actions 4 | description: How to test alt actions 5 | permalink: /docs/testing/actions/ 6 | --- 7 | 8 | # Testing Actions 9 | 10 | ## Conceptual how to 11 | 12 | The good news about testing actions is that you probably won't have to do a lot of testing! Most Flux actions just pass data directly to the dispatcher, which then gets passed to the store to be handled. 13 | 14 | However, there are instances where actions will modify the data in some way before passing it on to the dispatcher. We can test these by "spying on"/listening to the dispatcher and ensuring that `alt.dispatcher.dispatch` is called with the correct payload. 15 | 16 | An action might also have other side effects such as calling another action, in which case we might also need to "spy" on that action to ensure that it is also called correctly. 17 | 18 | Let's jump into it with an example: 19 | 20 | # Example 21 | 22 | You can also download and run this [example](https://github.com/jdlehman/alt-example-tests). 23 | 24 | ## Action 25 | ```javascript 26 | import alt from 'MyAlt'; 27 | import legalActions from 'actions/LegalActions'; 28 | 29 | class PetActions { 30 | constructor() { 31 | // these we do not need to test as we trust alt tests `generateActions` 32 | this.generateActions('buyPet', 'sellPet'); 33 | } 34 | 35 | // this action modifies the dispatched data AND calls another action 36 | buyExoticPet({pet, cost}) { 37 | var importCost = 1000, 38 | illegalAnimals = ['dragon', 'unicorn', 'cyclops'], 39 | // adds import charge to cost 40 | totalCost = importCost + cost; 41 | 42 | return (dispatch) => { 43 | dispatch({ 44 | pet, 45 | cost: totalCost 46 | }); 47 | // checks if pet is legal 48 | if(illegalAnimals.indexOf(pet) >= 0) { 49 | legalActions.illegalPet(pet); 50 | } 51 | } 52 | 53 | } 54 | } 55 | 56 | export default alt.createActions(PetActions); 57 | ``` 58 | 59 | ## Action Test 60 | 61 | ```javascript 62 | import alt from 'MyAlt'; 63 | import petActions from 'actions/PetActions'; 64 | import legalActions from 'actions/LegalActions'; 65 | // you can use any assertion lib you want 66 | import {assert} from 'chai'; 67 | // we will use [sinon](http://sinonjs.org/docs/) for spying, but you can use any similar lib 68 | import sinon from 'sinon'; 69 | 70 | describe('PetActions', () => { 71 | beforeEach(() => { 72 | // here we use sinon to create a spy on the alt.dispatcher.dispatch function 73 | this.dispatcherSpy = sinon.spy(alt.dispatcher, 'dispatch'); 74 | this.illegalSpy = sinon.spy(legalActions, 'illegalPet'); 75 | }); 76 | 77 | afterEach(() => { 78 | // clean up our sinon spy so we do not affect other tests 79 | alt.dispatcher.dispatch.restore(); 80 | legalActions.illegalPet.restore(); 81 | }); 82 | 83 | describe('#buyExoticPet', () => { 84 | it('dispatches correct data', () => { 85 | var pet = 'moose', 86 | cost = 18.20, 87 | totalCost = 1000 + cost, 88 | action = petActions.BUY_EXOTIC_PET; 89 | 90 | // fire the action 91 | petActions.buyExoticPet({pet, cost}); 92 | // use our spy to see what payload the dispatcher was called with 93 | // this lets us ensure that the expected payload (with US dollars) was fired 94 | var dispatcherArgs = this.dispatcherSpy.args[0]; 95 | var firstArg = dispatcherArgs[0]; 96 | assert.equal(firstArg.action, action); 97 | assert.deepEqual(firstArg.data, {pet, cost: totalCost}); 98 | }); 99 | 100 | it('does not fire illegal action for legal pets', () => { 101 | var pet = 'dog', 102 | cost = 18.20; 103 | 104 | // fire the action 105 | petActions.buyExoticPet({pet, cost}); 106 | // use our spy to ensure that the illegal action was NOT called 107 | assert.equal(this.illegalSpy.callCount, 0); 108 | }); 109 | 110 | it('fires illegal action for illegal pets', () => { 111 | var pet = 'unicorn', 112 | cost = 18.20; 113 | 114 | // fire the action 115 | petActions.buyExoticPet({pet, cost}); 116 | // use our spy to ensure that the illegal action was called 117 | assert(this.illegalSpy.calledOnce, 'the illegal action was not fired'); 118 | }); 119 | }); 120 | }); 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/testing/stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Testing Stores 4 | description: How to test alt stores 5 | permalink: /docs/testing/stores/ 6 | --- 7 | 8 | # Testing Stores 9 | 10 | ## Conceptual how to 11 | 12 | Since Flux stores have no direct setters testing the action handlers of a store or any of the store's internal business can be tricky. This short tutorial demonstrates how to use ES6 modules in order to export both the alt created store as well as the store's unwrapped model for testing. 13 | 14 | The primary functionality of Stores in the Flux pattern is to listen for Actions, update/manage data to be used by the view, and emit change events to let the views know that data has been changed and they need to update. Based on this, the main thing we want to do is dispatch an event that our store is listening to (via `alt.dispatcher.dispatch(payload)`) and then check if the data in the store is updated in the way we expect (via methods that return data like `store.getState()` or `store.myPublicMethod()`). 15 | 16 | This form of "blackbox testing" should cover most of your store testing needs, but what if our store methods that respond to actions do other things besides update data, or what if we have some other private helper methods that we need to test? 17 | 18 | One thing we can do is to test our class before it is wrapped by alt, the unwrapped class. We should trust the alt library to test its own internals, so we can just test the inaccessible methods in our plain class by exporting it separately from our alt wrapped store (via `alt.createStore`). To do this, we can export it as a separate file, or use a named export for our unwrapped class and the default export for our alt wrapped class (in the example below, we will use the latter for simplicity). 19 | 20 | The best way to demonstrate testing stores is with an example. 21 | 22 | ## Example 23 | 24 | You can also download and run this [example](https://github.com/jdlehman/alt-example-tests). 25 | 26 | ### Store 27 | 28 | ```javascript 29 | // stores/PetStore.js 30 | import alt from 'MyAlt'; // instance of alt 31 | import petActions from 'actions/PetActions'; 32 | 33 | // named export 34 | export class UnwrappedPetStore { 35 | constructor() { 36 | this.bindActions(petActions); // buyPet, sellPet 37 | 38 | this.pets = {hamsters: 2, dogs: 0, cats: 3}; 39 | this.storeName = "Pete's Pets"; 40 | this.revenue = 0; 41 | } 42 | 43 | onBuyPet({cost, pet}) { 44 | this.pets[pet]++; 45 | this.revenue -= this.roundMoney(cost); 46 | } 47 | 48 | onSellPet({price, pet}) { 49 | this.pets[pet]--; 50 | this.revenue += this.roundMoney(price); 51 | } 52 | 53 | // this is inaccessible from our alt wrapped store 54 | roundMoney(money) { 55 | // rounds to cents 56 | return Math.round(money * 100) / 100; 57 | } 58 | 59 | static getInventory() { 60 | return this.getState().pets; 61 | } 62 | } 63 | 64 | // default export 65 | export default alt.createStore(UnwrappedPetStore, 'PetStore'); 66 | ``` 67 | 68 | ### Related Actions 69 | 70 | ```javascript 71 | // actions/PetActions.js 72 | import alt from 'MyAlt'; 73 | 74 | class PetActions { 75 | constructor() { 76 | this.generateActions('buyPet', 'sellPet'); 77 | } 78 | } 79 | 80 | export default alt.createActions(PetActions); 81 | ``` 82 | 83 | ### Store test 84 | 85 | ```javascript 86 | // tests/stores/PetStore_test.js 87 | import alt from 'MyAlt'; 88 | // wrappedPetStore is alt store, UnwrappedPetStore is UnwrappedPetStore class 89 | import wrappedPetStore, {UnwrappedPetStore} from 'stores/PetStore'; 90 | import petActions from 'actions/PetActions'; 91 | // you can use any assertion library you want 92 | import {assert} from 'chai'; 93 | 94 | // These testing utils will auto stub the stuff that alt.createStore does 95 | import AltTestingUtils from 'alt/utils/AltTestingUtils'; 96 | 97 | describe('PetStore', () => { 98 | it('listens for buy a pet action', () => { 99 | // get initial state of store 100 | var oldRevenue = wrappedPetStore.getState().revenue, 101 | oldDogs = wrappedPetStore.getInventory().dogs; 102 | 103 | // create action to be dispatched 104 | var data = {cost: 10.223, pet: 'dogs'}, 105 | action = petActions.BUY_PET; 106 | 107 | // dispatch action (store is listening for action) 108 | // NOTE: FB's dispatcher expects keys "action" and "data" 109 | alt.dispatcher.dispatch({action, data}); 110 | 111 | // assertions 112 | assert.equal(wrappedPetStore.getState().revenue, oldRevenue - 10.22); 113 | assert.equal(wrappedPetStore.getInventory().dogs, oldDogs + 1); 114 | }); 115 | 116 | it('listens for sell a pet action', () => { 117 | // get initial state of store 118 | var oldRevenue = wrappedPetStore.getState().revenue, 119 | oldDogs = wrappedPetStore.getInventory().dogs; 120 | 121 | // create action to be dispatched 122 | var data = {price: 40.125, pet: 'dogs'}, 123 | action = petActions.SELL_PET; 124 | 125 | // dispatch action (store is listening for action) 126 | // NOTE: FB's dispatcher expects keys "action" and "data" 127 | alt.dispatcher.dispatch({action, data}); 128 | 129 | // assertions 130 | assert.equal(wrappedPetStore.getState().revenue, oldRevenue + 40.13); 131 | assert.equal(wrappedPetStore.getInventory().dogs, oldDogs - 1); 132 | }); 133 | 134 | // though we can see that this method is working from our tests above, 135 | // lets use this inaccessible method to show how we can test 136 | // non static methods if we desire/need to 137 | it('rounds money to 2 decimal places', () => { 138 | var unwrappedStore = AltTestingUtils.makeStoreTestable(alt, UnwrappedPetStore); 139 | assert.equal(unwrappedStore.roundMoney(21.221234), 21.22); 140 | assert.equal(unwrappedStore.roundMoney(11.2561341), 11.26) 141 | }); 142 | }); 143 | ``` 144 | 145 | If you're using jest to test it is advised you unmock your alt instance as well as alt itself. 146 | 147 | You can set this up in your `package.json` like so: 148 | 149 | ```js 150 | "jest": { 151 | "unmockedModulePathPatterns": [ 152 | "node_modules/alt", 153 | "alt.js" 154 | ] 155 | } 156 | ``` 157 | 158 | You can also test the dispatcher by overwriting `alt.dispatcher`. Here is an example: 159 | 160 | ```js 161 | beforeEach(function() { 162 | alt = require('../../alt'); 163 | alt.dispatcher = { register: jest.genMockFunction() }; 164 | UnreadThreadStore = require('../UnreadThreadStore'); 165 | callback = alt.dispatcher.register.mock.calls[0][0]; 166 | }); 167 | ``` 168 | 169 | You can see a working jest test [here](https://github.com/goatslacker/alt/blob/master/examples/chat/js/stores/__tests__/UnreadThreadStore-test.js) which tests the [UnreadThreadStore](https://github.com/goatslacker/alt/blob/master/examples/chat/js/stores/UnreadThreadStore.js) from the [flux chat example](https://github.com/goatslacker/alt/tree/master/examples/chat) application. 170 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: TypeScriptSupport 4 | description: Use Alt from Typescript 5 | permalink: /docs/typescript/ 6 | --- 7 | 8 | Defintions are stored in the /typings directory of Alt. To run these you will need to reference the alt.d.ts file from the same directory where you manage your type definitions from [TSD](http://definitelytyped.org/tsd/): 9 | `` 10 | 11 | The alt definitions depend on react and es6-promise which should be installed with tsd. Once installed reference the Alt library from the Typescript legacy import statement: 12 | ```javascript 13 | import Alt = require("alt"); 14 | ``` 15 | Currently Typescript 1.5 is having some [issues](https://github.com/Microsoft/TypeScript/issues/3218) with exporting a top level default class from a module. You can also import the chromeDebug function or AltContainer in a similar manner: 16 | ```javascript 17 | import chromeDebug = "alt/utils/chromeDebug"; 18 | import AltContainer = "alt/AltContainer"; 19 | ``` 20 | 21 | Using alt from Typescript is nearly the same as vanilla ES6 Javascript other than the need to pass generics through Store and Action Creators and the need to inherit stores and actions from an abstract class because accessing "ghost methods" does not play well with a type system: 22 | 23 | example: 24 | ```javascript 25 | class SomeCoolActions { 26 | constructor() { 27 | this.generateActions( 28 | "firstAction", 29 | "secondAction", 30 | "thirdAction", 31 | "fourthAction 32 | ); 33 | } 34 | } 35 | 36 | ``` 37 | 38 | The above example would not compile as the method `SomeCoolActions#generateActions` does not yet exist. Generally we would extend 39 | an empty ActionsClass that contains declarations for ghost methods but no implementation: 40 | ```javascript 41 | 42 | /**AltJS is a "ghost module" full of non-instantiable interfaces and typedefs to help you write easier code 43 | * included in our alt.d.ts type definitions 44 | */ 45 | 46 | class AbstractActions implements AltJS.ActionsClass { 47 | constructor( alt:AltJS.Alt){} 48 | actions:any; 49 | dispatch: ( ...payload:Array) => void; 50 | generateActions:( ...actions:Array) => void; 51 | } 52 | 53 | 54 | //This class will now compile!! 55 | class SomeCoolActions extends AbstractActions { 56 | constructor() { 57 | this.generateActions( 58 | "firstAction", 59 | "secondAction", 60 | "thirdAction", 61 | "fourthAction 62 | ); 63 | } 64 | } 65 | ``` 66 | 67 | Alt from typescript needs a little extra boilerplate to pass actions type definitions down to the generated actions. 68 | 69 | 70 | ```javascript 71 | 72 | interface Actions { 73 | firstAction(name:string):void; 74 | secondAction(num:number):void; 75 | thirdAction( ...args:Array):void; 76 | fourthAction(waves:Array):void; 77 | } 78 | class SomeCoolActions extends AbstractActions { 79 | constructor(config) { 80 | this.generateActions( 81 | "firstAction", 82 | "secondAction", 83 | "thirdAction", 84 | "fourthAction 85 | ); 86 | super(config); 87 | } 88 | } 89 | 90 | //passing the interface as a generic enables us to see the methods on the generated class. 91 | let someCoolActions = alt.createActions(SomeCoolActions); 92 | 93 | //This works! Would throw a compile error without the generic 94 | someCoolActions.firstAction("Mike"); 95 | ``` 96 | 97 | Stores are handled in much the same way: 98 | ```javascript 99 | // create an interface to model the "state" in this store 100 | 101 | interface State { 102 | developers:Array; 103 | isBusy:boolean; 104 | } 105 | 106 | //abstract class with no implementation - moving to be included in Alt 0.17 release 107 | class AbstractStoreModel implements AltJS.StoreModel { 108 | bindActions:( ...actions:Array) => void; 109 | bindAction:( ...args:Array) => void; 110 | bindListeners:(obj:any)=> void; 111 | exportPublicMethods:(config:{[key:string]:(...args:Array) => any}) => any; 112 | exportAsync:( source:any) => void; 113 | waitFor:any; 114 | exportConfig:any; 115 | getState:() => S; 116 | } 117 | 118 | /** class both extends a model using our state interface and implements the interface it's self 119 | * this way we ensure that state based methods only deal with our declared state model and that 120 | * we remember to declare our state in the store model its self 121 | */ 122 | class DeveloperStore extends AbstractStoreModel implements State { 123 | developers = ["Mike", "Jordan", "Jose"]; 124 | isBusy = false; 125 | 126 | constructor() { 127 | super(); 128 | /** binding to automatically attach methods to 129 | * Actions for example the action developerActions#addDeveloper 130 | * would automatically call and pass its args to the onAddDeveloperMethod 131 | * when a dispatch is called 132 | * Alt stores a automatically "emit-change" and inform listening ui components of the update after 133 | * State is set 134 | **/ 135 | this.bindActions(developerActions); 136 | 137 | /** different than the above - this takes a single action from an "actions" instance and binds it to whatever method 138 | * you specify - really useful when a store has an actions class bound but maybe needs one or two actions from another 139 | * as well 140 | */ 141 | this.bindAction(repoActions.working, this.setWorking); 142 | } 143 | 144 | onAddDeveloper(dev:string) { 145 | ///details 146 | } 147 | 148 | setWorking() { 149 | ////do stuff 150 | } 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /docs/utils/hotReload.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Webpack Hot Module Replacement 4 | description: Hot reload on stores for use with webpack 5 | permalink: /docs/hot-reload/ 6 | --- 7 | 8 | # HMR (hot module replacement) 9 | 10 | When working with webpack and something like [react-hot-loader](https://github.com/gaearon/react-hot-loader) changing stores often causes your stores to be re-created (since they're wrapped) while this isn't a big deal it does cause a surplus of stores which pollutes the alt namespace and the alt debugger. With this util you're able to get proper hot replacement for stores, changing code within a store will reload it properly. 11 | 12 | To use this just export a hot store using `makeHot` rather than using `alt.createStore`. 13 | 14 | ```js 15 | import alt from '../alt'; 16 | import makeHot from 'alt/utils/makeHot'; 17 | 18 | class TodoStore { 19 | static displayName = 'TodoStore' 20 | 21 | constructor() { 22 | this.todos = {}; 23 | } 24 | } 25 | 26 | export default makeHot(alt, TodoStore); 27 | ``` 28 | -------------------------------------------------------------------------------- /guides/es5/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guides 3 | title: Alt and ES5 4 | description: Using alt with ES5 or ES3 5 | permalink: /guides/es5/ 6 | --- 7 | 8 | # Plain JavaScript 9 | 10 | While alt examples encourage ES6 and alt was built with ES6 in mind it is perfectly valid to use plain old JavaScript instead. This guide will focus on a few examples of how you can use alt without the new ES6 hotness. 11 | 12 | ## Creating Actions 13 | 14 | There are quite a few ways to create actions. If they don't process any data and just dispatch a single argument through you can generate them based off of strings. You can create them using constructors and prototypes, or you can use a plain old JS object. 15 | 16 | ```js 17 | var foodActions = alt.generateActions('addItem'); 18 | ``` 19 | 20 | which is the equivalent to 21 | 22 | ```js 23 | var foodActions = alt.createActions(function () { 24 | this.addItem = function (item) { 25 | return item; 26 | }; 27 | }); 28 | ``` 29 | 30 | or 31 | 32 | ```js 33 | var foodActions = alt.createActions({ 34 | addItem: function (item) { 35 | return item; 36 | } 37 | }); 38 | ``` 39 | 40 | ## Creating Stores 41 | 42 | You can use constructors and prototypes to create a store, or use an object. 43 | 44 | ```js 45 | function FoodStore() { 46 | this.foods = []; 47 | 48 | this.bindListeners({ 49 | addItem: foodActions.addItem 50 | }); 51 | 52 | this.exportPublicMethods({ 53 | hasFood: function() { 54 | return !!this.getState().foods.length; 55 | } 56 | }); 57 | } 58 | 59 | FoodStore.prototype.addItem = function (item) { 60 | this.foods.push(item); 61 | }; 62 | 63 | FoodStore.displayName = 'FoodStore'; 64 | 65 | var foodStore = alt.createStore(FoodStore); 66 | ``` 67 | 68 | which can also be written as an Object: 69 | 70 | ```js 71 | var FoodStore = alt.createStore({ 72 | displayName: 'FoodStore', 73 | 74 | bindListeners: { 75 | addItem: foodActions.addItem 76 | }, 77 | 78 | state: { 79 | foods: [] 80 | }, 81 | 82 | publicMethods: { 83 | hasFood: function () { 84 | return !!this.getState().foods.length; 85 | } 86 | }, 87 | 88 | addItem: function (item) { 89 | var foods = this.state.foods; 90 | foods.push(item); 91 | this.setState({ 92 | foods: foods 93 | }); 94 | } 95 | }); 96 | ``` 97 | 98 | The interesting thing about the Object pattern is that you can use the old ES3 Module pattern to create your stores: 99 | 100 | ```js 101 | function FoodStore(initialFood) { 102 | var foods = []; 103 | for (var i = 0; i < initialFood.length; i += 1) { 104 | if (initialFood !== 'banana') { 105 | foods.push(initialFood); 106 | } 107 | } 108 | 109 | return { 110 | displayName: 'FoodStore', 111 | 112 | bindListeners: { 113 | addItem: foodActions.addItem 114 | }, 115 | 116 | state: { 117 | foods: foods 118 | }, 119 | publicMethods: { 120 | hasFood: function () { 121 | return foods.length; 122 | } 123 | }, 124 | 125 | addItem: function (item) { 126 | var foods = this.state.foods; 127 | foods.push(item); 128 | this.setState({ 129 | foods: foods 130 | }); 131 | } 132 | }; 133 | } 134 | 135 | var foodStore = alt.createStore( 136 | FoodStore(['banana', 'strawberry', 'mango', 'orange']) 137 | ); 138 | ``` 139 | 140 | A less explicit way of creating a public method is to statically define it as property of the store constructor function: 141 | ``` 142 | FoodStore.hasFood = function() { 143 | return !!this.getState().length; 144 | } 145 | ``` 146 | 147 | ## Instances 148 | 149 | You can even create instances of alt if you prefer that over singletons. You would use JavaScript's prototypal inheritance to achieve this. 150 | 151 | ```js 152 | // Actions 153 | var FoodActionsObject = { 154 | addItem: function (item) { 155 | return item; 156 | } 157 | }; 158 | 159 | // Creating a store, notice how we inject the actions 160 | function FoodStore(FoodActions) { 161 | return { 162 | state: { 163 | foods: [] 164 | }, 165 | 166 | bindListeners: { 167 | addItem: FoodActions.addItem 168 | }, 169 | 170 | addItem: function (item) { 171 | var foods = this.state.foods; 172 | foods.push(item); 173 | this.setState({ 174 | foods: foods 175 | }); 176 | } 177 | }; 178 | } 179 | 180 | // The flux class 181 | function Flux() { 182 | // super() 183 | Alt.apply(this, arguments); 184 | 185 | this.addActions('FoodActions', FoodActionsObject); 186 | this.addStore('FoodStore', FoodStore(this.getActions('FoodActions'))); 187 | } 188 | 189 | // ES5 based inheritance 190 | Flux.prototype = Object.create(Alt.prototype, { 191 | constructor: { 192 | value: Flux, 193 | enumerable: false, 194 | writable: true, 195 | configurable: true 196 | } 197 | }); 198 | 199 | Flux.__proto__ = Alt 200 | 201 | // using it 202 | var flux = new Flux(); 203 | 204 | // use flux like you normally would 205 | flux.getActions('FoodActions').addItem('celery'); 206 | console.log( 207 | flux.getStore('FoodStore').getState() 208 | ); 209 | ``` 210 | -------------------------------------------------------------------------------- /guides/getting-started/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Creating Actions 4 | description: How to create actions for managing state 5 | permalink: /guide/actions/ 6 | --- 7 | 8 | # Creating Actions 9 | 10 | The first actions we create will be simple, they'll take in an array of locations we'll pass in at the start of the application and just dispatch them to the store. 11 | 12 | We create an action by creating a class, the class' prototype methods will become the actions. The class syntax is completely optional you can use regular constructors and prototypes. 13 | 14 | Whatever value you return from your action will be sent through the Dispatcher and onto the stores. Finally, make sure you export the created actions using `alt.createActions`. 15 | 16 | --- 17 | 18 | `actions/LocationActions.js` 19 | 20 | ```js 21 | var alt = require('../alt'); 22 | 23 | class LocationActions { 24 | updateLocations(locations) { 25 | return locations; 26 | } 27 | } 28 | 29 | module.exports = alt.createActions(LocationActions); 30 | ``` 31 | 32 | [Continue to next step...](store.md) 33 | -------------------------------------------------------------------------------- /guides/getting-started/async.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Fetching Data 4 | description: Asynchronous flow control and flux state 5 | permalink: /guide/async/ 6 | --- 7 | 8 | # Fetching Data 9 | 10 | This tutorial shows how to fetch data and handle a failure. 11 | 12 | Where should async go? There is no right answer. You can put it in actions or in stores. In this tutorial, we'll be calling async from the actions and the data fetching will exist in a new folder `sources`. 13 | Create `sources/LocationSource.js`. You can use something like [`fetch`](https://github.com/github/fetch) to fetch some data from a server. For the purposes of this tutorial we will be using `setTimeout` and `Promise` to mimic a request made using `fetch` API. 14 | Here's some mock data we'll be using: 15 | 16 | ```js 17 | var mockData = [ 18 | { id: 0, name: 'Abu Dhabi' }, 19 | { id: 1, name: 'Berlin' }, 20 | { id: 2, name: 'Bogota' }, 21 | { id: 3, name: 'Buenos Aires' }, 22 | { id: 4, name: 'Cairo' }, 23 | { id: 5, name: 'Chicago' }, 24 | { id: 6, name: 'Lima' }, 25 | { id: 7, name: 'London' }, 26 | { id: 8, name: 'Miami' }, 27 | { id: 9, name: 'Moscow' }, 28 | { id: 10, name: 'Mumbai' }, 29 | { id: 11, name: 'Paris' }, 30 | { id: 12, name: 'San Francisco' } 31 | ]; 32 | ``` 33 | 34 | So let's create the `LocationSource`. 35 | 36 | `sources/LocationSource.js` 37 | 38 | ```js 39 | var LocationSource = { 40 | fetch: function () { 41 | // returning a Promise because that is what fetch does. 42 | return new Promise(function (resolve, reject) { 43 | // simulate an asynchronous action where data is fetched on 44 | // a remote server somewhere. 45 | setTimeout(function () { 46 | // resolve with some mock data 47 | resolve(mockData); 48 | }, 250); 49 | }); 50 | } 51 | }; 52 | ``` 53 | 54 | Next, we'll need to change the actions to use this new method we created. We will add an action called `fetchLocations` which will fetch the locations and then call `updateLocations` when it successfully completes. A new action `locationsFailed` deals with the locations not being available. Add these methods to the class. 55 | `actions/LocationActions.js` 56 | 57 | ```js 58 | fetchLocations() { 59 | return (dispatch) => { 60 | // we dispatch an event here so we can have "loading" state. 61 | dispatch(); 62 | LocationSource.fetch() 63 | .then((locations) => { 64 | // we can access other actions within our action through `this.actions` 65 | this.updateLocations(locations); 66 | }) 67 | .catch((errorMessage) => { 68 | this.locationsFailed(errorMessage); 69 | }); 70 | } 71 | } 72 | locationsFailed(errorMessage) { 73 | return errorMessage; 74 | } 75 | ``` 76 | 77 | Next we'll update our store to handle these new actions. It's just a matter of adding the new actions and their handlers to `bindListeners`. A new state 'errorMessage' is added to deal with a potential error message. 78 | `stores/LocationStore.js` 79 | 80 | ```js 81 | class LocationStore { 82 | constructor() { 83 | this.locations = []; 84 | this.errorMessage = null; 85 | this.bindListeners({ 86 | handleUpdateLocations: LocationActions.UPDATE_LOCATIONS, 87 | handleFetchLocations: LocationActions.FETCH_LOCATIONS, 88 | handleLocationsFailed: LocationActions.LOCATIONS_FAILED 89 | }); 90 | } 91 | handleUpdateLocations(locations) { 92 | this.locations = locations; 93 | this.errorMessage = null; 94 | } 95 | handleFetchLocations() { 96 | // reset the array while we're fetching new locations so React can 97 | // be smart and render a spinner for us since the data is empty. 98 | this.locations = []; 99 | } 100 | 101 | handleLocationsFailed(errorMessage) { 102 | this.errorMessage = errorMessage; 103 | } 104 | } 105 | ``` 106 | 107 | And finally, the view will change slightly. We'll be displaying an error message if it exists and showing a spinner if the content is loading. 108 | 109 | ```js 110 | componentDidMount() { 111 | LocationStore.listen(this.onChange); 112 | 113 | LocationActions.fetchLocations(); 114 | }, 115 | 116 | render() { 117 | if (this.state.errorMessage) { 118 | return ( 119 |
Something is wrong
120 | ); 121 | } 122 | 123 | if (!this.state.locations.length) { 124 | return ( 125 |
126 | 127 |
128 | ) 129 | } 130 | 131 | return ( 132 |
    133 | {this.state.locations.map((location) => { 134 | return ( 135 |
  • {location.name}
  • 136 | ); 137 | })} 138 |
139 | ); 140 | } 141 | ``` 142 | 143 | [Continue to next step...](wait-for.md) 144 | -------------------------------------------------------------------------------- /guides/getting-started/container-components.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Container Components 4 | description: Higher-order container components for handling store state 5 | permalink: /guide/container-components/ 6 | --- 7 | 8 | # Container Components 9 | 10 | Now that we've learned the basics on how to [create actions](actions.md), [create stores](store.md), 11 | and then [listen to them in the view](view.md); it's time to learn some more advanced topics like container components. 12 | 13 | What are [container components](https://medium.com/@learnreact/container-components-c0e67432e005)? 14 | 15 | They are components that are responsible for managing your state. Remember how in the view we would mix state and rendering? 16 | 17 | ```js 18 | var Locations = React.createClass({ 19 | getInitialState() { 20 | return LocationStore.getState(); 21 | }, 22 | 23 | componentDidMount() { 24 | LocationStore.listen(this.onChange); 25 | }, 26 | 27 | onChange(state) { 28 | this.setState(state); 29 | }, 30 | 31 | render() { 32 | return ( 33 |
    34 | {this.state.locations.map((location) => { 35 | return ( 36 |
  • {location.name}
  • 37 | ); 38 | })} 39 |
40 | ); 41 | } 42 | }); 43 | ``` 44 | 45 | This is actually a bad pattern to follow because it makes it more difficult to re-use view specific code. Your components should be split up into two types: those that manage state (stateful components) and those that just deal with the display of data (pure components). Some people call these [smart/dumb components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0). 46 | 47 | The goal of pure components is to write them so they only accept props and are responsible for rendering those props into a view. This makes it easier to test and re-use those components. A simple example is a UserComponent, you can couple this with a UserStore, but then later on if you want to re-use UserComponent with your AdminStore you're going to have a bad time. 48 | 49 | A pure Locations component would look like: 50 | 51 | ```js 52 | var Locations = React.createClass({ 53 | render() { 54 | return ( 55 |
    56 | {this.props.locations.map((location, i) => { 57 | return ( 58 |
  • 59 | {location.name} 60 |
  • 61 | ); 62 | })} 63 |
64 | ); 65 | } 66 | }); 67 | ``` 68 | 69 | Seems easy enough, all we really did was change from `this.state` to `this.props` and removed all the store listening code. So wait, how do you listen to stores then? Enter the wrapping container component. 70 | 71 | There are two ways to create a container component in Alt. Through a declarative approach and through a function, we can check out both. 72 | 73 | ### connectToStores 74 | 75 | This is a util function that will connect a component to a store. 76 | 77 | ```js 78 | var LocationsContainer = connectToStores({ 79 | getStores() { 80 | // this will handle the listening/unlistening for you 81 | return [LocationStore] 82 | }, 83 | 84 | getPropsFromStores() { 85 | // this is the data that gets passed down as props 86 | // each key in the object returned by this function is added to the `this.props` 87 | var locationState = LocationStore.getState() 88 | return { 89 | locations: locationState.locations 90 | } 91 | } 92 | }, React.createClass({ 93 | render() { 94 | return 95 | } 96 | })) 97 | ``` 98 | 99 | As you can see, locations can now be re-used in other places since it's not tied to a particular store. All locations will do is accept an array of locations and concern itself with rendering them. This may seem like extra boilerplate but you'll be grateful for it later on when coming back to the app to maintain it. As a bonus, `connectToStores` handles the listening and unlistening of your stores. Every time a store changes, `getPropsFromStores` is called and the result of it is passed down as props to the connected component. 100 | 101 | Please note that as mentioned above, `connectToStores` is actually wrapping your React component with a container component that handles the listening to stores and passing that state down as props to your component. If you are using `connectToStores` as a decorator you will need to make sure it is the outermost decorator to ensure that your other decorators are applied to your actual component rather than the wrapped component. For example: 102 | 103 | ```js 104 | @connectToStores 105 | @someDecorator 106 | @anotherDecorator 107 | class MyComponent extends React.Component { 108 | // ... 109 | } 110 | 111 | // equivalent to connectToStores(someDecorator(anotherDecorator(MyComponent))) 112 | ``` 113 | 114 | ### AltContainer 115 | 116 | This is a component you can use to declaratively connect a store, or pass down actions, or context to the pure components. 117 | 118 | ```js 119 | var LocationsContainer = React.createClass({ 120 | render() { 121 | return ( 122 | 123 | 124 | 125 | ) 126 | } 127 | })) 128 | ``` 129 | 130 | With AltContainer, hooking up a single store to a single component is fairly simple, we use the `store` prop. This will automatically listen and unlisten to your store and whenever state changes it'll re-render the child components passing the entire store's state as props to each component. 131 | -------------------------------------------------------------------------------- /guides/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Getting Started 4 | description: Learn about flux and alt 5 | permalink: /guide/ 6 | --- 7 | 8 | # Getting Started 9 | 10 | Alt is a library that facilitates the managing of state within your JavaScript applications. It is modeled after flux. 11 | 12 | ## What is flux? 13 | 14 | Flux is an application architecture for building complex user interfaces. It eschews MVC in favor of unidirectional data flow. What this means is that data enters through a single place (your actions) and then flow outward through their state manager (the store) and finally onto the view. The view can then restart the flow by calling other actions in response to user input. 15 | 16 | ## Setup 17 | 18 | For this tutorial I'll be assuming you're familiar with [React](https://facebook.github.io/react/), [CommonJS](http://www.commonjs.org/), [ES5 JavaScript](https://es5.github.io/), and a subset of [ES6](https://people.mozilla.org/~jorendorff/es6-draft.html) specifically the one that works with react's transform. I'll also assume you're on a modern browser or a node environment. Alt is not restricted to only React or CommonJS, you may download a browser build of alt [here](https://cdn.rawgit.com/goatslacker/alt/master/dist/alt.js). 19 | 20 | ## Installing 21 | 22 | If you're using a package manager like npm or bower then go ahead and install alt. 23 | 24 | ```bash 25 | npm install alt 26 | ``` 27 | 28 | ## Folder structure 29 | 30 | A typical folder structure would look like this 31 | 32 | ```txt 33 | your_project 34 | |--actions/ 35 | | |--MyActions.js 36 | |--stores/ 37 | | |--MyStore.js 38 | |--components/ 39 | | |--MyComponent.jsx 40 | |--alt.js 41 | |--app.js 42 | ``` 43 | 44 | ## Creating your first alt 45 | 46 | For this guide we'll be creating a very simple application which has a list of travel destinations and allows you to favorite which ones you're interested in. Let's get started. 47 | 48 | We'll be creating an instance of alt, this instantiates a [Flux dispatcher](http://facebook.github.io/flux/docs/dispatcher.html#content) for you and gives you methods to create your actions and stores. We'll be referring back to this file throughout this guide. 49 | 50 | In the root of your project, create a new file called `alt.js`. 51 | 52 | ```js 53 | var Alt = require('alt'); 54 | var alt = new Alt(); 55 | 56 | module.exports = alt; 57 | ``` 58 | 59 | [Continue to next step...](actions.md) 60 | -------------------------------------------------------------------------------- /guides/getting-started/store.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Creating a Store 4 | description: Store manages the state and is shared across your components 5 | permalink: /guide/store/ 6 | --- 7 | 8 | # Creating a Store 9 | 10 | The store is your data warehouse. This is the single source of truth for a particular piece of your application's state. Similar to actions, we'll be creating a class for the store. Also like the actions, the class syntax is completely optional, you can use regular constructors and prototypes. 11 | 12 | ```js 13 | class LocationStore { 14 | constructor() { 15 | } 16 | } 17 | ``` 18 | 19 | 20 | Instance variables defined anywhere in the store will become the state. This resembles how we reason about and build normal JS classes. You can initialize these in the constructor and then update them directly in the prototype methods. 21 | 22 | ```js 23 | this.locations = []; 24 | ``` 25 | 26 | Next, we define methods in the store's prototype that will deal with the actions. These are called action handlers. 27 | Stores automatically emit a change event when an action is dispatched through the store and the action handler ends. In order to suppress the change event you can return false from the action handler. 28 | 29 | ```js 30 | handleUpdateLocations(locations) { 31 | this.locations = locations; 32 | // optionally return false to suppress the store change event 33 | } 34 | ``` 35 | 36 | And then in the constructor, we bind our action handlers to our actions. 37 | 38 | ```js 39 | this.bindListeners({ 40 | handleUpdateLocations: LocationActions.UPDATE_LOCATIONS 41 | }); 42 | ``` 43 | 44 | Finally, we export our newly created store. 45 | 46 | ```js 47 | module.exports = alt.createStore(LocationStore, 'LocationStore'); 48 | ``` 49 | 50 | --- 51 | 52 | `stores/LocationStore.js` 53 | 54 | ```js 55 | var alt = require('../alt'); 56 | var LocationActions = require('../actions/LocationActions'); 57 | 58 | class LocationStore { 59 | constructor() { 60 | this.locations = []; 61 | 62 | this.bindListeners({ 63 | handleUpdateLocations: LocationActions.UPDATE_LOCATIONS 64 | }); 65 | } 66 | 67 | handleUpdateLocations(locations) { 68 | this.locations = locations; 69 | } 70 | } 71 | 72 | module.exports = alt.createStore(LocationStore, 'LocationStore'); 73 | ``` 74 | 75 | [Continue to next step...](view.md) 76 | -------------------------------------------------------------------------------- /guides/getting-started/view.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Using your View 4 | description: React components and how they fit into a flux architecture 5 | permalink: /guide/view/ 6 | --- 7 | 8 | # Using your View 9 | 10 | We won't spend too much time on all the parts of the view since it is more about React than it is Alt, however, the important piece is how you listen to stores and get data out of it. 11 | 12 | Getting the state out of your store is simple, every alt store has a method which returns its state. The state is copied over as a value when returned so you accidentally don't mutate it by reference. We can use React's `getInitialState` to set the initial state using the store's state. 13 | 14 | ```js 15 | getInitialState() { 16 | return LocationStore.getState(); 17 | }, 18 | ``` 19 | 20 | But then we'll want to listen to changes once the state in the store is updated. In your react component on `componentDidMount` you can add an event handler using `LocationStore.listen`. 21 | 22 | ```js 23 | componentDidMount() { 24 | LocationStore.listen(this.onChange); 25 | }, 26 | ``` 27 | 28 | And, don't forget to remove your event listener. 29 | 30 | ```js 31 | componentWillUnmount() { 32 | LocationStore.unlisten(this.onChange); 33 | }, 34 | ``` 35 | 36 | A few [mixins](https://github.com/altjs/mixins) or a ["higher-order-component"](https://github.com/altjs/connect-to-stores) are available to make this boilerplate go away. 37 | 38 | --- 39 | 40 | `components/Locations.jsx` 41 | 42 | ```js 43 | var React = require('react'); 44 | var LocationStore = require('../stores/LocationStore'); 45 | 46 | var Locations = React.createClass({ 47 | getInitialState() { 48 | return LocationStore.getState(); 49 | }, 50 | 51 | componentDidMount() { 52 | LocationStore.listen(this.onChange); 53 | }, 54 | 55 | componentWillUnmount() { 56 | LocationStore.unlisten(this.onChange); 57 | }, 58 | 59 | onChange(state) { 60 | this.setState(state); 61 | }, 62 | 63 | render() { 64 | return ( 65 |
    66 | {this.state.locations.map((location) => { 67 | return ( 68 |
  • {location.name}
  • 69 | ); 70 | })} 71 |
72 | ); 73 | } 74 | }); 75 | 76 | module.exports = Locations; 77 | ``` 78 | 79 | [Continue to next step...](async.md) 80 | -------------------------------------------------------------------------------- /guides/getting-started/wait-for.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: guide 3 | title: Data Dependencies 4 | description: How to deal with derived data within your stores 5 | permalink: /guide/wait-for/ 6 | --- 7 | 8 | # Data Dependencies 9 | 10 | Dealing with data dependencies between stores is often a tricky and time consuming endeavour. This is one of the reasons why flux was originally built. Flux comes with this method called `waitFor` which signals to the dispatcher that this store depends on another store for its data. 11 | 12 | Say we have a new `FavoritesStore` where you'll be able to mark your favorite locations. We want to update the LocationStore only after the FavoriteStore gets its update. 13 | 14 | First lets add a new action to our LocationActions. 15 | 16 | `actions/LocationActions.js` 17 | 18 | ```js 19 | favoriteLocation(locationId) { 20 | this.dispatch(locationId); 21 | } 22 | ``` 23 | 24 | Next, lets build our FavoritesStore. 25 | 26 | `stores/FavoritesStore.js` 27 | 28 | ```js 29 | var alt = require('../alt'); 30 | var LocationActions = require('../actions/LocationActions'); 31 | 32 | class FavoritesStore { 33 | constructor() { 34 | this.locations = []; 35 | 36 | this.bindListeners({ 37 | addFavoriteLocation: LocationActions.FAVORITE_LOCATION 38 | }); 39 | } 40 | 41 | addFavoriteLocation(location) { 42 | this.locations.push(location); 43 | } 44 | } 45 | 46 | module.exports = alt.createStore(FavoritesStore, 'FavoritesStore'); 47 | ``` 48 | 49 | And finally lets set the waitFor dependency in our LocationStore. But first, make sure you bind the new action to a new action handler in the store. 50 | 51 | ```js 52 | this.bindListeners({ 53 | handleUpdateLocations: LocationActions.UPDATE_LOCATIONS, 54 | handleFetchLocations: LocationActions.FETCH_LOCATIONS, 55 | handleLocationsFailed: LocationActions.LOCATIONS_FAILED, 56 | setFavorites: LocationActions.FAVORITE_LOCATION 57 | }); 58 | ``` 59 | 60 | And lets create the action handler with `waitFor`. 61 | 62 | ```js 63 | resetAllFavorites() { 64 | this.locations = this.locations.map((location) => { 65 | return { 66 | id: location.id, 67 | name: location.name, 68 | has_favorite: false 69 | }; 70 | }); 71 | } 72 | 73 | setFavorites(location) { 74 | this.waitFor(FavoritesStore); 75 | 76 | var favoritedLocations = FavoritesStore.getState().locations; 77 | 78 | this.resetAllFavorites(); 79 | 80 | favoritedLocations.forEach((location) => { 81 | // find each location in the array 82 | for (var i = 0; i < this.locations.length; i += 1) { 83 | 84 | // set has_favorite to true 85 | if (this.locations[i].id === location.id) { 86 | this.locations[i].has_favorite = true; 87 | break; 88 | } 89 | } 90 | }); 91 | } 92 | ``` 93 | 94 | You can check out the working final result [here](https://github.com/goatslacker/alt-tutorial). 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alt", 3 | "version": "0.18.6", 4 | "description": "A flux implementation", 5 | "main": "lib", 6 | "jsnext:main": "src", 7 | "dependencies": { 8 | "flux": "2.1.1", 9 | "is-promise": "2.1.0", 10 | "transmitter": "3.0.1" 11 | }, 12 | "devDependencies": { 13 | "alt-search-docs": "1.0.6", 14 | "babel-cli": "6.6.5", 15 | "babel-core": "6.7.4", 16 | "babel-eslint": "5.0.0", 17 | "babel-loader": "6.2.4", 18 | "babel-plugin-add-module-exports": "^0.1.2", 19 | "babel-plugin-external-helpers-2": "6.3.13", 20 | "babel-plugin-transform-class-properties": "^6.6.0", 21 | "babel-plugin-transform-es2015-classes": "6.6.5", 22 | "babel-preset-airbnb": "1.0.1", 23 | "babel-preset-stage-0": "6.3.13", 24 | "chai": "^2.3.0", 25 | "coveralls": "2.11.4", 26 | "es6-promise": "^2.1.1", 27 | "eslint": "1.10.3", 28 | "eslint-config-airbnb": "2.0.0", 29 | "eslint-plugin-react": "3.11.3", 30 | "ghooks": "^0.3.2", 31 | "immutable": "^3.7.2", 32 | "iso": "^4.1.0", 33 | "istanbul": "0.3.19", 34 | "jsdom": "6.3.0", 35 | "lunr": "^0.5.9", 36 | "mocha": "^2.2.4", 37 | "object-assign": "^2.0.0", 38 | "react": "0.14.0", 39 | "react-addons-test-utils": "0.14.0", 40 | "react-dom": "0.14.0", 41 | "rimraf": "^2.3.2", 42 | "sinon": "^1.14.0", 43 | "style-loader": "^0.13.0", 44 | "webpack": "^1.9.12" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "https://github.com/goatslacker/alt.git" 49 | }, 50 | "authors": [ 51 | "Josh Perez ", 52 | "Jonathan Lehman " 53 | ], 54 | "license": "MIT", 55 | "scripts": { 56 | "build": "npm run clean && npm run transpile && npm run build-alt-browser", 57 | "build-alt-browser": "webpack --config dist.config.js && webpack -p --config dist.min.config.js", 58 | "clean": "rimraf lib", 59 | "coverage": "npm run transpile-cover && babel-node node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u exports -R tap --require test/babel test", 60 | "lint": "eslint src components", 61 | "postversion": "git push && git push --tags", 62 | "prepublish": "npm run lint && npm run build", 63 | "pretest": "npm run clean && npm run transpile", 64 | "preversion": "npm run clean && npm run lint", 65 | "release": "npm run build && mversion patch -m", 66 | "size": "npm run transpile; browserify flux.js > flux-build.js; uglifyjs -m -c 'comparisons=false,keep_fargs=true,unsafe=true,unsafe_comps=true,warnings=false' flux-build.js > flux-build.min.js", 67 | "test": "npm run test-node", 68 | "test-node": "babel-node node_modules/.bin/_mocha -u exports -R nyan test", 69 | "transpile": "babel src --out-dir lib", 70 | "transpile-cover": "babel src --out-dir lib --plugins external-helpers-2", 71 | "version": "npm run build" 72 | }, 73 | "files": [ 74 | "src", 75 | "lib", 76 | "scripts", 77 | "typings", 78 | "dist", 79 | "docs", 80 | "guides", 81 | "README.md" 82 | ], 83 | "keywords": [ 84 | "alt", 85 | "es6", 86 | "flow", 87 | "flux", 88 | "react", 89 | "unidirectional" 90 | ], 91 | "config": { 92 | "ghooks": { 93 | "pre-push": "npm run lint" 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/this-dispatch-to-return.js: -------------------------------------------------------------------------------- 1 | const isDispatch = path => ( 2 | path.value.type === 'CallExpression' && 3 | path.value.callee.type === 'MemberExpression' && 4 | // path.value.callee.object.type === 'ThisExpression' && // commented out so we support var self = this; self.dispatch(); 5 | path.value.callee.property.type === 'Identifier' && 6 | path.value.callee.property.name === 'dispatch' 7 | ) 8 | 9 | const isThisActions = path => ( 10 | path.value.type === 'MemberExpression' && 11 | path.value.object.type === 'MemberExpression' && 12 | path.value.object.property.type === 'Identifier' && 13 | path.value.object.property.name === 'actions' 14 | ) 15 | 16 | const updateDispatchToReturn = j => (p) => { 17 | j(p).replaceWith(j.returnStatement(p.value.arguments[0] || null)) 18 | } 19 | 20 | const updateDispatchToCall = j => (p) => { 21 | j(p).replaceWith(j.callExpression(j.identifier('dispatch'), p.value.arguments)) 22 | } 23 | 24 | const updateToJustThis = j => (p) => { 25 | j(p).replaceWith(j.memberExpression(p.value.object.object, p.value.property)) 26 | } 27 | 28 | const findDispatches = (j, p) => { 29 | return j(p).find(j.CallExpression).filter(isDispatch) 30 | } 31 | 32 | const findThisActionReferences = (j, p) => { 33 | return j(p).find(j.MemberExpression).filter(isThisActions) 34 | } 35 | 36 | const replaceFunction = (j, p) => { 37 | j(p).replaceWith(j.functionExpression( 38 | null, 39 | p.value.params, 40 | j.blockStatement([ 41 | j.returnStatement( 42 | j.functionExpression( 43 | null, 44 | [j.identifier('dispatch')], 45 | j.blockStatement(p.value.body.body) 46 | ) 47 | ) 48 | ]) 49 | )) 50 | } 51 | 52 | module.exports = (file, api) => { 53 | const j = api.jscodeshift 54 | const root = j(file.source) 55 | 56 | root.find(j.FunctionExpression).forEach((p) => { 57 | // ignore constructors 58 | if (p.parent.value.type === 'MethodDefinition' && p.parent.value.kind === 'constructor') { 59 | return 60 | } 61 | 62 | // find all dispatches that are inside the function 63 | const dispatches = findDispatches(j, p).size() 64 | const withinParent = findDispatches(j, p).filter(x => x.parent.parent.parent.value === p.value).size() 65 | 66 | if (withinParent === 0 && dispatches > 0) { 67 | replaceFunction(j, p) 68 | findDispatches(j, p).forEach(updateDispatchToCall(j)) 69 | 70 | } else if (dispatches === 0) { 71 | const hasReturn = j(p).find(j.ReturnStatement).size() > 0 72 | if (hasReturn) { 73 | console.warn('Could not transform function because it returned', 'at line', p.parent.value.loc.start.line) 74 | } else { 75 | console.warn('This function does not dispatch?', 'at line', p.parent.value.loc.start.line) 76 | } 77 | 78 | // if there are multiple dispatches happening then we'll need to return a 79 | // dispatch function and update this.dispatch to a dispatch call 80 | } else if (dispatches > 1) { 81 | replaceFunction(j, p) 82 | findDispatches(j, p).forEach(updateDispatchToCall(j)) 83 | 84 | // if there's a single dispatch then it's ok to return to dispatch 85 | } else { 86 | // if its the only statement within the function 87 | if (p.value.body.body.length === 1) { 88 | findDispatches(j, p).forEach(updateDispatchToReturn(j)) 89 | // otherwise lets run the function 90 | } else { 91 | replaceFunction(j, p) 92 | findDispatches(j, p).forEach(updateDispatchToCall(j)) 93 | } 94 | } 95 | 96 | // Also find any mentions to `this.actions` 97 | findThisActionReferences(j, p).forEach(updateToJustThis(j)) 98 | }) 99 | 100 | return root.toSource({ quote: 'single' }) 101 | } 102 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as fn from '../functions' 2 | import * as utils from '../utils/AltUtils' 3 | import isPromise from 'is-promise' 4 | 5 | export default function makeAction(alt, namespace, name, implementation, obj) { 6 | const id = utils.uid(alt._actionsRegistry, `${namespace}.${name}`) 7 | alt._actionsRegistry[id] = 1 8 | 9 | const data = { id, namespace, name } 10 | 11 | const dispatch = (payload) => alt.dispatch(id, payload, data) 12 | 13 | // the action itself 14 | const action = (...args) => { 15 | const invocationResult = implementation.apply(obj, args) 16 | let actionResult = invocationResult 17 | 18 | // async functions that return promises should not be dispatched 19 | if (invocationResult !== undefined && !isPromise(invocationResult)) { 20 | if (fn.isFunction(invocationResult)) { 21 | // inner function result should be returned as an action result 22 | actionResult = invocationResult(dispatch, alt) 23 | } else { 24 | dispatch(invocationResult) 25 | } 26 | } 27 | 28 | if (invocationResult === undefined) { 29 | utils.warn('An action was called but nothing was dispatched') 30 | } 31 | 32 | return actionResult 33 | } 34 | action.defer = (...args) => setTimeout(() => action.apply(null, args)) 35 | action.id = id 36 | action.data = data 37 | 38 | // ensure each reference is unique in the namespace 39 | const container = alt.actions[namespace] 40 | const namespaceId = utils.uid(container, name) 41 | container[namespaceId] = action 42 | 43 | // generate a constant 44 | const constant = utils.formatAsConstant(namespaceId) 45 | container[constant] = id 46 | 47 | return action 48 | } 49 | -------------------------------------------------------------------------------- /src/functions.js: -------------------------------------------------------------------------------- 1 | export const isFunction = x => typeof x === 'function' 2 | 3 | export function isMutableObject(target) { 4 | const Ctor = target.constructor 5 | 6 | return ( 7 | !!target 8 | && 9 | Object.prototype.toString.call(target) === '[object Object]' 10 | && 11 | isFunction(Ctor) 12 | && 13 | !Object.isFrozen(target) 14 | && 15 | (Ctor instanceof Ctor || target.type === 'AltStore') 16 | ) 17 | } 18 | 19 | export function eachObject(f, o) { 20 | o.forEach((from) => { 21 | Object.keys(Object(from)).forEach((key) => { 22 | f(key, from[key]) 23 | }) 24 | }) 25 | } 26 | 27 | export function assign(target, ...source) { 28 | eachObject((key, value) => target[key] = value, source) 29 | return target 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | import { Dispatcher } from 'flux' 3 | 4 | import * as StateFunctions from './utils/StateFunctions' 5 | import * as fn from './functions' 6 | import * as store from './store' 7 | import * as utils from './utils/AltUtils' 8 | import makeAction from './actions' 9 | 10 | class Alt { 11 | constructor(config = {}) { 12 | this.config = config 13 | this.serialize = config.serialize || JSON.stringify 14 | this.deserialize = config.deserialize || JSON.parse 15 | this.dispatcher = config.dispatcher || new Dispatcher() 16 | this.batchingFunction = config.batchingFunction || (callback => callback()) 17 | this.actions = { global: {} } 18 | this.stores = {} 19 | this.storeTransforms = config.storeTransforms || [] 20 | this.trapAsync = false 21 | this._actionsRegistry = {} 22 | this._initSnapshot = {} 23 | this._lastSnapshot = {} 24 | } 25 | 26 | dispatch(action, data, details) { 27 | this.batchingFunction(() => { 28 | const id = Math.random().toString(18).substr(2, 16) 29 | 30 | // support straight dispatching of FSA-style actions 31 | if (action.hasOwnProperty('type') && action.hasOwnProperty('payload')) { 32 | const fsaDetails = { 33 | id: action.type, 34 | namespace: action.type, 35 | name: action.type, 36 | } 37 | return this.dispatcher.dispatch( 38 | utils.fsa(id, action.type, action.payload, fsaDetails) 39 | ) 40 | } 41 | 42 | if (action.id && action.dispatch) { 43 | return utils.dispatch(id, action, data, this) 44 | } 45 | 46 | return this.dispatcher.dispatch(utils.fsa(id, action, data, details)) 47 | }) 48 | } 49 | 50 | createUnsavedStore(StoreModel, ...args) { 51 | const key = StoreModel.displayName || '' 52 | store.createStoreConfig(this.config, StoreModel) 53 | const Store = store.transformStore(this.storeTransforms, StoreModel) 54 | 55 | return fn.isFunction(Store) 56 | ? store.createStoreFromClass(this, Store, key, ...args) 57 | : store.createStoreFromObject(this, Store, key) 58 | } 59 | 60 | createStore(StoreModel, iden, ...args) { 61 | let key = iden || StoreModel.displayName || StoreModel.name || '' 62 | store.createStoreConfig(this.config, StoreModel) 63 | const Store = store.transformStore(this.storeTransforms, StoreModel) 64 | 65 | /* istanbul ignore next */ 66 | if (module.hot) delete this.stores[key] 67 | 68 | if (this.stores[key] || !key) { 69 | if (this.stores[key]) { 70 | utils.warn( 71 | `A store named ${key} already exists, double check your store ` + 72 | `names or pass in your own custom identifier for each store` 73 | ) 74 | } else { 75 | utils.warn('Store name was not specified') 76 | } 77 | 78 | key = utils.uid(this.stores, key) 79 | } 80 | 81 | const storeInstance = fn.isFunction(Store) 82 | ? store.createStoreFromClass(this, Store, key, ...args) 83 | : store.createStoreFromObject(this, Store, key) 84 | 85 | this.stores[key] = storeInstance 86 | StateFunctions.saveInitialSnapshot(this, key) 87 | 88 | return storeInstance 89 | } 90 | 91 | generateActions(...actionNames) { 92 | const actions = { name: 'global' } 93 | return this.createActions(actionNames.reduce((obj, action) => { 94 | obj[action] = utils.dispatchIdentity 95 | return obj 96 | }, actions)) 97 | } 98 | 99 | createAction(name, implementation, obj) { 100 | return makeAction(this, 'global', name, implementation, obj) 101 | } 102 | 103 | createActions(ActionsClass, exportObj = {}, ...argsForConstructor) { 104 | const actions = {} 105 | const key = utils.uid( 106 | this._actionsRegistry, 107 | ActionsClass.displayName || ActionsClass.name || 'Unknown' 108 | ) 109 | 110 | if (fn.isFunction(ActionsClass)) { 111 | fn.assign(actions, utils.getPrototypeChain(ActionsClass)) 112 | class ActionsGenerator extends ActionsClass { 113 | constructor(...args) { 114 | super(...args) 115 | } 116 | 117 | generateActions(...actionNames) { 118 | actionNames.forEach((actionName) => { 119 | actions[actionName] = utils.dispatchIdentity 120 | }) 121 | } 122 | } 123 | 124 | fn.assign(actions, new ActionsGenerator(...argsForConstructor)) 125 | } else { 126 | fn.assign(actions, ActionsClass) 127 | } 128 | 129 | this.actions[key] = this.actions[key] || {} 130 | 131 | fn.eachObject((actionName, action) => { 132 | if (!fn.isFunction(action)) { 133 | exportObj[actionName] = action 134 | return 135 | } 136 | 137 | // create the action 138 | exportObj[actionName] = makeAction( 139 | this, 140 | key, 141 | actionName, 142 | action, 143 | exportObj 144 | ) 145 | 146 | // generate a constant 147 | const constant = utils.formatAsConstant(actionName) 148 | exportObj[constant] = exportObj[actionName].id 149 | }, [actions]) 150 | 151 | return exportObj 152 | } 153 | 154 | takeSnapshot(...storeNames) { 155 | const state = StateFunctions.snapshot(this, storeNames) 156 | fn.assign(this._lastSnapshot, state) 157 | return this.serialize(state) 158 | } 159 | 160 | rollback() { 161 | StateFunctions.setAppState( 162 | this, 163 | this.serialize(this._lastSnapshot), 164 | storeInst => { 165 | storeInst.lifecycle('rollback') 166 | storeInst.emitChange() 167 | } 168 | ) 169 | } 170 | 171 | recycle(...storeNames) { 172 | const initialSnapshot = storeNames.length 173 | ? StateFunctions.filterSnapshots( 174 | this, 175 | this._initSnapshot, 176 | storeNames 177 | ) 178 | : this._initSnapshot 179 | 180 | StateFunctions.setAppState( 181 | this, 182 | this.serialize(initialSnapshot), 183 | (storeInst) => { 184 | storeInst.lifecycle('init') 185 | storeInst.emitChange() 186 | } 187 | ) 188 | } 189 | 190 | flush() { 191 | const state = this.serialize(StateFunctions.snapshot(this)) 192 | this.recycle() 193 | return state 194 | } 195 | 196 | bootstrap(data) { 197 | StateFunctions.setAppState(this, data, (storeInst, state) => { 198 | storeInst.lifecycle('bootstrap', state) 199 | storeInst.emitChange() 200 | }) 201 | } 202 | 203 | prepare(storeInst, payload) { 204 | const data = {} 205 | if (!storeInst.displayName) { 206 | throw new ReferenceError('Store provided does not have a name') 207 | } 208 | data[storeInst.displayName] = payload 209 | return this.serialize(data) 210 | } 211 | 212 | // Instance type methods for injecting alt into your application as context 213 | 214 | addActions(name, ActionsClass, ...args) { 215 | this.actions[name] = Array.isArray(ActionsClass) 216 | ? this.generateActions.apply(this, ActionsClass) 217 | : this.createActions(ActionsClass, ...args) 218 | } 219 | 220 | addStore(name, StoreModel, ...args) { 221 | this.createStore(StoreModel, name, ...args) 222 | } 223 | 224 | getActions(name) { 225 | return this.actions[name] 226 | } 227 | 228 | getStore(name) { 229 | return this.stores[name] 230 | } 231 | 232 | static debug(name, alt, win) { 233 | const key = 'alt.js.org' 234 | let context = win 235 | if (!context && typeof window !== 'undefined') { 236 | context = window 237 | } 238 | if (typeof context !== 'undefined') { 239 | context[key] = context[key] || [] 240 | context[key].push({ name, alt }) 241 | } 242 | return alt 243 | } 244 | } 245 | 246 | export default Alt 247 | -------------------------------------------------------------------------------- /src/store/AltStore.js: -------------------------------------------------------------------------------- 1 | import * as fn from '../functions' 2 | import transmitter from 'transmitter' 3 | 4 | class AltStore { 5 | constructor(alt, model, state, StoreModel) { 6 | const lifecycleEvents = model.lifecycleEvents 7 | this.transmitter = transmitter() 8 | this.lifecycle = (event, x) => { 9 | if (lifecycleEvents[event]) lifecycleEvents[event].publish(x) 10 | } 11 | this.state = state 12 | 13 | this.alt = alt 14 | this.preventDefault = false 15 | this.displayName = model.displayName 16 | this.boundListeners = model.boundListeners 17 | this.StoreModel = StoreModel 18 | this.reduce = model.reduce || (x => x) 19 | this.subscriptions = [] 20 | 21 | const output = model.output || (x => x) 22 | 23 | this.emitChange = () => this.transmitter.publish(output(this.state)) 24 | 25 | const handleDispatch = (f, payload) => { 26 | try { 27 | return f() 28 | } catch (e) { 29 | if (model.handlesOwnErrors) { 30 | this.lifecycle('error', { 31 | error: e, 32 | payload, 33 | state: this.state, 34 | }) 35 | return false 36 | } 37 | 38 | throw e 39 | } 40 | } 41 | 42 | fn.assign(this, model.publicMethods) 43 | 44 | // Register dispatcher 45 | this.dispatchToken = alt.dispatcher.register((payload) => { 46 | this.preventDefault = false 47 | 48 | this.lifecycle('beforeEach', { 49 | payload, 50 | state: this.state, 51 | }) 52 | 53 | const actionHandlers = model.actionListeners[payload.action] 54 | 55 | if (actionHandlers || model.otherwise) { 56 | let result 57 | 58 | if (actionHandlers) { 59 | result = handleDispatch(() => { 60 | return actionHandlers.filter(Boolean).every((handler) => { 61 | return handler.call(model, payload.data, payload.action) !== false 62 | }) 63 | }, payload) 64 | } else { 65 | result = handleDispatch(() => { 66 | return model.otherwise(payload.data, payload.action) 67 | }, payload) 68 | } 69 | 70 | if (result !== false && !this.preventDefault) this.emitChange() 71 | } 72 | 73 | if (model.reduce) { 74 | handleDispatch(() => { 75 | const value = model.reduce(this.state, payload) 76 | if (value !== undefined) this.state = value 77 | }, payload) 78 | if (!this.preventDefault) this.emitChange() 79 | } 80 | 81 | this.lifecycle('afterEach', { 82 | payload, 83 | state: this.state, 84 | }) 85 | }) 86 | 87 | this.lifecycle('init') 88 | } 89 | 90 | listen(cb) { 91 | if (!fn.isFunction(cb)) throw new TypeError('listen expects a function') 92 | const { dispose } = this.transmitter.subscribe(cb) 93 | this.subscriptions.push({ cb, dispose }) 94 | return () => { 95 | this.lifecycle('unlisten') 96 | dispose() 97 | } 98 | } 99 | 100 | unlisten(cb) { 101 | this.lifecycle('unlisten') 102 | this.subscriptions 103 | .filter(subscription => subscription.cb === cb) 104 | .forEach(subscription => subscription.dispose()) 105 | } 106 | 107 | getState() { 108 | return this.StoreModel.config.getState.call(this, this.state) 109 | } 110 | } 111 | 112 | export default AltStore 113 | -------------------------------------------------------------------------------- /src/store/StoreMixin.js: -------------------------------------------------------------------------------- 1 | import transmitter from 'transmitter' 2 | import * as fn from '../functions' 3 | 4 | const StoreMixin = { 5 | waitFor(...sources) { 6 | if (!sources.length) { 7 | throw new ReferenceError('Dispatch tokens not provided') 8 | } 9 | 10 | let sourcesArray = sources 11 | if (sources.length === 1) { 12 | sourcesArray = Array.isArray(sources[0]) ? sources[0] : sources 13 | } 14 | 15 | const tokens = sourcesArray.map((source) => { 16 | return source.dispatchToken || source 17 | }) 18 | 19 | this.dispatcher.waitFor(tokens) 20 | }, 21 | 22 | exportAsync(asyncMethods) { 23 | this.registerAsync(asyncMethods) 24 | }, 25 | 26 | registerAsync(asyncDef) { 27 | let loadCounter = 0 28 | 29 | const asyncMethods = fn.isFunction(asyncDef) 30 | ? asyncDef(this.alt) 31 | : asyncDef 32 | 33 | const toExport = Object.keys(asyncMethods).reduce((publicMethods, methodName) => { 34 | const desc = asyncMethods[methodName] 35 | const spec = fn.isFunction(desc) ? desc(this) : desc 36 | 37 | const validHandlers = ['success', 'error', 'loading'] 38 | validHandlers.forEach((handler) => { 39 | if (spec[handler] && !spec[handler].id) { 40 | throw new Error(`${handler} handler must be an action function`) 41 | } 42 | }) 43 | 44 | publicMethods[methodName] = (...args) => { 45 | const state = this.getInstance().getState() 46 | const value = spec.local && spec.local(state, ...args) 47 | const shouldFetch = spec.shouldFetch 48 | ? spec.shouldFetch(state, ...args) 49 | /*eslint-disable*/ 50 | : value == null 51 | /*eslint-enable*/ 52 | const intercept = spec.interceptResponse || (x => x) 53 | 54 | const makeActionHandler = (action, isError) => { 55 | return (x) => { 56 | const fire = () => { 57 | loadCounter -= 1 58 | action(intercept(x, action, args)) 59 | if (isError) throw x 60 | return x 61 | } 62 | return this.alt.trapAsync ? () => fire() : fire() 63 | } 64 | } 65 | 66 | // if we don't have it in cache then fetch it 67 | if (shouldFetch) { 68 | loadCounter += 1 69 | /* istanbul ignore else */ 70 | if (spec.loading) spec.loading(intercept(null, spec.loading, args)) 71 | return spec.remote(state, ...args).then( 72 | makeActionHandler(spec.success), 73 | makeActionHandler(spec.error, 1) 74 | ) 75 | } 76 | 77 | // otherwise emit the change now 78 | this.emitChange() 79 | return value 80 | } 81 | 82 | return publicMethods 83 | }, {}) 84 | 85 | this.exportPublicMethods(toExport) 86 | this.exportPublicMethods({ 87 | isLoading: () => loadCounter > 0, 88 | }) 89 | }, 90 | 91 | exportPublicMethods(methods) { 92 | fn.eachObject((methodName, value) => { 93 | if (!fn.isFunction(value)) { 94 | throw new TypeError('exportPublicMethods expects a function') 95 | } 96 | 97 | this.publicMethods[methodName] = value 98 | }, [methods]) 99 | }, 100 | 101 | emitChange() { 102 | this.getInstance().emitChange() 103 | }, 104 | 105 | on(lifecycleEvent, handler) { 106 | if (lifecycleEvent === 'error') this.handlesOwnErrors = true 107 | const bus = this.lifecycleEvents[lifecycleEvent] || transmitter() 108 | this.lifecycleEvents[lifecycleEvent] = bus 109 | return bus.subscribe(handler.bind(this)) 110 | }, 111 | 112 | bindAction(symbol, handler) { 113 | if (!symbol) { 114 | throw new ReferenceError('Invalid action reference passed in') 115 | } 116 | if (!fn.isFunction(handler)) { 117 | throw new TypeError('bindAction expects a function') 118 | } 119 | 120 | // You can pass in the constant or the function itself 121 | const key = symbol.id ? symbol.id : symbol 122 | this.actionListeners[key] = this.actionListeners[key] || [] 123 | this.actionListeners[key].push(handler.bind(this)) 124 | this.boundListeners.push(key) 125 | }, 126 | 127 | bindActions(actions) { 128 | fn.eachObject((action, symbol) => { 129 | const matchFirstCharacter = /./ 130 | const assumedEventHandler = action.replace(matchFirstCharacter, (x) => { 131 | return `on${x[0].toUpperCase()}` 132 | }) 133 | 134 | if (this[action] && this[assumedEventHandler]) { 135 | // If you have both action and onAction 136 | throw new ReferenceError( 137 | `You have multiple action handlers bound to an action: ` + 138 | `${action} and ${assumedEventHandler}` 139 | ) 140 | } 141 | 142 | const handler = this[action] || this[assumedEventHandler] 143 | if (handler) { 144 | this.bindAction(symbol, handler) 145 | } 146 | }, [actions]) 147 | }, 148 | 149 | bindListeners(obj) { 150 | fn.eachObject((methodName, symbol) => { 151 | const listener = this[methodName] 152 | 153 | if (!listener) { 154 | throw new ReferenceError( 155 | `${methodName} defined but does not exist in ${this.displayName}` 156 | ) 157 | } 158 | 159 | if (Array.isArray(symbol)) { 160 | symbol.forEach((action) => { 161 | this.bindAction(action, listener) 162 | }) 163 | } else { 164 | this.bindAction(symbol, listener) 165 | } 166 | }, [obj]) 167 | }, 168 | } 169 | 170 | export default StoreMixin 171 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils/AltUtils' 2 | import * as fn from '../functions' 3 | import AltStore from './AltStore' 4 | import StoreMixin from './StoreMixin' 5 | 6 | function doSetState(store, storeInstance, state) { 7 | if (!state) { 8 | return 9 | } 10 | 11 | const { config } = storeInstance.StoreModel 12 | 13 | const nextState = fn.isFunction(state) 14 | ? state(storeInstance.state) 15 | : state 16 | 17 | storeInstance.state = config.setState.call( 18 | store, 19 | storeInstance.state, 20 | nextState 21 | ) 22 | 23 | if (!store.alt.dispatcher.isDispatching()) { 24 | store.emitChange() 25 | } 26 | } 27 | 28 | function createPrototype(proto, alt, key, extras) { 29 | return fn.assign(proto, StoreMixin, { 30 | displayName: key, 31 | alt: alt, 32 | dispatcher: alt.dispatcher, 33 | preventDefault() { 34 | this.getInstance().preventDefault = true 35 | }, 36 | boundListeners: [], 37 | lifecycleEvents: {}, 38 | actionListeners: {}, 39 | publicMethods: {}, 40 | handlesOwnErrors: false, 41 | }, extras) 42 | } 43 | 44 | export function createStoreConfig(globalConfig, StoreModel) { 45 | StoreModel.config = fn.assign({ 46 | getState(state) { 47 | if (Array.isArray(state)) { 48 | return state.slice() 49 | } else if (fn.isMutableObject(state)) { 50 | return fn.assign({}, state) 51 | } 52 | 53 | return state 54 | }, 55 | setState(currentState, nextState) { 56 | if (fn.isMutableObject(nextState)) { 57 | return fn.assign(currentState, nextState) 58 | } 59 | return nextState 60 | }, 61 | }, globalConfig, StoreModel.config) 62 | } 63 | 64 | export function transformStore(transforms, StoreModel) { 65 | return transforms.reduce((Store, transform) => transform(Store), StoreModel) 66 | } 67 | 68 | export function createStoreFromObject(alt, StoreModel, key) { 69 | let storeInstance 70 | 71 | const StoreProto = createPrototype({}, alt, key, fn.assign({ 72 | getInstance() { 73 | return storeInstance 74 | }, 75 | setState(nextState) { 76 | doSetState(this, storeInstance, nextState) 77 | }, 78 | }, StoreModel)) 79 | 80 | // bind the store listeners 81 | /* istanbul ignore else */ 82 | if (StoreProto.bindListeners) { 83 | StoreMixin.bindListeners.call( 84 | StoreProto, 85 | StoreProto.bindListeners, 86 | ) 87 | } 88 | /* istanbul ignore else */ 89 | if (StoreProto.observe) { 90 | StoreMixin.bindListeners.call( 91 | StoreProto, 92 | StoreProto.observe(alt), 93 | ) 94 | } 95 | 96 | // bind the lifecycle events 97 | /* istanbul ignore else */ 98 | if (StoreProto.lifecycle) { 99 | fn.eachObject((eventName, event) => { 100 | StoreMixin.on.call(StoreProto, eventName, event) 101 | }, [StoreProto.lifecycle]) 102 | } 103 | 104 | // create the instance and fn.assign the public methods to the instance 105 | storeInstance = fn.assign( 106 | new AltStore( 107 | alt, 108 | StoreProto, 109 | StoreProto.state !== undefined ? StoreProto.state : {}, 110 | StoreModel 111 | ), 112 | StoreProto.publicMethods, 113 | { 114 | displayName: key, 115 | config: StoreModel.config, 116 | } 117 | ) 118 | 119 | return storeInstance 120 | } 121 | 122 | export function createStoreFromClass(alt, StoreModel, key, ...argsForClass) { 123 | let storeInstance 124 | const { config } = StoreModel 125 | 126 | // Creating a class here so we don't overload the provided store's 127 | // prototype with the mixin behaviour and I'm extending from StoreModel 128 | // so we can inherit any extensions from the provided store. 129 | class Store extends StoreModel { 130 | constructor(...args) { 131 | super(...args) 132 | } 133 | } 134 | 135 | createPrototype(Store.prototype, alt, key, { 136 | type: 'AltStore', 137 | getInstance() { 138 | return storeInstance 139 | }, 140 | setState(nextState) { 141 | doSetState(this, storeInstance, nextState) 142 | }, 143 | }) 144 | 145 | const store = new Store(...argsForClass) 146 | 147 | /* istanbul ignore next */ 148 | if (config.bindListeners) store.bindListeners(config.bindListeners) 149 | /* istanbul ignore next */ 150 | if (config.datasource) store.registerAsync(config.datasource) 151 | 152 | storeInstance = fn.assign( 153 | new AltStore( 154 | alt, 155 | store, 156 | store.state !== undefined ? store.state : store, 157 | StoreModel 158 | ), 159 | utils.getInternalMethods(StoreModel), 160 | config.publicMethods, 161 | { displayName: key }, 162 | ) 163 | 164 | return storeInstance 165 | } 166 | -------------------------------------------------------------------------------- /src/utils/AltUtils.js: -------------------------------------------------------------------------------- 1 | import * as fn from '../functions' 2 | 3 | /*eslint-disable*/ 4 | const builtIns = Object.getOwnPropertyNames(NoopClass) 5 | const builtInProto = Object.getOwnPropertyNames(NoopClass.prototype) 6 | /*eslint-enable*/ 7 | 8 | export function getInternalMethods(Obj, isProto) { 9 | const excluded = isProto ? builtInProto : builtIns 10 | const obj = isProto ? Obj.prototype : Obj 11 | return Object.getOwnPropertyNames(obj).reduce((value, m) => { 12 | if (excluded.indexOf(m) !== -1) { 13 | return value 14 | } 15 | 16 | value[m] = obj[m] 17 | return value 18 | }, {}) 19 | } 20 | 21 | export function getPrototypeChain(Obj, methods = {}) { 22 | return Obj === Function.prototype 23 | ? methods 24 | : getPrototypeChain( 25 | Object.getPrototypeOf(Obj), 26 | fn.assign(getInternalMethods(Obj, true), methods) 27 | ) 28 | } 29 | 30 | export function warn(msg) { 31 | /* istanbul ignore else */ 32 | /*eslint-disable*/ 33 | if (typeof console !== 'undefined') { 34 | console.warn(new ReferenceError(msg)) 35 | } 36 | /*eslint-enable*/ 37 | } 38 | 39 | export function uid(container, name) { 40 | let count = 0 41 | let key = name 42 | while (Object.hasOwnProperty.call(container, key)) { 43 | key = name + String(++count) 44 | } 45 | return key 46 | } 47 | 48 | export function formatAsConstant(name) { 49 | return name.replace(/[a-z]([A-Z])/g, (i) => { 50 | return `${i[0]}_${i[1].toLowerCase()}` 51 | }).toUpperCase() 52 | } 53 | 54 | export function dispatchIdentity(x, ...a) { 55 | if (x === undefined) return null 56 | return a.length ? [x].concat(a) : x 57 | } 58 | 59 | export function fsa(id, type, payload, details) { 60 | return { 61 | type, 62 | payload, 63 | meta: { 64 | dispatchId: id, 65 | ...details, 66 | }, 67 | 68 | id, 69 | action: type, 70 | data: payload, 71 | details, 72 | } 73 | } 74 | 75 | export function dispatch(id, actionObj, payload, alt) { 76 | const data = actionObj.dispatch(payload) 77 | if (data === undefined) return null 78 | 79 | const type = actionObj.id 80 | const namespace = type 81 | const name = type 82 | const details = { id: type, namespace, name } 83 | 84 | const dispatchLater = x => alt.dispatch(type, x, details) 85 | 86 | if (fn.isFunction(data)) return data(dispatchLater, alt) 87 | 88 | // XXX standardize this 89 | return alt.dispatcher.dispatch(fsa(id, type, data, details)) 90 | } 91 | 92 | /* istanbul ignore next */ 93 | function NoopClass() { } 94 | -------------------------------------------------------------------------------- /src/utils/StateFunctions.js: -------------------------------------------------------------------------------- 1 | import * as fn from '../functions' 2 | 3 | export function setAppState(instance, data, onStore) { 4 | const obj = instance.deserialize(data) 5 | fn.eachObject((key, value) => { 6 | const store = instance.stores[key] 7 | if (store) { 8 | const { config } = store.StoreModel 9 | const state = store.state 10 | if (config.onDeserialize) obj[key] = config.onDeserialize(value) || value 11 | if (fn.isMutableObject(state)) { 12 | fn.eachObject(k => delete state[k], [state]) 13 | fn.assign(state, obj[key]) 14 | } else { 15 | store.state = obj[key] 16 | } 17 | onStore(store, store.state) 18 | } 19 | }, [obj]) 20 | } 21 | 22 | export function snapshot(instance, storeNames = []) { 23 | const stores = storeNames.length ? storeNames : Object.keys(instance.stores) 24 | return stores.reduce((obj, storeHandle) => { 25 | const storeName = storeHandle.displayName || storeHandle 26 | const store = instance.stores[storeName] 27 | const { config } = store.StoreModel 28 | store.lifecycle('snapshot') 29 | const customSnapshot = config.onSerialize && 30 | config.onSerialize(store.state) 31 | obj[storeName] = customSnapshot ? customSnapshot : store.getState() 32 | return obj 33 | }, {}) 34 | } 35 | 36 | export function saveInitialSnapshot(instance, key) { 37 | const state = instance.deserialize( 38 | instance.serialize(instance.stores[key].state) 39 | ) 40 | instance._initSnapshot[key] = state 41 | instance._lastSnapshot[key] = state 42 | } 43 | 44 | export function filterSnapshots(instance, state, stores) { 45 | return stores.reduce((obj, store) => { 46 | const storeName = store.displayName || store 47 | if (!state[storeName]) { 48 | throw new ReferenceError(`${storeName} is not a valid store`) 49 | } 50 | obj[storeName] = state[storeName] 51 | return obj 52 | }, {}) 53 | } 54 | -------------------------------------------------------------------------------- /test/actions-dump-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../' 2 | import { assert } from 'chai' 3 | 4 | const alt = new Alt() 5 | 6 | alt.generateActions('one', 'two') 7 | const test = alt.generateActions('three') 8 | alt.generateActions('one') 9 | 10 | alt.createActions(class FooActions { 11 | static displayName = 'FooActions'; 12 | one() {} 13 | two() {} 14 | }) 15 | 16 | const pojo = alt.createActions({ 17 | displayName: 'Pojo', 18 | one() { }, 19 | two() { } 20 | }) 21 | 22 | alt.createActions({ 23 | one() { }, 24 | two() { } 25 | }) 26 | 27 | alt.createAction('test', function () { }) 28 | 29 | export default { 30 | 'actions obj'() { 31 | assert.isObject(alt.actions, 'actions exist') 32 | assert.isFunction(alt.actions.global.test, 'test exists') 33 | assert(Object.keys(alt.actions.global).length === 10, 'global actions contain stuff from createAction and generateActions') 34 | assert(Object.keys(alt.actions.FooActions).length === 4, '2 actions namespaced on FooActions') 35 | assert.isObject(alt.actions.Pojo, 'pojo named action exists') 36 | assert(Object.keys(alt.actions.Pojo).length == 4, 'pojo has 2 actions associated with it') 37 | 38 | assert.isDefined(alt.actions.global.three, 'three action is defined in global') 39 | 40 | assert.isDefined(alt.actions.Unknown.one, 'one exists in Unknown') 41 | assert.isDefined(alt.actions.global.one1, 'one1 was created because of a name clash') 42 | 43 | assert.isDefined(alt.actions.global.THREE, 'the constant exists too') 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /test/alt-config-object.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | 4 | class MyActions { 5 | constructor() { 6 | this.generateActions('changeNumber') 7 | } 8 | } 9 | 10 | class MyStore { 11 | constructor() { 12 | this.number = 2 13 | this.letter = 'a' 14 | } 15 | 16 | onChangeNumber() { 17 | this.number *= 2 18 | } 19 | } 20 | 21 | export default { 22 | 'custom dispatcher can be specified in alt config'() { 23 | class CustomDispatcher { 24 | waitFor() {} 25 | register() {} 26 | dispatch() {} 27 | extraMethod() {} 28 | } 29 | 30 | const alt = new Alt({ 31 | dispatcher: new CustomDispatcher() 32 | }) 33 | const dispatcher = alt.dispatcher 34 | 35 | // uses custom dispatcher 36 | assert.equal(typeof dispatcher.extraMethod, 'function') 37 | assert.equal(typeof dispatcher.dispatch, 'function') 38 | }, 39 | 40 | 'custom serialize/deserialize'() { 41 | const CustomSerialize = (data) => { 42 | return Object.keys(data).reduce((obj, key) => { 43 | obj[key] = {wrapper: data[key]} 44 | return obj 45 | }, {}) 46 | } 47 | const CustomDeserialize = (data) => { 48 | return Object.keys(data).reduce((obj, key) => { 49 | obj[key] = data[key].wrapper 50 | return obj 51 | }, {}) 52 | } 53 | 54 | const alt = new Alt({ 55 | serialize: CustomSerialize, 56 | deserialize: CustomDeserialize, 57 | }) 58 | alt.addStore('MyStore', MyStore) 59 | alt.addActions('MyActions', MyActions) 60 | const snapshot = alt.takeSnapshot() 61 | alt.getActions('MyActions').changeNumber() 62 | alt.rollback() 63 | 64 | assert.deepEqual(snapshot, {MyStore: {wrapper: {number: 2, letter: 'a'}}}) 65 | assert.deepEqual(alt.getStore('MyStore').getState(), {number: 2, letter: 'a'}) 66 | }, 67 | 68 | 'custom transforms'() { 69 | const alt = new Alt({ storeTransforms: [] }) 70 | assert.isArray(alt.storeTransforms) 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /test/async-action-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../' 2 | import { assert } from 'chai' 3 | import isPromise from 'is-promise' 4 | 5 | const alt = new Alt() 6 | 7 | const actions = alt.createActions(class AsyncActions { 8 | static displayName = 'AsyncActions'; 9 | fetch() { 10 | return Promise.resolve('foo') 11 | } 12 | 13 | fetchAndDispatch() { 14 | return (dispatch) => { 15 | dispatch() 16 | return Promise.resolve('foo') 17 | } 18 | } 19 | }) 20 | 21 | const store = alt.createStore(class FooStore { 22 | static displayName = 'FooStore'; 23 | constructor() { 24 | this.dispatched = false 25 | this.bindActions(actions) 26 | } 27 | onFetch() { 28 | this.dispatched = true 29 | } 30 | onFetchAndDispatch() { 31 | this.dispatched = true 32 | } 33 | }) 34 | 35 | export default { 36 | 'async actions': { 37 | afterEach() { 38 | alt.recycle(store) 39 | }, 40 | 41 | 'are not dispatched automatically'() { 42 | actions.fetch() 43 | assert(store.state.dispatched === false, 'async action is not automatically dispatched') 44 | }, 45 | 46 | 'return the result of inner function invocation'() { 47 | const promise = actions.fetchAndDispatch() 48 | assert(isPromise(promise), 'async action does not return the result of inner function invocation') 49 | assert(store.state.dispatched === true, 'async action is dispatched when the dispatch is invoked manually') 50 | }, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /test/batching-test.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | import Alt from '../' 3 | import React from 'react' 4 | import { assert } from 'chai' 5 | import sinon from 'sinon' 6 | import TestUtils from 'react-addons-test-utils' 7 | import ReactDom from 'react-dom' 8 | 9 | // TOOD action was called but not dispatched? 10 | const Actions = { 11 | buttonClick() { 12 | setTimeout(() => { 13 | this.switchComponent() 14 | }, 10) 15 | }, 16 | 17 | switchComponent() { 18 | return null 19 | }, 20 | 21 | uhoh() { 22 | return null 23 | } 24 | } 25 | 26 | function Store(actions) { 27 | this.active = false 28 | 29 | this.bindAction(actions.switchComponent, () => { 30 | this.active = true 31 | }) 32 | } 33 | 34 | class ComponentA extends React.Component { 35 | constructor(props) { 36 | super(props) 37 | 38 | this.state = props.alt.stores.store.getState() 39 | } 40 | 41 | componentWillMount() { 42 | this.props.alt.stores.store.listen(state => this.setState(state)) 43 | } 44 | 45 | render() { 46 | if (this.state.active) { 47 | return 48 | } else { 49 | return
50 | } 51 | } 52 | } 53 | 54 | class ComponentB extends React.Component { 55 | componentWillMount() { 56 | let error = null 57 | try { 58 | this.props.alt.actions.actions.uhoh() 59 | } catch (err) { 60 | error = err 61 | } finally { 62 | this.props.callback(error) 63 | } 64 | } 65 | 66 | render() { 67 | return
68 | } 69 | } 70 | 71 | export default { 72 | 'Batching dispatcher': { 73 | beforeEach() { 74 | global.document = jsdom('') 75 | global.window = global.document.defaultView 76 | }, 77 | 78 | afterEach() { 79 | delete global.document 80 | delete global.window 81 | }, 82 | 83 | 'does not batch'(done) { 84 | const alt = new Alt() 85 | alt.addActions('actions', Actions) 86 | alt.addStore('store', Store, alt.actions.actions) 87 | 88 | function test(err) { 89 | assert.match(err, /dispatch in the middle of a dispatch/) 90 | done() 91 | } 92 | 93 | TestUtils.renderIntoDocument() 94 | alt.actions.actions.buttonClick() 95 | }, 96 | 97 | 'allows batching'(done) { 98 | const alt = new Alt({ batchingFunction: ReactDom.unstable_batchedUpdates }) 99 | alt.addActions('actions', Actions) 100 | alt.addStore('store', Store, alt.actions.actions) 101 | 102 | function test(err) { 103 | assert.isNull(err) 104 | done() 105 | } 106 | 107 | TestUtils.renderIntoDocument() 108 | alt.actions.actions.buttonClick() 109 | }, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/before-and-after-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../' 2 | import { assert } from 'chai' 3 | 4 | import sinon from 'sinon' 5 | 6 | const alt = new Alt() 7 | const action = alt.generateActions('fire') 8 | 9 | const beforeEach = sinon.spy() 10 | const afterEach = sinon.spy() 11 | 12 | const store = alt.createStore({ 13 | displayName: 'Store', 14 | 15 | state: { a: 1 }, 16 | 17 | bindListeners: { 18 | change: action.fire 19 | }, 20 | 21 | lifecycle: { 22 | beforeEach, 23 | afterEach 24 | }, 25 | 26 | change(x) { 27 | this.setState({ a: x }) 28 | }, 29 | }) 30 | 31 | export default { 32 | 'Before and After hooks': { 33 | beforeEach() { 34 | alt.recycle() 35 | }, 36 | 37 | 'before and after hook fire once'() { 38 | action.fire(2) 39 | 40 | assert.ok(beforeEach.calledOnce) 41 | assert.ok(afterEach.calledOnce) 42 | }, 43 | 44 | 'before is called before after'() { 45 | action.fire(2) 46 | 47 | assert.ok(beforeEach.calledBefore(afterEach)) 48 | assert.ok(afterEach.calledAfter(beforeEach)) 49 | }, 50 | 51 | 'args passed in'() { 52 | action.fire(2) 53 | 54 | assert.ok(beforeEach.args[0].length === 1, '1 arg is passed') 55 | assert.ok(afterEach.args[0].length === 1, '1 arg is passed') 56 | 57 | assert.ok(beforeEach.args[0][0].payload.data === 2, 'before has payload') 58 | assert.ok(afterEach.args[0][0].payload.data === 2, 'after has payload') 59 | }, 60 | 61 | 'before and after get state'() { 62 | let beforeValue = null 63 | let afterValue = null 64 | 65 | const store = alt.createStore({ 66 | displayName: 'SpecialStore', 67 | state: { a: 1 }, 68 | bindListeners: { 69 | change: action.fire 70 | }, 71 | lifecycle: { 72 | beforeEach({ state }) { 73 | beforeValue = state.a 74 | }, 75 | afterEach({ state }) { 76 | afterValue = state.a 77 | } 78 | }, 79 | change(x) { 80 | this.setState({ a: x }) 81 | } 82 | }) 83 | 84 | action.fire(2) 85 | 86 | assert.ok(beforeValue === 1, 'before has current state') 87 | assert.ok(afterValue === 2, 'after has next state') 88 | }, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/bound-listeners-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../dist/alt-with-runtime' 2 | import { assert } from 'chai' 3 | 4 | const alt = new Alt() 5 | 6 | const Actions = alt.generateActions('one', 'two', 'three') 7 | 8 | class NoActions { 9 | constructor() { 10 | this.bindActions(Actions) 11 | } 12 | 13 | foo() { } 14 | bar() { } 15 | } 16 | 17 | class OneAction { 18 | constructor() { 19 | this.bindAction(Actions.ONE, this.one) 20 | } 21 | 22 | one() { } 23 | } 24 | 25 | class TwoAction { 26 | constructor() { 27 | this.bindListeners({ 28 | two: Actions.TWO 29 | }) 30 | } 31 | 32 | two() { } 33 | } 34 | 35 | class BindActions { 36 | constructor() { 37 | this.bindActions(Actions) 38 | } 39 | 40 | one() { } 41 | two() { } 42 | } 43 | 44 | 45 | export default { 46 | 'Exporting listener names': { 47 | 'when no actions are listened on'() { 48 | const myStore = alt.createStore(NoActions) 49 | assert(myStore.boundListeners.length === 0, 'none are returned') 50 | }, 51 | 52 | 'when using bindAction'() { 53 | const myStore = alt.createStore(OneAction) 54 | assert(myStore.boundListeners.length === 1) 55 | assert(myStore.boundListeners[0] === Actions.ONE) 56 | }, 57 | 58 | 'when using bindListeners'() { 59 | const myStore = alt.createStore(TwoAction) 60 | assert(myStore.boundListeners.length === 1) 61 | assert(myStore.boundListeners[0] === Actions.TWO) 62 | }, 63 | 64 | 'when using bindActions'() { 65 | const myStore = alt.createStore(BindActions) 66 | assert(myStore.boundListeners.length === 2) 67 | assert( 68 | myStore.boundListeners.indexOf(Actions.ONE) > -1 && 69 | myStore.boundListeners.indexOf(Actions.TWO) > -1 70 | ) 71 | }, 72 | 73 | 'dispatching actions'() { 74 | const alt = new Alt() 75 | 76 | const one = alt.generateActions('one') 77 | const two = alt.generateActions('one') 78 | 79 | const store = alt.createStore(function Store() { 80 | this.bindAction(one.one, function (x) { 81 | assert(x === 1) 82 | }) 83 | this.bindAction(two.one, function (x) { 84 | assert(x === 2) 85 | }) 86 | }) 87 | 88 | alt.dispatch('global.one', 1) 89 | alt.dispatch('global.one1', 2) 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 8 | 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | const assign = require('object-assign') 2 | 3 | const tests = assign( 4 | {}, 5 | require('../index'), 6 | require('../listen-to-actions'), 7 | require('../final-store'), 8 | require('../recorder'), 9 | require('../setting-state'), 10 | require('../stores-get-alt'), 11 | require('../stores-with-colliding-names'), 12 | require('../testing-utils'), 13 | require('../use-event-emitter'), 14 | require('../store-as-a-module'), 15 | require('../es3-module-pattern') 16 | ) 17 | 18 | // This code is directly from mocha/lib/interfaces/exports.js 19 | // with a few modifications 20 | function manualExports(exports, suite) { 21 | var suites = [suite] 22 | 23 | visit(exports) 24 | 25 | function visit(obj) { 26 | var suite 27 | for (var key in obj) { 28 | if ('function' == typeof obj[key]) { 29 | var fn = obj[key] 30 | switch (key) { 31 | case 'before': 32 | suites[0].beforeAll(fn) 33 | break 34 | case 'after': 35 | suites[0].afterAll(fn) 36 | break 37 | case 'beforeEach': 38 | suites[0].beforeEach(fn) 39 | break 40 | case 'afterEach': 41 | suites[0].afterEach(fn) 42 | break 43 | default: 44 | suites[0].addTest(new Mocha.Test(key, fn)) 45 | } 46 | } else { 47 | var suite = Mocha.Suite.create(suites[0], key) 48 | suites.unshift(suite) 49 | visit(obj[key]) 50 | suites.shift() 51 | } 52 | } 53 | } 54 | } 55 | 56 | manualExports(tests, mocha.suite) 57 | 58 | mocha.setup('exports') 59 | mocha.checkLeaks() 60 | mocha.run() 61 | -------------------------------------------------------------------------------- /test/config-set-get-state-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../' 2 | import { assert } from 'chai' 3 | import sinon from 'sinon' 4 | 5 | 6 | export default { 7 | 'Config state getter and setter': { 8 | 'setting state'() { 9 | const setState = sinon.stub().returns({ 10 | foo: 'bar' 11 | }) 12 | 13 | const alt = new Alt({ setState }) 14 | 15 | const action = alt.generateActions('fire') 16 | 17 | const store = alt.createStore({ 18 | displayName: 'store', 19 | bindListeners: { 20 | fire: action.fire 21 | }, 22 | state: { x: 1 }, 23 | fire() { 24 | this.setState({ x: 2 }) 25 | } 26 | }) 27 | 28 | assert(store.getState().x === 1) 29 | 30 | action.fire() 31 | 32 | assert.ok(setState.calledOnce) 33 | assert(setState.args[0].length === 2) 34 | assert(store.getState().foo === 'bar') 35 | }, 36 | 37 | 'getting state'() { 38 | const getState = sinon.stub().returns({ 39 | foo: 'bar' 40 | }) 41 | 42 | const alt = new Alt({ getState }) 43 | 44 | const store = alt.createStore({ 45 | displayName: 'store', 46 | state: { x: 1 } 47 | }) 48 | 49 | assert.isUndefined(store.getState().x) 50 | 51 | assert.ok(getState.calledOnce) 52 | assert(getState.args[0].length === 1) 53 | assert(store.getState().foo === 'bar') 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/debug-alt-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../' 2 | import { assert } from 'chai' 3 | 4 | export default { 5 | 'debug mode': { 6 | beforeEach() { 7 | global.window = {} 8 | }, 9 | 10 | 'enable debug mode'() { 11 | const alt = new Alt() 12 | Alt.debug('an identifier', alt) 13 | 14 | assert.isArray(global.window['alt.js.org']) 15 | assert(global.window['alt.js.org'].length === 1) 16 | assert.isString(global.window['alt.js.org'][0].name) 17 | assert(global.window['alt.js.org'][0].alt === alt) 18 | }, 19 | 20 | afterEach() { 21 | delete global.window 22 | } 23 | }, 24 | 25 | 'isomorphic debug mode': { 26 | 'enable debug mode does not make things explode'() { 27 | const alt = new Alt() 28 | Alt.debug(alt) 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/es3-module-pattern.js: -------------------------------------------------------------------------------- 1 | import Alt from '../dist/alt-with-runtime' 2 | import { assert } from 'chai' 3 | import sinon from 'sinon' 4 | 5 | const alt = new Alt() 6 | 7 | const actions = alt.generateActions('fire') 8 | 9 | function MyStore() { 10 | var privateVariable = true 11 | 12 | return { 13 | displayName: 'MyStore', 14 | 15 | state: { 16 | data: 1 17 | }, 18 | 19 | publicMethods: { 20 | getData: function () { 21 | return this.getState().data 22 | } 23 | }, 24 | 25 | testProperty: 4, 26 | 27 | bindListeners: { 28 | handleFire: actions.FIRE 29 | }, 30 | 31 | handleFire: function (data) { 32 | this.setState({ data }) 33 | } 34 | } 35 | } 36 | 37 | const myStore = alt.createStore(MyStore()) 38 | 39 | export default { 40 | 'Creating store using ES3 module pattern': { 41 | beforeEach() { 42 | alt.recycle() 43 | console.warn = function () { } 44 | }, 45 | 46 | 'store method exists'() { 47 | const storePrototype = Object.getPrototypeOf(myStore) 48 | const assertMethods = ['constructor', 'listen', 'unlisten', 'getState'] 49 | assert.deepEqual(Object.getOwnPropertyNames(storePrototype), assertMethods, 'methods exist for store') 50 | assert.isUndefined(myStore.addListener, 'event emitter methods not present') 51 | assert.isUndefined(myStore.removeListener, 'event emitter methods not present') 52 | assert.isUndefined(myStore.emit, 'event emitter methods not present') 53 | }, 54 | 55 | 'public methods available'() { 56 | assert.isFunction(myStore.getData, 'public methods are available') 57 | assert(myStore.getData() === 1, 'initial store data is set') 58 | }, 59 | 60 | 'private and instance variables are not available'() { 61 | assert.isUndefined(myStore.privateVariable, 'encapsulated variables are not available') 62 | assert.isUndefined(myStore.testProperty, 'instance variables are not available') 63 | }, 64 | 65 | 'firing an action'() { 66 | actions.fire(2) 67 | 68 | assert(myStore.getState().data === 2, 'action was fired and handled correctly') 69 | }, 70 | 71 | 'adding lifecycle events'() { 72 | let spy = sinon.spy() 73 | 74 | class TestStore { 75 | constructor() { 76 | this.lifecycle = { init: spy } 77 | 78 | this.state = { 79 | foo: 'bar' 80 | } 81 | } 82 | } 83 | 84 | const store = alt.createStore(new TestStore()) 85 | 86 | assert.ok(spy.calledOnce, 'lifecycle event was called') 87 | assert(store.getState().foo === 'bar', 'state is set') 88 | }, 89 | 90 | 'set state'() { 91 | const TestStore = { 92 | state: { hello: null }, 93 | 94 | bindListeners: { 95 | handleFire: actions.FIRE 96 | }, 97 | 98 | handleFire(data) { 99 | this.setState({ 100 | hello: data 101 | }) 102 | 103 | this.setState() 104 | } 105 | } 106 | 107 | const store = alt.createStore(TestStore) 108 | assert.isNull(store.getState().hello, 'store state property has not been set yet') 109 | 110 | actions.fire('world') 111 | 112 | assert(store.getState().hello === 'world', 'store state was set using setState') 113 | }, 114 | 115 | 'set state in lifecycle'() { 116 | const TestStore = { 117 | state: { test: null }, 118 | 119 | lifecycle: { 120 | init() { 121 | this.state.test = 'i am here' 122 | } 123 | } 124 | } 125 | 126 | const store = alt.createStore(TestStore) 127 | assert(store.getState().test === 'i am here', 'setting state on lifecycle') 128 | }, 129 | 130 | 'get instance works'() { 131 | const TestStore = { 132 | state: { test: null }, 133 | bindListeners: { 134 | handleFire: actions.FIRE 135 | }, 136 | handleFire() { 137 | this.setState({ test: this.getInstance() }) 138 | } 139 | } 140 | 141 | const store = alt.createStore(TestStore) 142 | actions.fire() 143 | assert(store.getState().test === store) 144 | }, 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /test/failed-dispatch-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | import sinon from 'sinon' 4 | 5 | export default { 6 | 'catch failed dispatches': { 7 | 'uncaught dispatches result in an error'() { 8 | const alt = new Alt() 9 | const actions = alt.generateActions('fire') 10 | 11 | class Uncaught { 12 | constructor() { 13 | this.bindListeners({ fire: actions.FIRE }) 14 | } 15 | 16 | fire() { 17 | throw new Error('oops') 18 | } 19 | } 20 | 21 | const uncaught = alt.createStore(Uncaught) 22 | 23 | assert.throws(() => actions.fire()) 24 | }, 25 | 26 | 'errors can be caught though'() { 27 | const alt = new Alt() 28 | const actions = alt.generateActions('fire') 29 | 30 | class Caught { 31 | constructor() { 32 | this.x = 0 33 | this.bindListeners({ fire: actions.FIRE }) 34 | 35 | this.on('error', () => { 36 | this.x = 1 37 | }) 38 | } 39 | 40 | fire() { 41 | throw new Error('oops') 42 | } 43 | } 44 | 45 | const caught = alt.createStore(Caught) 46 | 47 | const storeListener = sinon.spy() 48 | 49 | caught.listen(storeListener) 50 | 51 | assert(caught.getState().x === 0) 52 | assert.doesNotThrow(() => actions.fire()) 53 | assert(caught.getState().x === 1) 54 | 55 | assert.notOk(storeListener.calledOnce, 'the store did not emit a change') 56 | 57 | caught.unlisten(storeListener) 58 | }, 59 | 60 | 'you have to emit changes yourself'() { 61 | const alt = new Alt() 62 | const actions = alt.generateActions('fire') 63 | 64 | class CaughtReturn { 65 | constructor() { 66 | this.x = 0 67 | this.bindListeners({ fire: actions.FIRE }) 68 | 69 | this.on('error', () => { 70 | this.x = 1 71 | this.emitChange() 72 | }) 73 | } 74 | 75 | fire() { 76 | throw new Error('oops') 77 | } 78 | } 79 | 80 | const caughtReturn = alt.createStore(CaughtReturn) 81 | 82 | const storeListener = sinon.spy() 83 | 84 | const dispose = caughtReturn.listen(storeListener) 85 | 86 | assert(caughtReturn.getState().x === 0) 87 | assert.doesNotThrow(() => actions.fire()) 88 | assert(caughtReturn.getState().x === 1) 89 | 90 | assert.ok(storeListener.calledOnce) 91 | 92 | dispose() 93 | }, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/functional-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | import sinon from 'sinon' 4 | 5 | export default { 6 | 'functional goodies for alt': { 7 | 'observing for changes in a POJO so we get context passed in'() { 8 | const alt = new Alt() 9 | 10 | const observe = sinon.stub().returns({}) 11 | const displayName = 'store' 12 | 13 | alt.createStore({ displayName, observe }) 14 | 15 | assert.ok(observe.calledOnce) 16 | assert(observe.args[0][0] === alt, 'first arg is alt') 17 | }, 18 | 19 | 'when observing changes, they are observed'() { 20 | const alt = new Alt() 21 | const actions = alt.generateActions('fire') 22 | 23 | const displayName = 'store' 24 | 25 | const store = alt.createStore({ 26 | displayName, 27 | observe() { 28 | return { fire: actions.fire } 29 | }, 30 | fire() { } 31 | }) 32 | 33 | assert(store.boundListeners.length === 1, 'there is 1 action bound') 34 | }, 35 | 36 | 'otherwise works like a haskell guard'() { 37 | const alt = new Alt() 38 | const actions = alt.generateActions('fire', 'test') 39 | 40 | const spy = sinon.spy() 41 | 42 | const store = alt.createStore({ 43 | displayName: 'store', 44 | state: { x: 0 }, 45 | bindListeners: { 46 | fire: actions.fire 47 | }, 48 | 49 | fire() { 50 | this.setState({ x: 1 }) 51 | }, 52 | 53 | otherwise() { 54 | this.setState({ x: 2 }) 55 | } 56 | }) 57 | 58 | const kill = store.listen(spy) 59 | 60 | actions.test() 61 | assert(store.getState().x === 2, 'the otherwise clause was ran') 62 | 63 | actions.fire() 64 | assert(store.getState().x === 1, 'just fire was ran') 65 | 66 | assert.ok(spy.calledTwice) 67 | 68 | kill() 69 | }, 70 | 71 | 'preventDefault prevents a change event to be emitted'() { 72 | const alt = new Alt() 73 | const actions = alt.generateActions('fire') 74 | 75 | const spy = sinon.spy() 76 | 77 | const store = alt.createStore({ 78 | displayName: 'store', 79 | state: { x: 0 }, 80 | bindListeners: { 81 | fire: actions.fire 82 | }, 83 | 84 | fire() { 85 | this.setState({ x: 1 }) 86 | this.preventDefault() 87 | } 88 | }) 89 | 90 | const kill = store.listen(spy) 91 | 92 | actions.fire() 93 | assert(store.getState().x === 1, 'just fire was ran') 94 | 95 | assert(spy.callCount === 0, 'store listener was never called') 96 | 97 | kill() 98 | }, 99 | 100 | 'reduce fires on every dispatch if defined'() { 101 | const alt = new Alt() 102 | const actions = alt.generateActions('fire') 103 | 104 | const store = alt.createStore({ 105 | displayName: 'store', 106 | 107 | state: { x: 0 }, 108 | 109 | reduce(state) { 110 | if (state.x >= 3) return 111 | return { x: state.x + 1 } 112 | } 113 | }) 114 | 115 | actions.fire() 116 | actions.fire() 117 | actions.fire() 118 | actions.fire() 119 | 120 | assert(store.getState().x === 3, 'counter was incremented') 121 | }, 122 | 123 | 'reduce doesnt emit if preventDefault'() { 124 | const alt = new Alt() 125 | const actions = alt.generateActions('fire') 126 | 127 | const store = alt.createStore({ 128 | displayName: 'store', 129 | 130 | state: { x: 0 }, 131 | 132 | reduce(state) { 133 | this.preventDefault() 134 | return {} 135 | } 136 | }) 137 | 138 | const spy = sinon.spy() 139 | 140 | const unsub = store.listen(spy) 141 | 142 | actions.fire() 143 | 144 | assert(spy.callCount === 0) 145 | 146 | unsub() 147 | }, 148 | 149 | 'stores have a reduce method'() { 150 | const alt = new Alt() 151 | 152 | const store = alt.createStore({ 153 | displayName: 'store', 154 | 155 | state: { x: 0 }, 156 | 157 | reduce(state) { 158 | return state 159 | } 160 | }) 161 | 162 | const store2 = alt.createStore({ 163 | displayName: 'store2', 164 | 165 | state: { x: 1 }, 166 | }) 167 | 168 | assert.isFunction(store.reduce) 169 | assert.isFunction(store2.reduce) 170 | 171 | assert(store.reduce(store.state).x === 0) 172 | assert(store2.reduce(store2.state).x === 1) 173 | }, 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/functions-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as fn from '../lib/functions' 3 | import Alt from '../dist/alt-with-runtime' 4 | 5 | const alt = new Alt() 6 | 7 | export default { 8 | 'test the functions.js isMutableObject()': { 9 | 'can import lib/functions'() { 10 | assert.ok(fn) 11 | assert.ok(fn.isMutableObject) 12 | assert(typeof fn.isMutableObject === 'function', 'isMutableObject is imported') 13 | }, 14 | 15 | 'isMutableObject works on regular objects'() { 16 | const obj = {} 17 | const obj2 = {foo: 'bar'} 18 | 19 | assert(fn.isMutableObject(obj) === true, 'regular object should pass') 20 | assert(fn.isMutableObject(obj2) === true, 'regular object with fields should pass') 21 | }, 22 | 23 | 'isMutableObject fails on non-objects'() { 24 | assert(fn.isMutableObject(false) === false, 'boolean should fail') 25 | assert(fn.isMutableObject(1) === false, 'integer should fail') 26 | assert(fn.isMutableObject(new Date()) === false, 'date should fail') 27 | }, 28 | 29 | 'isMutableObject works on frozen objects'() { 30 | const obj = {} 31 | Object.freeze(obj) 32 | 33 | assert(fn.isMutableObject(obj) === false, 'frozen objects should fail') 34 | }, 35 | }, 36 | 37 | 'can bootstrap a store with/without frozen state': { 38 | 'normal store state can be bootstrapped'() { 39 | class NonFrozenStore { 40 | constructor() { 41 | this.state = { 42 | foo: 'bar', 43 | } 44 | 45 | assert(this.state.foo === 'bar', 'State is initialized') 46 | } 47 | } 48 | 49 | alt.createStore(NonFrozenStore, 'NonFrozenStore', alt) 50 | alt.bootstrap('{"NonFrozenStore": {"foo":"bar2"}}') 51 | 52 | const myStore = alt.getStore('NonFrozenStore') 53 | assert(myStore.getState().foo === 'bar2', 'State was bootstrapped with updated bar') 54 | }, 55 | 56 | 'frozen store state can be bootstrapped'() { 57 | class FrozenStateStore { 58 | constructor() { 59 | this.config = { 60 | onDeserialize: (data) => { 61 | Object.freeze(data) 62 | return data 63 | }, 64 | } 65 | 66 | this.state = { 67 | foo: 'bar', 68 | } 69 | 70 | Object.freeze(this.state) 71 | 72 | assert(this.state.foo === 'bar', 'State is initialized') 73 | } 74 | } 75 | 76 | alt.createStore(FrozenStateStore, 'FrozenStateStore', alt) 77 | alt.bootstrap('{"FrozenStateStore": {"foo":"bar2"}}') 78 | 79 | const myStore = alt.getStore('FrozenStateStore') 80 | assert(myStore.getState().foo === 'bar2', 'State was bootstrapped with updated bar') 81 | }, 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /test/helpers/SaaM.js: -------------------------------------------------------------------------------- 1 | export const displayName = 'SaaM' 2 | 3 | export const state = 1 4 | 5 | export function reduce(state, payload) { 6 | return state + 1 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/SampleActions.js: -------------------------------------------------------------------------------- 1 | import alt from './alt' 2 | 3 | export default alt.generateActions('fire') 4 | -------------------------------------------------------------------------------- /test/helpers/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from '../../dist/alt-with-runtime' 2 | export default new Alt() 3 | -------------------------------------------------------------------------------- /test/setting-state.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | import sinon from 'sinon' 4 | 5 | const alt = new Alt() 6 | 7 | const actions = alt.generateActions('fire', 'nothing') 8 | 9 | class MyStore { 10 | constructor() { 11 | this.foo = 1 12 | this.bindListeners({ increment: actions.FIRE, nothing: actions.NOTHING }) 13 | } 14 | 15 | increment() { 16 | this.retVal = this.setState({ foo: this.foo + 1 }) 17 | return this.retVal 18 | } 19 | 20 | nothing() { 21 | this.setState() 22 | } 23 | } 24 | 25 | const myStore = alt.createStore(MyStore) 26 | 27 | export default { 28 | 'setState': { 29 | beforeEach() { 30 | alt.recycle() 31 | }, 32 | 33 | 'using setState to set the state'() { 34 | const spy = sinon.spy() 35 | const dispose = myStore.listen(spy) 36 | 37 | actions.fire() 38 | 39 | assert(myStore.getState().foo === 2, 'foo was incremented') 40 | assert.isUndefined(myStore.getState().retVal, 'return value of setState is undefined') 41 | 42 | dispose() 43 | 44 | // calling set state without anything doesn't make things crash and burn 45 | actions.nothing() 46 | 47 | assert.ok(spy.calledOnce, 'spy was only called once') 48 | }, 49 | 50 | 'by using setState a change event is not emitted twice'() { 51 | const spy = sinon.spy() 52 | const dispose = myStore.listen(spy) 53 | 54 | actions.nothing() 55 | 56 | assert(myStore.getState().foo === 1, 'foo remains the same') 57 | 58 | assert.ok(spy.calledOnce, 'spy was only called once') 59 | 60 | dispose() 61 | }, 62 | 63 | 'transactional setState'() { 64 | const alt = new Alt() 65 | 66 | const actions = alt.generateActions('fire') 67 | class SetState { 68 | constructor() { 69 | this.bindActions(actions) 70 | this.x = 0 71 | } 72 | 73 | fire() { 74 | this.setState(() => { 75 | return { 76 | x: 1 77 | } 78 | }) 79 | } 80 | } 81 | 82 | const store = alt.createStore(SetState) 83 | 84 | assert(store.getState().x === 0, 'x is initially 0') 85 | actions.fire() 86 | assert(store.getState().x === 1, 'x is 1') 87 | }, 88 | 89 | 'transactional setState with failure'() { 90 | const alt = new Alt() 91 | 92 | const actions = alt.generateActions('fire') 93 | class SetState { 94 | constructor() { 95 | this.bindActions(actions) 96 | this.x = 0 97 | } 98 | 99 | fire() { 100 | this.setState(() => { 101 | throw new Error('error') 102 | }) 103 | } 104 | } 105 | 106 | const store = alt.createStore(SetState) 107 | 108 | assert(store.getState().x === 0, 'x is initially 0') 109 | assert.throws(() => actions.fire()) 110 | assert(store.getState().x === 0, 'x remains 0') 111 | }, 112 | 113 | 'setState no dispatch'() { 114 | const alt = new Alt() 115 | 116 | const actions = alt.generateActions('fire') 117 | class BrokenSetState { 118 | constructor() { 119 | this.x = 0 120 | this.setState({ x: 1 }) 121 | } 122 | } 123 | 124 | assert.throws(() => { 125 | alt.createStore(BrokenSetState) 126 | }) 127 | }, 128 | 129 | 'state is set not replaced'() { 130 | const alt = new Alt() 131 | 132 | const actions = alt.generateActions('fire') 133 | class SetState { 134 | constructor() { 135 | this.bindActions(actions) 136 | this.x = 0 137 | this.y = 0 138 | } 139 | 140 | fire() { 141 | this.setState({ x: 1 }) 142 | } 143 | } 144 | const store = alt.createStore(SetState) 145 | 146 | assert(store.getState().x === 0, 'x is initially 0') 147 | actions.fire() 148 | assert(store.getState().x === 1, 'x is now 1') 149 | assert(store.getState().y === 0, 'y was untouched') 150 | }, 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/store-as-a-module.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import Alt from '../' 3 | 4 | import * as StoreModel from './helpers/SaaM' 5 | 6 | const alt = new Alt() 7 | const actions = alt.generateActions('increment') 8 | const store = alt.createStore(StoreModel) 9 | 10 | export default { 11 | 'Stores as a Module': { 12 | 'store state is there'() { 13 | assert.equal(store.getState(), 1, 'store data is initialized to 1') 14 | 15 | actions.increment() 16 | 17 | assert.equal(store.getState(), 2, 'store data was updated') 18 | 19 | actions.increment() 20 | 21 | assert.equal(store.getState(), 3, 'incremented again') 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/store-model-test.js: -------------------------------------------------------------------------------- 1 | import Alt from '../dist/alt-with-runtime' 2 | import { assert } from 'chai' 3 | 4 | const alt = new Alt() 5 | const Actions = alt.generateActions('hello') 6 | 7 | function MyStoreModel() { 8 | this.bindActions(Actions) 9 | 10 | this.test = 2 11 | } 12 | MyStoreModel.prototype.onHello = function () { this.test = 1 } 13 | 14 | const MyStoreModelObj = { 15 | displayName: 'MyStoreAsObject', 16 | 17 | state: { test: 2 }, 18 | 19 | bindListeners: { 20 | onHello: Actions.HELLO 21 | }, 22 | 23 | onHello: function () { 24 | this.state.test = 1 25 | } 26 | } 27 | 28 | export default { 29 | 'Exposing the StoreModel': { 30 | beforeEach() { 31 | alt.recycle() 32 | }, 33 | 34 | 'as an object'() { 35 | const MyStore = alt.createStore(MyStoreModelObj) 36 | 37 | assert(MyStore.getState().test === 2, 'store state is initially set') 38 | 39 | assert.isDefined(MyStore.StoreModel, 'store model is available') 40 | assert.isObject(MyStore.StoreModel, 'store model is an object') 41 | 42 | assert(MyStore.StoreModel === MyStoreModelObj, 'the store model is the same as the original object') 43 | 44 | Actions.hello() 45 | 46 | assert(MyStore.getState().test === 1, 'i can change state through actions') 47 | }, 48 | 49 | 'as a class'() { 50 | const MyStore = alt.createStore(MyStoreModel, 'MyStore') 51 | 52 | assert(MyStore.getState().test === 2, 'store state is initially set') 53 | 54 | assert.isDefined(MyStore.StoreModel, 'store model is available') 55 | assert.isFunction(MyStore.StoreModel, 'store model is a function') 56 | 57 | assert(MyStore.StoreModel === MyStoreModel, 'the store model is the same as the original object') 58 | 59 | Actions.hello() 60 | 61 | assert(MyStore.getState().test === 1, 'i can change state through actions') 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/store-transforms-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | 4 | const alt = new Alt() 5 | 6 | alt.storeTransforms.push(function (Store) { 7 | Store.test = 'hello' 8 | return Store 9 | }) 10 | 11 | class Store { 12 | constructor() { 13 | this.x = 0 14 | } 15 | } 16 | 17 | class Store2 { 18 | constructor() { 19 | this.y = 0 20 | } 21 | } 22 | 23 | export default { 24 | 'store transforms': { 25 | 'when creating stores alt goes through its series of transforms'() { 26 | const store = alt.createStore(Store) 27 | assert(alt.storeTransforms.length === 1) 28 | assert.isDefined(store.test) 29 | assert(store.test === 'hello', 'store that adds hello to instance transform') 30 | }, 31 | 32 | 'unsaved stores get the same treatment'() { 33 | const store2 = alt.createUnsavedStore(Store) 34 | assert.isDefined(store2.test) 35 | }, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /test/stores-get-alt.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../dist/alt-with-runtime' 3 | 4 | const alt = new Alt() 5 | 6 | export default { 7 | 'the stores get the alt instance'() { 8 | class MyStore { 9 | constructor(alt) { 10 | assert.instanceOf(alt, Alt, 'alt is an instance of Alt') 11 | } 12 | } 13 | 14 | alt.createStore(MyStore, 'MyStore', alt) 15 | }, 16 | 17 | 'the actions get the alt instance'() { 18 | class MyActions { 19 | constructor(alt) { 20 | assert.instanceOf(alt, Alt, 'alt is an instance of Alt') 21 | } 22 | } 23 | 24 | alt.createActions(MyActions, undefined, alt) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/stores-with-colliding-names.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import sinon from 'sinon' 3 | import Alt from '../dist/alt-with-runtime' 4 | 5 | const alt = new Alt() 6 | 7 | alt.createStore(function MyStore() { }) 8 | 9 | export default { 10 | 'console warn for missing identifier': { 11 | beforeEach() { 12 | console.warn = sinon.stub() 13 | console.warn.returnsArg(0) 14 | }, 15 | 16 | 'stores with colliding names'() { 17 | const MyStore = (function () { 18 | return function MyStore() { } 19 | }()) 20 | alt.createStore(MyStore) 21 | 22 | assert.isObject(alt.stores.MyStore1, 'a store was still created') 23 | 24 | }, 25 | 26 | 'colliding names via identifier'() { 27 | class auniquestore { } 28 | alt.createStore(auniquestore, 'MyStore') 29 | 30 | assert.isObject(alt.stores.MyStore1, 'a store was still created') 31 | }, 32 | 33 | 'not providing a store name via anonymous function'() { 34 | alt.createStore(function () { }) 35 | 36 | assert.isObject(alt.stores[''], 'a store with no name was still created') 37 | }, 38 | 39 | afterEach() { 40 | assert.ok(console.warn.calledOnce, 'the warning was called') 41 | assert.instanceOf(console.warn.returnValues[0], ReferenceError, 'value returned is an instanceof referenceerror') 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/value-stores-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Alt from '../' 3 | import sinon from 'sinon' 4 | 5 | const alt = new Alt() 6 | 7 | const actions = alt.generateActions('fire') 8 | 9 | const store = alt.createStore({ 10 | state: 21, 11 | 12 | displayName: 'ValueStore', 13 | 14 | reduce(state, payload) { 15 | return state + 1 16 | } 17 | }) 18 | 19 | const store2 = alt.createStore({ 20 | state: [1, 2, 3], 21 | 22 | displayName: 'Value2Store', 23 | 24 | reduce(state, payload) { 25 | return state.concat(state[state.length - 1] + 1) 26 | } 27 | }) 28 | 29 | const store3 = alt.createStore({ 30 | state: 21, 31 | 32 | displayName: 'ValueStore3', 33 | 34 | bindListeners: { 35 | fire: actions.fire 36 | }, 37 | 38 | fire() { 39 | this.setState(this.state + 1) 40 | } 41 | }) 42 | 43 | export default { 44 | 'value stores': { 45 | beforeEach() { 46 | alt.recycle() 47 | }, 48 | 49 | 'stores can contain state as any value'(done) { 50 | assert(store.state === 21, 'store state is value') 51 | assert(store.getState() === 21, 'getState returns value too') 52 | 53 | const unlisten = store.listen((state) => { 54 | assert(state === 22, 'incremented store state') 55 | unlisten() 56 | done() 57 | }) 58 | 59 | assert(JSON.parse(alt.takeSnapshot()).ValueStore === 21, 'snapshot ok') 60 | 61 | actions.fire() 62 | }, 63 | 64 | 'stores can contain state as any value (non reduce)'(done) { 65 | assert(store3.state === 21, 'store state is value') 66 | assert(store3.getState() === 21, 'getState returns value too') 67 | 68 | const unlisten = store3.listen((state) => { 69 | assert(state === 22, 'incremented store state') 70 | unlisten() 71 | done() 72 | }) 73 | 74 | assert(JSON.parse(alt.takeSnapshot()).ValueStore3 === 21, 'snapshot ok') 75 | 76 | actions.fire() 77 | }, 78 | 79 | 'store with array works too'() { 80 | assert.deepEqual(store2.state, [1, 2, 3]) 81 | actions.fire() 82 | assert.deepEqual(store2.state, [1, 2, 3, 4]) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /typings/alt/alt-tests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by shearerbeard on 6/28/15. 3 | */ 4 | /// 5 | /// 6 | 7 | import Alt = require("alt"); 8 | import Promise = require("es6-promise"); 9 | 10 | //New alt instance 11 | var alt = new Alt(); 12 | 13 | //Interfaces for our Action Types 14 | interface TestActionsGenerate { 15 | notifyTest(str:string):void; 16 | } 17 | 18 | interface TestActionsExplicit { 19 | doTest(str:string):void; 20 | success():void; 21 | error():void; 22 | loading():void; 23 | } 24 | 25 | //Create abstracts to inherit ghost methods 26 | class AbstractActions implements AltJS.ActionsClass { 27 | constructor( alt:AltJS.Alt){} 28 | actions:any; 29 | dispatch: ( ...payload:Array) => void; 30 | generateActions:( ...actions:Array) => void; 31 | } 32 | 33 | class AbstractStoreModel implements AltJS.StoreModel { 34 | bindActions:( ...actions:Array) => void; 35 | bindAction:( ...args:Array) => void; 36 | bindListeners:(obj:any)=> void; 37 | exportPublicMethods:(config:{[key:string]:(...args:Array) => any}) => any; 38 | exportAsync:( source:any) => void; 39 | waitFor:any; 40 | exportConfig:any; 41 | getState:() => S; 42 | } 43 | 44 | class GenerateActionsClass extends AbstractActions { 45 | constructor(config:AltJS.Alt) { 46 | this.generateActions("notifyTest"); 47 | super(config); 48 | } 49 | } 50 | 51 | class ExplicitActionsClass extends AbstractActions { 52 | doTest(str:string) { 53 | this.dispatch(str); 54 | } 55 | success() { 56 | this.dispatch(); 57 | } 58 | error() { 59 | this.dispatch(); 60 | } 61 | loading() { 62 | this.dispatch(); 63 | } 64 | } 65 | 66 | var generatedActions = alt.createActions(GenerateActionsClass); 67 | var explicitActions = alt.createActions(ExplicitActionsClass); 68 | 69 | interface AltTestState { 70 | hello:string; 71 | } 72 | 73 | var testSource:AltJS.Source = { 74 | fakeLoad():AltJS.SourceModel { 75 | return { 76 | remote() { 77 | return new Promise.Promise((res:any, rej:any) => { 78 | setTimeout(() => { 79 | if(true) { 80 | res("stuff"); 81 | } else { 82 | rej("Things have broken"); 83 | } 84 | }, 250) 85 | }); 86 | }, 87 | local() { 88 | return "local"; 89 | }, 90 | success: explicitActions.success, 91 | error: explicitActions.error, 92 | loading:explicitActions.loading 93 | }; 94 | } 95 | }; 96 | 97 | class TestStore extends AbstractStoreModel implements AltTestState { 98 | hello:string = "world"; 99 | constructor() { 100 | super(); 101 | this.bindAction(generatedActions.notifyTest, this.onTest); 102 | this.bindActions(explicitActions); 103 | this.exportAsync(testSource); 104 | this.exportPublicMethods({ 105 | split: this.split 106 | }); 107 | } 108 | onTest(str:string) { 109 | this.hello = str; 110 | } 111 | 112 | onDoTest(str:string) { 113 | this.hello = str; 114 | } 115 | 116 | split():string[] { 117 | return this.hello.split(""); 118 | } 119 | } 120 | 121 | interface ExtendedTestStore extends AltJS.AltStore { 122 | fakeLoad():string; 123 | split():Array; 124 | } 125 | 126 | var testStore = alt.createStore(TestStore); 127 | 128 | function testCallback(state:AltTestState) { 129 | console.log(state); 130 | } 131 | 132 | //Listen allows a typed state callback 133 | testStore.listen(testCallback); 134 | testStore.unlisten(testCallback); 135 | 136 | //State generic passes to derived store 137 | var name:string = testStore.getState().hello; 138 | var nameChars:Array = testStore.split(); 139 | 140 | generatedActions.notifyTest("types"); 141 | explicitActions.doTest("more types"); 142 | 143 | export var result = testStore.getState(); 144 | -------------------------------------------------------------------------------- /typings/alt/alt.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Alt 0.16.10 2 | // Project: https://github.com/goatslacker/alt 3 | // Definitions by: Michael Shearer 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | /// 8 | 9 | declare module AltJS { 10 | 11 | interface StoreReduce { 12 | action:any; 13 | data: any; 14 | } 15 | 16 | export interface StoreModel { 17 | //Actions 18 | bindAction?( action:Action, handler:ActionHandler):void; 19 | bindActions?(actions:ActionsClass):void; 20 | 21 | //Methods/Listeners 22 | exportPublicMethods?(exportConfig:any):void; 23 | bindListeners?(config:{[methodName:string]:Action | Actions}):void; 24 | exportAsync?(source:Source):void; 25 | registerAsync?(datasource:Source):void; 26 | 27 | //state 28 | setState?(state:S):void; 29 | setState?(stateFn:(currentState:S, nextState:S) => S):void; 30 | getState?():S; 31 | waitFor?(store:AltStore):void; 32 | 33 | //events 34 | onSerialize?(fn:(data:any) => any):void; 35 | onDeserialize?(fn:(data:any) => any):void; 36 | on?(event:AltJS.lifeCycleEvents, callback:() => any):void; 37 | emitChange?():void; 38 | waitFor?(storeOrStores:AltStore | Array>):void; 39 | otherwise?(data:any, action:AltJS.Action):void; 40 | observe?(alt:Alt):any; 41 | reduce?(state:any, config:StoreReduce):Object; 42 | preventDefault?():void; 43 | afterEach?(payload:Object, state:Object):void; 44 | beforeEach?(payload:Object, state:Object):void; 45 | // TODO: Embed dispatcher interface in def 46 | dispatcher?:any; 47 | 48 | //instance 49 | getInstance?():AltJS.AltStore; 50 | alt?:Alt; 51 | displayName?:string; 52 | } 53 | 54 | export type Source = {[name:string]: () => SourceModel}; 55 | 56 | export interface SourceModel { 57 | local(state:any):any; 58 | remote(state:any):Promise; 59 | shouldFetch?(fetchFn:(...args:Array) => boolean):void; 60 | loading?:(args:any) => void; 61 | success?:(state:S) => void; 62 | error?:(args:any) => void; 63 | interceptResponse?(response:any, action:Action, ...args:Array):any; 64 | } 65 | 66 | export interface AltStore { 67 | getState():S; 68 | listen(handler:(state:S) => any):() => void; 69 | unlisten(handler:(state:S) => any):void; 70 | emitChange():void; 71 | } 72 | 73 | export enum lifeCycleEvents { 74 | bootstrap, 75 | snapshot, 76 | init, 77 | rollback, 78 | error 79 | } 80 | 81 | export type Actions = {[action:string]:Action}; 82 | 83 | export interface Action { 84 | ( args:T):void; 85 | defer(data:any):void; 86 | } 87 | 88 | export interface ActionsClass { 89 | generateActions?( ...action:Array):void; 90 | actions?:Actions; 91 | } 92 | 93 | type StateTransform = (store:StoreModel) => AltJS.AltStore; 94 | 95 | interface AltConfig { 96 | dispatcher?:any; 97 | serialize?:(serializeFn:(data:Object) => string) => void; 98 | deserialize?:(deserializeFn:(serialData:string) => Object) => void; 99 | storeTransforms?:Array; 100 | batchingFunction?:(callback:( ...data:Array) => any) => void; 101 | } 102 | 103 | class Alt { 104 | constructor(config?:AltConfig); 105 | actions:Actions; 106 | bootstrap(jsonData:string):void; 107 | takeSnapshot( ...storeNames:Array):string; 108 | flush():Object; 109 | recycle( ...stores:Array>):void; 110 | rollback():void; 111 | dispatch(action?:AltJS.Action, data?:Object, details?:any):void; 112 | 113 | //Actions methods 114 | addActions(actionsName:string, ActionsClass: ActionsClassConstructor):void; 115 | createActions(ActionsClass: ActionsClassConstructor, exportObj?: Object):T; 116 | createActions(ActionsClass: ActionsClassConstructor, exportObj?: Object, ...constructorArgs:Array):T; 117 | generateActions( ...actions:Array):T; 118 | getActions(actionsName:string):AltJS.Actions; 119 | 120 | //Stores methods 121 | addStore(name:string, store:StoreModel, saveStore?:boolean):void; 122 | createStore(store:StoreModel, name?:string):AltJS.AltStore; 123 | getStore(name:string):AltJS.AltStore; 124 | } 125 | 126 | export interface AltFactory { 127 | new(config?:AltConfig):Alt; 128 | } 129 | 130 | type ActionsClassConstructor = new (alt:Alt) => AltJS.ActionsClass; 131 | 132 | type ActionHandler = ( ...data:Array) => any; 133 | type ExportConfig = {[key:string]:(...args:Array) => any}; 134 | } 135 | 136 | declare module "alt/utils/chromeDebug" { 137 | function chromeDebug(alt:AltJS.Alt):void; 138 | export = chromeDebug; 139 | } 140 | 141 | declare module "alt/AltContainer" { 142 | 143 | import React = require("react"); 144 | 145 | interface ContainerProps { 146 | store?:AltJS.AltStore; 147 | stores?:Array>; 148 | inject?:{[key:string]:any}; 149 | actions?:{[key:string]:Object}; 150 | render?:(...props:Array) => React.ReactElement; 151 | flux?:AltJS.Alt; 152 | transform?:(store:AltJS.AltStore, actions:any) => any; 153 | shouldComponentUpdate?:(props:any) => boolean; 154 | component?:React.Component; 155 | } 156 | 157 | type AltContainer = React.ReactElement; 158 | var AltContainer:React.ComponentClass; 159 | 160 | export = AltContainer; 161 | } 162 | 163 | declare module "alt" { 164 | var alt:AltJS.AltFactory; 165 | export = alt; 166 | } 167 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | .bundle 3 | .ruby-version 4 | _site 5 | docs 6 | guide 7 | guides 8 | -------------------------------------------------------------------------------- /web/CNAME: -------------------------------------------------------------------------------- 1 | alt.js.org 2 | -------------------------------------------------------------------------------- /web/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.1.2' 3 | 4 | gem 'rake' 5 | gem 'jekyll' 6 | gem 'sass' 7 | -------------------------------------------------------------------------------- /web/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | blankslate (2.1.2.4) 5 | celluloid (0.16.0) 6 | timers (~> 4.0.0) 7 | classifier-reborn (2.0.3) 8 | fast-stemmer (~> 1.0) 9 | coffee-script (2.3.0) 10 | coffee-script-source 11 | execjs 12 | coffee-script-source (1.9.1) 13 | colorator (0.1) 14 | execjs (2.4.0) 15 | fast-stemmer (1.0.2) 16 | ffi (1.9.8) 17 | hitimes (1.2.2) 18 | jekyll (2.5.3) 19 | classifier-reborn (~> 2.0) 20 | colorator (~> 0.1) 21 | jekyll-coffeescript (~> 1.0) 22 | jekyll-gist (~> 1.0) 23 | jekyll-paginate (~> 1.0) 24 | jekyll-sass-converter (~> 1.0) 25 | jekyll-watch (~> 1.1) 26 | kramdown (~> 1.3) 27 | liquid (~> 2.6.1) 28 | mercenary (~> 0.3.3) 29 | pygments.rb (~> 0.6.0) 30 | redcarpet (~> 3.1) 31 | safe_yaml (~> 1.0) 32 | toml (~> 0.1.0) 33 | jekyll-coffeescript (1.0.1) 34 | coffee-script (~> 2.2) 35 | jekyll-gist (1.2.1) 36 | jekyll-paginate (1.1.0) 37 | jekyll-sass-converter (1.3.0) 38 | sass (~> 3.2) 39 | jekyll-watch (1.2.1) 40 | listen (~> 2.7) 41 | kramdown (1.6.0) 42 | liquid (2.6.2) 43 | listen (2.9.0) 44 | celluloid (>= 0.15.2) 45 | rb-fsevent (>= 0.9.3) 46 | rb-inotify (>= 0.9) 47 | mercenary (0.3.5) 48 | parslet (1.5.0) 49 | blankslate (~> 2.0) 50 | posix-spawn (0.3.10) 51 | pygments.rb (0.6.2) 52 | posix-spawn (~> 0.3.6) 53 | yajl-ruby (~> 1.2.0) 54 | rake (10.4.2) 55 | rb-fsevent (0.9.4) 56 | rb-inotify (0.9.5) 57 | ffi (>= 0.5.0) 58 | redcarpet (3.2.2) 59 | safe_yaml (1.0.4) 60 | sass (3.4.13) 61 | timers (4.0.1) 62 | hitimes 63 | toml (0.1.2) 64 | parslet (~> 1.5.0) 65 | yajl-ruby (1.2.1) 66 | 67 | PLATFORMS 68 | ruby 69 | 70 | DEPENDENCIES 71 | jekyll 72 | rake 73 | sass 74 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This is the source of the [alt website](github.io/goatslacker/alt), which is generated using [Github Pages](https://pages.github.com/) and [Jekyll](http://jekyllrb.com/). 4 | 5 | The source utilizes the same `docs` folder as the rest of the project, but converts the links from their usable format on github, to work with the website. 6 | 7 | ## Setup 8 | 9 | You must first install the Ruby dependencies specified in the `Gemfile` with [bundler](http://bundler.io/), `bundle install --path .bundle` 10 | 11 | ## Development 12 | 13 | This project uses [Rake](https://github.com/ruby/rake) to run commands to build/deploy the Ruby based Jekyll site. 14 | 15 | To view all the Rake commands available in the `Rakefile` run `bundle exec rake -T`. 16 | 17 | - Generate the Jekyll website (gets written to `_site/`) with `bundle exec rake build`. 18 | - Watch for changes and serve at `localhost:4000/alt/` with `bundle exec rake watch` or just `bundle exec rake` (because it is the default task). 19 | 20 | ## Deploying 21 | 22 | Changes on the website are made directly to the `master` branch like any other alt changes, but they will not be deployed to the site automatically. The maintainers will use the rake deploy task which will commit the latest site changes to the `gh-pages` branch, where github will handle propagating these so they are viewable online. 23 | -------------------------------------------------------------------------------- /web/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'jekyll' 3 | require 'tmpdir' 4 | require 'tempfile' 5 | require 'fileutils' 6 | require 'yaml' 7 | 8 | # Set "rake watch" as default task 9 | task :default => :watch 10 | 11 | GITHUB_REPONAME = 'alt' 12 | GITHUB_BRANCH = 'gh-pages' 13 | GITHUB_USER = 'goatslacker' 14 | 15 | # rake clean 16 | desc 'Serve and watch the site' 17 | task :watch => [:clean] do 18 | system 'jekyll serve --watch' 19 | end 20 | 21 | # rake build 22 | desc 'Build the site' 23 | task :build => [:clean] do 24 | system 'jekyll build' 25 | end 26 | 27 | # rake watch 28 | desc 'Serve and watch the site' 29 | task :clean do 30 | system 'rm -rf _site/' 31 | clean_and_copy_docs 32 | clean_and_copy_guide 33 | end 34 | 35 | 36 | # rake deploy 37 | # rake deploy['commit message'] 38 | desc "Generate and deploy blog to #{GITHUB_BRANCH}" 39 | task :deploy, [:commit_message] => [:build] do |t, args| 40 | commit_message = args[:commit_message] || `git log -1 --pretty=%B` 41 | commit_message = commit_message.gsub('"', "'") 42 | sha = `git log`.match(/[a-z0-9]{40}/)[0] 43 | 44 | Dir.mktmpdir do |tmp| 45 | pwd = Dir.pwd 46 | Dir.chdir tmp 47 | 48 | # setup repo in tmp dir 49 | system 'git init' 50 | system "git remote add origin git@github.com:#{GITHUB_USER}/#{GITHUB_REPONAME}.git" 51 | system "git pull origin #{GITHUB_BRANCH}" 52 | 53 | # ensure that previously generated files that are now deleted do not remain 54 | rm_rf Dir.glob("#{tmp}/*") 55 | # copy latest production site generation 56 | cp_r "#{pwd}/_site/.", tmp 57 | # prevents github from trying to parse our generated content 58 | system 'touch .nojekyll' 59 | 60 | # commit and push 61 | system 'git add .' 62 | system "git commit -m \"#{sha}: #{commit_message}\"" 63 | system "git push origin master:refs/heads/#{GITHUB_BRANCH}" 64 | end 65 | end 66 | 67 | private 68 | 69 | def clean_and_copy_docs 70 | system 'rm -rf docs/' 71 | system 'cp -r ../docs .' 72 | format_markdown_links('docs') 73 | end 74 | 75 | def clean_and_copy_guide 76 | system 'rm -rf guide/' 77 | system 'rm -rf guides/' 78 | system 'mkdir guide' 79 | system 'mkdir guides' 80 | system 'cp -r ../guides/getting-started/* guide' 81 | system 'cp -r ../guides/es5 guides' 82 | format_markdown_links('guide') 83 | format_markdown_links('guides') 84 | end 85 | 86 | def format_markdown_links(dir) 87 | site = YAML.load_file('_config.yml') 88 | 89 | Dir[dir + '/**/*.md'].each do |file| 90 | full_dir = File.dirname(file) 91 | temp_file = Tempfile.new('tmp') 92 | begin 93 | File.open(file, 'r') do |file| 94 | file.each_line do |line| 95 | line.scan(/(?(?\[.+?\])\((?.+?)\.md(?#.+?)?\))/).each do |all, name, link, hash| 96 | line.gsub!(all, "#{name}(/#{full_dir}/#{link}#{hash})") 97 | end 98 | 99 | temp_file.puts line 100 | end 101 | end 102 | temp_file.close 103 | FileUtils.mv(temp_file.path, file) 104 | ensure 105 | temp_file.close 106 | temp_file.unlink 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /web/_config.yml: -------------------------------------------------------------------------------- 1 | # Site settings 2 | title: Alt 3 | description: > # this means to ignore newlines until "baseurl:" 4 | A library for managing data within JavaScript applications. Alt is a pure 5 | flux implementation that is small, terse, well tested, extremely flexible, 6 | and forward thinking. 7 | baseurl: "" # the subpath of your site, e.g. /blog/ 8 | url: "http://alt.js.org" # the base hostname & protocol for your site 9 | twitter_username: goatslacker 10 | github_username: goatslacker/alt 11 | 12 | sidenav: 13 | - title: Creating Actions 14 | url: /docs/createActions 15 | - title: Actions 16 | url: /docs/actions 17 | - title: Creating Stores 18 | url: /docs/createStore 19 | - title: Stores 20 | url: /docs/stores 21 | - title: Handling Async 22 | url: /docs/async 23 | - title: Lifecycle Listeners 24 | url: /docs/lifecycleListeners 25 | - title: Bootstrap 26 | url: /docs/bootstrap 27 | - title: Take Snapshot 28 | url: /docs/takeSnapshot 29 | - title: Flush 30 | url: /docs/flush 31 | - title: Recycle 32 | url: /docs/recycle 33 | - title: Rollback 34 | url: /docs/rollback 35 | - title: Alt Instances 36 | url: /docs/altInstances 37 | - title: AltContainer 38 | url: /docs/components/altContainer 39 | 40 | guidenav: 41 | - title: Getting Started 42 | url: /guide/ 43 | - title: Creating Actions 44 | url: /guide/actions/ 45 | - title: Creating a Store 46 | url: /guide/store/ 47 | - title: Using your View 48 | url: /guide/view/ 49 | - title: Fetching Data 50 | url: /guide/async/ 51 | - title: Data Dependencies 52 | url: /guide/wait-for/ 53 | - title: Container Components 54 | url: /guide/container-components 55 | - title: Using Alt with ES5 56 | url: /guides/es5/ 57 | 58 | guidesnav: 59 | - title: Getting Started 60 | url: /guide/ 61 | - title: Using Alt with ES5 62 | url: /guides/es5/ 63 | 64 | # Build settings 65 | markdown: kramdown 66 | 67 | kramdown: 68 | input: GFM 69 | 70 | exclude: 71 | - Rakefile 72 | - Gemfile 73 | - Gemfile.lock 74 | - README.md 75 | - .npmignore 76 | - .gitignore 77 | - .travis.yml 78 | - .editorconfig 79 | -------------------------------------------------------------------------------- /web/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /web/_includes/guidenav.html: -------------------------------------------------------------------------------- 1 |
    2 | {% for item in site.guidenav %} 3 |
  • 4 | {{ item.title }} 5 |
  • 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /web/_includes/guidesnav.html: -------------------------------------------------------------------------------- 1 |
    2 | {% for item in site.guidesnav %} 3 |
  • 4 | {{ item.title }} 5 |
  • 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /web/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/_includes/header.html: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /web/_includes/sidenav.html: -------------------------------------------------------------------------------- 1 |
    2 | {% for item in site.sidenav %} 3 |
  • 4 | {{ item.title }} 5 |
  • 6 | {% endfor %} 7 |
8 | -------------------------------------------------------------------------------- /web/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 | {{ content }} 12 |
13 | 14 | {% include footer.html %} 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/_layouts/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 |
12 |
13 | {% include sidenav.html %} 14 |
15 |
16 |
17 | {{ content }} 18 |
19 |
20 |
21 |
22 | 23 | {% include footer.html %} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/_layouts/guide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 |
12 |
13 | {% include guidenav.html %} 14 |
15 |
16 |
17 | {{ content }} 18 |
19 |
20 |
21 |
22 | 23 | {% include footer.html %} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/_layouts/guides.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 |
12 |
13 | {% include guidesnav.html %} 14 |
15 |
16 |
17 | {{ content }} 18 |
19 |
20 |
21 |
22 | 23 | {% include footer.html %} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | 7 |
8 |

{{ page.title }}

9 |

{{ page.date | date: "%b %-d, %Y" }}{% if page.author %} • {{ page.author }}{% endif %}{% if page.meta %} • {{ page.meta }}{% endif %}

10 |
11 | 12 |
13 | {{ content }} 14 |
15 | -------------------------------------------------------------------------------- /web/assets/alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goatslacker/alt/d4cd64938d249463e9717426d223eba49d8a0fc2/web/assets/alt.png -------------------------------------------------------------------------------- /web/assets/bootstrap-navbar.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=774f46b6a0d7eb077004) 9 | * Config saved to config.json and https://gist.github.com/774f46b6a0d7eb077004 10 | */ 11 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(t){"use strict";function e(e){e&&3===e.which||(t(n).remove(),t(s).each(function(){var o=t(this),n=i(o),s={relatedTarget:this};n.hasClass("open")&&(n.trigger(e=t.Event("hide.bs.dropdown",s)),e.isDefaultPrevented()||(o.attr("aria-expanded","false"),n.removeClass("open").trigger("hidden.bs.dropdown",s)))}))}function i(e){var i=e.attr("data-target");i||(i=e.attr("href"),i=i&&/#[A-Za-z]/.test(i)&&i.replace(/.*(?=#[^\s]*$)/,""));var o=i&&t(i);return o&&o.length?o:e.parent()}function o(e){return this.each(function(){var i=t(this),o=i.data("bs.dropdown");o||i.data("bs.dropdown",o=new a(this)),"string"==typeof e&&o[e].call(i)})}var n=".dropdown-backdrop",s='[data-toggle="dropdown"]',a=function(e){t(e).on("click.bs.dropdown",this.toggle)};a.VERSION="3.3.2",a.prototype.toggle=function(o){var n=t(this);if(!n.is(".disabled, :disabled")){var s=i(n),a=s.hasClass("open");if(e(),!a){"ontouchstart"in document.documentElement&&!s.closest(".navbar-nav").length&&t('