├── .eslintrc ├── .gitignore ├── .versions ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── circle.yml ├── git-packages.json ├── package.js ├── source ├── automation.js ├── blaze_component.js ├── event.js ├── helpers.js ├── mixins │ ├── reactive.js │ └── stateful.js └── module.js ├── test.sh └── tests ├── event.tests.js └── reactive.tests.js /.eslintrc: -------------------------------------------------------------------------------- 1 | /** 2 | * 0 - turn the rule off 3 | * 1 - turn the rule on as a warning (doesn't affect exit code) 4 | * 2 - turn the rule on as an error (exit code will be 1) 5 | * 6 | * Meteor Style Guide: https://github.com/meteor/meteor/wiki/Meteor-Style-Guide 7 | * 8 | */ 9 | 10 | { 11 | "parser": "babel-eslint", 12 | "env": { 13 | "browser": true, 14 | "node": true 15 | }, 16 | "ecmaFeatures": { 17 | "arrowFunctions": true, 18 | "blockBindings": true, 19 | "classes": true, 20 | "defaultParams": true, 21 | "destructuring": true, 22 | "forOf": true, 23 | "generators": false, 24 | "modules": true, 25 | "objectLiteralComputedProperties": true, 26 | "objectLiteralDuplicateProperties": false, 27 | "objectLiteralShorthandMethods": true, 28 | "objectLiteralShorthandProperties": true, 29 | "spread": true, 30 | "superInFunctions": true, 31 | "templateStrings": true, 32 | "jsx": true 33 | }, 34 | "rules": { 35 | /** 36 | * Strict mode 37 | */ 38 | // babel inserts "use strict"; for us 39 | // http://eslint.org/docs/rules/strict 40 | "strict": 0, 41 | 42 | /** 43 | * ES6 44 | */ 45 | "no-var": 1, // http://eslint.org/docs/rules/no-var 46 | 47 | /** 48 | * Variables 49 | */ 50 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 51 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 52 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 53 | "vars": "local", 54 | "args": "after-used" 55 | }], 56 | "no-use-before-define": [2, "nofunc"], // http://eslint.org/docs/rules/no-use-before-define 57 | 58 | /** 59 | * Possible errors 60 | */ 61 | "comma-dangle": [1, "never"], // http://eslint.org/docs/rules/comma-dangle 62 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 63 | "no-console": 1, // http://eslint.org/docs/rules/no-console 64 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 65 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 66 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 67 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 68 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 69 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 70 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 71 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 72 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 73 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 74 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 75 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 76 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 77 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 78 | "quote-props": [2, "as-needed", { "keywords": true, "unnecessary": false }], // http://eslint.org/docs/rules/quote-props (previously known as no-reserved-keys) 79 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 80 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 81 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 82 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 83 | 84 | /** 85 | * Best practices 86 | */ 87 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 88 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 89 | "default-case": 2, // http://eslint.org/docs/rules/default-case 90 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 91 | "allowKeywords": true 92 | }], 93 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 94 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 95 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 96 | //"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 97 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 98 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 99 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 100 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 101 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 102 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 103 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 104 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 105 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 106 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 107 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 108 | "no-new": 2, // http://eslint.org/docs/rules/no-new 109 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 110 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 111 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 112 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 113 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 114 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 115 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 116 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 117 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 118 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 119 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 120 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 121 | "no-with": 2, // http://eslint.org/docs/rules/no-with 122 | "radix": 2, // http://eslint.org/docs/rules/radix 123 | "vars-on-top": 1, // http://eslint.org/docs/rules/vars-on-top 124 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 125 | "yoda": 2, // http://eslint.org/docs/rules/yoda 126 | "max-len": [1, 200, 2], // http://eslint.org/docs/rules/max-len 127 | 128 | /** 129 | * Style 130 | */ 131 | "indent": [2, 2, {"VariableDeclarator": 2}], // http://eslint.org/docs/rules/indent 132 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 133 | "1tbs", { 134 | "allowSingleLine": true 135 | }], 136 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 137 | "properties": "never" 138 | }], 139 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 140 | "before": false, 141 | "after": true 142 | }], 143 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 144 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 145 | "func-names": 0, // http://eslint.org/docs/rules/func-names 146 | "func-style": [2, "expression"], // http://eslint.org/docs/rules/func-style 147 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 148 | "beforeColon": false, 149 | "afterColon": true 150 | }], 151 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 152 | "newIsCap": true, 153 | "capIsNew": false 154 | }], 155 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 156 | "max": 2 157 | }], 158 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 159 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 160 | "no-array-constructor": 2, // http://eslint.org/docs/rules/no-array-constructor 161 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 162 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 163 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 164 | "one-var": [1, "never"], // http://eslint.org/docs/rules/one-var 165 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 166 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 167 | "before": false, 168 | "after": true 169 | }], 170 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 171 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 172 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 173 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 174 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 175 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment (previously known as spaced-line-comment) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | .DS_Store 3 | local-packages.json 4 | packages 5 | .idea -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | binary-heap@1.0.4 5 | blaze@2.1.3 6 | blaze-tools@1.0.4 7 | boilerplate-generator@1.0.4 8 | caching-compiler@1.0.0 9 | callback-hook@1.0.4 10 | check@1.1.0 11 | coffeescript@1.0.11 12 | ddp@1.2.2 13 | ddp-client@1.2.1 14 | ddp-common@1.2.2 15 | ddp-server@1.2.2 16 | deps@1.0.9 17 | diff-sequence@1.0.1 18 | ecmascript@0.1.6 19 | ecmascript-runtime@0.2.6 20 | ejson@1.0.7 21 | fongandrew:find-and-modify@0.2.1 22 | geojson-utils@1.0.4 23 | html-tools@1.0.5 24 | htmljs@1.0.5 25 | id-map@1.0.4 26 | jquery@1.11.4 27 | local-test:space:ui@6.0.0 28 | logging@1.0.8 29 | meteor@1.1.10 30 | minimongo@1.0.10 31 | mongo@1.1.3 32 | mongo-id@1.0.1 33 | npm-mongo@1.4.39_1 34 | observe-sequence@1.0.7 35 | ordered-dict@1.0.4 36 | practicalmeteor:chai@2.1.0_1 37 | practicalmeteor:loglevel@1.2.0_2 38 | practicalmeteor:munit@2.1.5 39 | practicalmeteor:sinon@1.14.1_2 40 | promise@0.5.1 41 | random@1.0.5 42 | reactive-dict@1.1.3 43 | reactive-var@1.0.6 44 | retry@1.0.4 45 | routepolicy@1.0.6 46 | space:base@4.0.1 47 | space:messaging@3.0.1 48 | space:testing@3.0.1 49 | space:ui@6.0.0 50 | spacebars@1.0.7 51 | spacebars-compiler@1.0.7 52 | test-helpers@1.0.5 53 | tinytest@1.0.6 54 | tracker@1.0.9 55 | ui@1.0.8 56 | underscore@1.0.4 57 | webapp@1.2.3 58 | webapp-hashing@1.0.5 59 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## 6.0.0 5 | This major version has been months in the works, and we're excited to be introducing Version 6 as a simplification of APIs and separation of concerns. 6 | 7 | With this release we see _flux_ concepts extracted into a dedicated package [space:flux](https://github.com/meteor-space/flux), allowing `space:ui` to provide pattern-agnostic UI base features suitable for tailored implementations. Installing `space:flux` will install `space:ui`, but if _flux_ or our implementation is not what you're looking for, just use `space:ui` instead. Too easy! 8 | 9 | We're now 100% focused on ES6, and have introduced declarative APIs as the original Coffeescript style didn't translate well. View the complete [TodoMVC](https://github.com/meteor-space/TodoMVC/tree/develop/javascript) example here to get a feel for the new style 10 | 11 | State APIs have been unified into `Space.ui.Stateful`; an object added as a mixin. There are three types of state it can manage: 12 | 13 | 14 | 15 | - 1. You already have a reactive data source like `Mongo.Collection::find`: in 16 | this case you simply create methods on the class that return these: 17 | 18 | ```javascript 19 | Space.flux.Store.extend(TodoMVC, 'TodosStore', { 20 | completedTodos: function() { 21 | return this.todos.find({ isCompleted: true }); 22 | } 23 | }); 24 | 25 | ``` 26 | - 2. If you need to manage state that you don't want to hold in a collection 27 | you can use the new API to generate `ReactiveVar` instance accessors: 28 | 29 | ```javascript 30 | Space.flux.Store.extend(TodoMVC, 'TodosStore', { 31 | reactiveVars: function() { 32 | return [{ 33 | activeFilter: this.FILTERS.ALL, 34 | }]; 35 | }, 36 | filteredTodos: function() { 37 | // Depend on the reactive value to choose a todos filter 38 | switch (this.activeFilter()) { 39 | case this.FILTERS.ALL: return this.todos.find(); 40 | case this.FILTERS.ACTIVE: return this.todos.find({ isCompleted: false}); 41 | case this.FILTERS.COMPLETED: return this.todos.find({ isCompleted: true }); 42 | } 43 | }, 44 | _changeActiveFilter: function(event) { 45 | // Set the reactive var to a new value 46 | this.activeFilter(event.filter); 47 | } 48 | }); 49 | ``` 50 | 51 | - 3. If you need to manage state that you don't want to hold in a collection, and you need the values to persist during a hot-code push use the new sessionVars API to generate a scoped `ReactiveDict` instance accessor: 52 | 53 | ```javascript 54 | Space.flux.Store.extend(TodoMVC, 'TodosStore', { 55 | sessionVars() { 56 | return [{ 57 | editingTodoId: null 58 | }]; 59 | }, 60 | 61 | eventSubscriptions() { 62 | return [{ 63 | 'TodoMVC.TodoEditingStarted': this._setEditingTodoId, 64 | 'TodoMVC.TodoEditingEnded': this._unsetEditingTodoId, 65 | }]; 66 | }, 67 | 68 | _setEditingTodoId(event) { 69 | this._setSessionVar('editingTodoId', event.todoId); 70 | }, 71 | 72 | _unsetEditingTodoId() { 73 | this._setSessionVar('editingTodoId', null); 74 | } 75 | 76 | }); 77 | ``` 78 | `Space.ui.BlazeComponent` is `Stateful` 79 | 80 | ```javascript 81 | Space.ui.BlazeComponent.extend('TodoMVC.MyCustomComponent', { 82 | _session: 'TodoMVC.MyCustomComponent', // some unique name for session 83 | sessionVars() { 84 | return [{ mySessionVar: null }]; // default value 85 | }, 86 | reactiveVars() { 87 | return [{ someReactiveVar: null }]; // default value 88 | } 89 | }); 90 | ``` 91 | 92 | ### New Features: 93 | - `Space.ui.Event` is an extension of `Space.messaging.Event` 94 | Currently this is just a more expressive object, but any future UI specific 95 | event features will be added here and not the base class. 96 | 97 | ### Breaking Changes: 98 | - This version uses space:base 4.x which includes breaking changes. 99 | Please see the [changelog](https://github.com/meteor-space/base/blob/master/CHANGELOG.md). 100 | - This version uses space:messaging 3.x which includes breaking changes. 101 | Please see the [changelog](https://github.com/meteor-space/messaging/blob/master/CHANGELOG.md). 102 | 103 | - Must be running Meteor 1.2.0.1 or later. 104 | - `Space.ui.Store` has been extracted to a separate package [space:flux](https://github.com/meteor-space/flux/) 105 | - Reactive data sources now just need to be defined as methods on `Space.flux.Store`, 106 | with non-reactive sources returned in `reactiveVars` or `sessionVars`. 107 | This change was motivated to simplify the component's interface, but it also improves 108 | the clarity of reactive state management in the store. 109 | - `Space.ui.Mediator` was removed from the project in favour of more popular and 110 | recommended alternatives like blaze-components. 111 | 112 | 113 | ### Upgrade from 5.x 114 | - Please see the migration guide in [space:base](https://github.com/meteor-space/base/blob/master/README.md#migration-guide) 115 | 116 | - `meteor add space:flux` 117 | - Switch base object `Space.ui.Store` to `Space.flux.Store` 118 | - Replace any `Space.ui.Mediator` and standard 'template managers' with a 119 | `Space.ui.BlazeComponent` (Example: [TodoList component](https://github.com/meteor-space/ui/blob/develop/examples/TodoMVC/client/views/todo_list/todo_list.js)) 120 | - Change all `Space.flux.Store` event subscribers to the new declarative 121 | `eventSubscriptions` API. 122 | - Ensure all store state is either determined via a method (if already reactive), 123 | or is part of `reactiveVars` or `sessionVars` (a store scoped reactiveDict). 124 | 125 | Further detail can be seen in 126 | [TodosMVC](https://github.com/meteor-space/TodoMVC/tree/develop/javascript) sample app 127 | 128 | ## 5.3.0 129 | Updates to latest `space:base` and `space:messaging` packages. 130 | 131 | ## 5.2.1 132 | Updated Github location to point to new home at https://github.com/meteor-space/ui 133 | 134 | ## 5.2.0 135 | Updates to latest `space:base` and `space:messaging` packages which introduced 136 | many small debugging and API improvements. 137 | 138 | ## 5.1.3 139 | Throw better errors when blaze components could not be resolved. 140 | 141 | ## 5.1.2 142 | Let mediators and blaze components cleanup their state on destruction 143 | 144 | ## 5.1.1 145 | Adds weak dependency on blaze-components package so that it work in a package 146 | only app. 147 | 148 | ## 5.1.0 149 | Introduces support for `blaze-components` via the new class `Space.ui.BlazeComponent`. This works very similar to `Space.ui.Mediator` 150 | but it also extends `BlazeComponent` with all its capabilities. 151 | See the TodoMVC example for a basic reference. 152 | 153 | ## 5.0.2 154 | Adds support for Meteor > 1.0 template hooks. 155 | 156 | ## 5.0.1 157 | @Sanjo fixed a minor issue #29 with `Space.ui.createEvents` 158 | 159 | ## 5.0.0 160 | Updates to `space:base@2.0.0` and `space:messaging@1.0.0` (checkout breaking 161 | changes in both packages!). Here is short summary: 162 | 163 | ### Upgrade from 4.x 164 | - replace the app/module `run` hook with `startup` 165 | - start your application with `start` instead of `run` 166 | - There is a new `Space.messaging.Api` to define Meteor methods that can be 167 | tested easily. This **replaces** the functionality from `Space.messaging.CommandBus` 168 | to send commands from client to server. **This is not possible anymore!** 169 | Commands are now just the same as events, you can use them on the client side 170 | and server side the same way, **BUT** if you want "type safety" when sending 171 | a command from client to server, you have to check it yourself within the server 172 | method. See the updated TodoMVC example. 173 | 174 | ## 4.3.2 175 | Introduces the concept of non-reactive default state for stores and mediators 176 | so that state is not always overwritten by default values when some reactive 177 | dependency triggers a re-run of the `setInitialState` method. If you have default 178 | values for the state use `setDefaultState` instead. This method is only run once 179 | when the instance is created, after the dependencies were injected. 180 | 181 | ## 4.3.1 182 | Make setting the initial state of stores and mediators reactive. This has the 183 | benefit that one can take advantage of `Mongo.Collection#findOne` and other reactivity 184 | features when initializing the state. 185 | 186 | ## 4.3.0 187 | Use the new `Space.Object.mixin` capabilities to introduce the `Space.ui.Stateful` 188 | mixin that encapsulates the state setting functionality that the store had and make 189 | it available on mediators too. 190 | 191 | ## 4.2.1 192 | Introduce Space.ui.Module with same mapping automation features as the previously 193 | added Space.ui.Application 194 | 195 | ## 4.2.0 196 | Updates to latest space package dependencies, especially to `space:messaging` 197 | which introduced a simpler controller api for handling messages. 198 | 199 | ## 4.1.1 200 | Fixes bug with reactivity when setting store state via `set`. It is non-reactive now. 201 | 202 | ## 4.1.0 203 | Adds `Space.ui.Application` class with simplified api for setting up stores, 204 | mediators and controllers. 205 | 206 | ## 4.0.1 207 | Use latest space:messaging release 208 | 209 | ## 4.0.0 210 | 211 | - Upgrades to `space:base@1.3.0` 212 | 213 | ### Upgrade from 3.x 214 | Replace the flux dispatcher and actions with more solid event architecture 215 | called `space:messaging`. This also introduces type-checked events. Please 216 | have a look at the TodoMVC example to see how the new system works. 217 | 218 | ## 3.4.4 219 | Upgrades to `space:base@1.2.6` 220 | 221 | ## 3.4.3 222 | Adds tests for store `state` related methods and improves its API 223 | 224 | ## 3.4.2 225 | Upgrades to `space:base@1.2.4` 226 | 227 | ## 3.4.0 228 | Removes iron-router suppport and its dependency on it. 229 | 230 | ## 3.3.0 231 | Improves the Mediator api for creating template helpers and event handlers. 232 | 233 | ## 3.2.0 234 | Adds simplified api for creating and dispatching actions (see TodoMVC example). 235 | 236 | ## 3.1.0 237 | Introduces auto-mapping of mediators and templates via annotations. 238 | 239 | ## 3.0.0 240 | Cleans up the mediator API and removed old relicts that are not used anymore. 241 | 242 | ## 2.0.0 243 | Update to the latest 1.0.3 verison of iron:router and fast-render packages. 244 | 245 | ## 1.0.0 246 | Publish first version to Meteor package system. 247 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) <2016> 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 10 | of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 13 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 14 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 15 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Space UI [![Circle CI](https://circleci.com/gh/meteor-space/ui.svg?style=svg)](https://circleci.com/gh/meteor-space/ui) 2 | 3 | _Pattern-agnostic base UI package to gain control over your Meteor UI_ 4 | ___ 5 | 6 | Licensed under the MIT license | [Changelog + Upgrade Guide](CHANGELOG.md) 7 | ## Documentation 8 | API Reference and User Guide are coming soon 9 | 10 | ## Installation 11 | ``` 12 | meteor add space:ui 13 | meteor add peerlibrary:blaze-components 14 | ``` 15 | _Implementing [flux](https://meteor-space.readme.io/docs/flux-in-depth)?_ 16 | ``` 17 | meteor add space:flux 18 | ``` 19 | 20 | ## Examples 21 | For a quick start take a look at the [TodoMVC example](https://github.com/meteor-space/TodoMVC) 22 | 23 | ## Tests 24 | ``` 25 | meteor test-packages ./ 26 | ``` 27 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.33 4 | environment: 5 | PACKAGE_DIRS: packages 6 | pre: 7 | - curl https://install.meteor.com | /bin/sh 8 | dependencies: 9 | pre: 10 | - npm install -g mgp 11 | - npm install -g spacejam 12 | override: 13 | - mgp 14 | test: 15 | override: 16 | - spacejam test-packages ./ 17 | -------------------------------------------------------------------------------- /git-packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "space:base": { 3 | "git":"https://github.com/meteor-space/base.git", 4 | "version": "1d99a64fe51904569742aee13e94b3cae29abdb4" 5 | }, 6 | "space:messaging": { 7 | "git":"https://github.com/meteor-space/messaging.git", 8 | "version": "bbaf57c511f7230c662d2ea4473b056148c82787" 9 | }, 10 | "space:testing": { 11 | "git":"https://github.com/meteor-space/testing.git", 12 | "version": "53f24417c325500e1b836b88d5f03a17b2d97585" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: 'Pattern-agnostic base UI package to gain control over your Meteor UI', 3 | name: 'space:ui', 4 | version: '6.0.0', 5 | git: 'https://github.com/meteor-space/ui.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | 10 | api.versionsFrom('1.2.0.1'); 11 | 12 | api.use([ 13 | 'underscore', 14 | 'tracker', 15 | 'ecmascript', 16 | 'reactive-var', 17 | 'reactive-dict', 18 | 'space:base@4.0.1', 19 | 'space:messaging@3.0.1' 20 | ]); 21 | 22 | api.use([ 23 | 'peerlibrary:blaze-components@0.16.2' 24 | ], 'client', {weak: true}); 25 | 26 | api.addFiles([ 27 | 'source/module.js', 28 | 'source/automation.js', 29 | 'source/mixins/stateful.js', 30 | 'source/mixins/reactive.js', 31 | 'source/blaze_component.js', 32 | 'source/helpers.js', 33 | 'source/event.js' 34 | ], 'client'); 35 | 36 | }); 37 | 38 | Package.onTest(function(api) { 39 | 40 | api.use([ 41 | 'ecmascript', 42 | 'space:ui', 43 | 'reactive-var', 44 | 'tracker', 45 | 'practicalmeteor:munit@2.1.5', 46 | 'space:base@4.0.1', 47 | 'space:messaging@3.0.1', 48 | 'space:testing@3.0.1' 49 | ]); 50 | 51 | api.addFiles([ 52 | 'tests/reactive.tests.js', 53 | 'tests/event.tests.js' 54 | ], 'client'); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /source/automation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides a convenience layer for mapping specific parts 3 | * within space applications as singletons by listing their 4 | * full namespace paths in arrays like: 5 | * controllers: ['My.awesome.Controller', 'My.second.Controller'] 6 | **/ 7 | 8 | Space.Module.mixin({ 9 | 10 | controllers: [], 11 | components: [], 12 | 13 | onDependenciesReady: function() { 14 | this._wrapLifecycleHook('onInitialize', this._onInitializeUi); 15 | this._wrapLifecycleHook('afterInitialize', this.afterInitializeUi); 16 | }, 17 | 18 | /** 19 | * This life-cycle hook is called during initialization of the space 20 | * application and sets up the singleton mappings and UI components. 21 | **/ 22 | _onInitializeUi(onInitialize) { 23 | // Map service-like classes that need to run during the complete 24 | // life-cycle of a space application as singletons. 25 | let mapAsSingleton = function(klass) { 26 | this.injector.map(klass).asSingleton(); 27 | }; 28 | _.each(this.controllers, mapAsSingleton, this); 29 | 30 | /** 31 | * The integration with blaze components is handled in the `onCreate` 32 | * hook of the component, that's why we provide the app instance so that 33 | * the blaze components can register themselves as soon as they are ready 34 | */ 35 | _.each(this.components, function setupBlazeComponents(componentPath) { 36 | let component = Space.resolvePath(componentPath); 37 | if (component === null) { 38 | throw new Error('Space.Module could not resolve component class <' + componentPath + '>'); 39 | } 40 | component.Application = this.app; 41 | }, this); 42 | 43 | onInitialize.call(this); 44 | }, 45 | 46 | /** 47 | * This life-cycle hook is called when the app starts to run 48 | * and creates the singleton instances. 49 | */ 50 | afterInitializeUi(afterInitialize) { 51 | let createSingletonInstance = _.bind(function(klass) { 52 | this.injector.create(klass); 53 | }, this); 54 | _.each(this.controllers, createSingletonInstance); 55 | afterInitialize.call(this); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /source/blaze_component.js: -------------------------------------------------------------------------------- 1 | let BlazeComponentsPackage = Package['peerlibrary:blaze-components']; 2 | 3 | if (BlazeComponentsPackage !== undefined) { // weak dependency 4 | let BlazeComponent = BlazeComponentsPackage.BlazeComponent; 5 | 6 | Space.ui.BlazeComponent = Space.Object.extend(); 7 | 8 | // Make it a Blaze Component by copying over static and prototype properties 9 | for (let property in BlazeComponent) { 10 | if (property !== '__super__') { 11 | Space.ui.BlazeComponent[property] = BlazeComponent[property]; 12 | } 13 | } 14 | for (let property in BlazeComponent.prototype) { 15 | if (property !== 'constructor') { 16 | let value = BlazeComponent.prototype[property]; 17 | Space.ui.BlazeComponent.prototype[property] = value; 18 | } 19 | } 20 | 21 | Space.ui.BlazeComponent.mixin({ 22 | 23 | dependencies: { 24 | eventBus: 'Space.messaging.EventBus' 25 | }, 26 | 27 | onCreated() { 28 | if (!this.constructor.Application) { 29 | throw new Error(`You forgot to map the component <${this}> in your app.`); 30 | } 31 | this.constructor.Application.injector.injectInto(this); 32 | }, 33 | 34 | onDestroyed() { 35 | this.stopComputations(); 36 | }, 37 | 38 | publish(event) { 39 | this.eventBus.publish(event); 40 | } 41 | 42 | }); 43 | 44 | Space.ui.BlazeComponent.mixin(Space.ui.Stateful); 45 | Space.ui.BlazeComponent.mixin(Space.ui.Reactive); 46 | } 47 | -------------------------------------------------------------------------------- /source/event.js: -------------------------------------------------------------------------------- 1 | Space.messaging.Event.extend('Space.ui.Event', {}); 2 | -------------------------------------------------------------------------------- /source/helpers.js: -------------------------------------------------------------------------------- 1 | Space.ui.getEventTarget = function(event) { 2 | return event.target.$blaze_range.view.templateInstance(); 3 | }; 4 | 5 | Space.ui.defineEvents = function() { 6 | let args = Array.prototype.slice.call(arguments); 7 | args.unshift(Space.ui.Event); 8 | return Space.messaging.define.apply(this, args); 9 | }; 10 | -------------------------------------------------------------------------------- /source/mixins/reactive.js: -------------------------------------------------------------------------------- 1 | Space.ui.Reactive = { 2 | 3 | dependencies: { 4 | tracker: 'Tracker' 5 | }, 6 | 7 | _computations: null, 8 | 9 | onDependenciesReady() { 10 | this._computations = []; 11 | for (computation of this.computations()) { 12 | this._computations.push( 13 | this.tracker.autorun(_.bind(computation, this), { 14 | onError: this.onComputationError 15 | }) 16 | ); 17 | } 18 | }, 19 | 20 | computations() { 21 | return []; 22 | }, 23 | 24 | stopComputations() { 25 | for (computation of this._computations) { 26 | computation.stop(); 27 | } 28 | }, 29 | 30 | onComputationError(error) { 31 | throw error; 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /source/mixins/stateful.js: -------------------------------------------------------------------------------- 1 | Space.ui.Stateful = { 2 | 3 | statics: { 4 | _session: null 5 | }, 6 | 7 | dependencies: { 8 | ReactiveVar: 'ReactiveVar', 9 | ReactiveDict: 'ReactiveDict', 10 | _: 'underscore' 11 | }, 12 | 13 | _reactiveVars: null, 14 | _session: null, 15 | 16 | onDependenciesReady() { 17 | this._reactiveVars = {}; 18 | this._setupReactiveVars(); 19 | // Only create one static singleton of the reactive-dict for this class! 20 | if (this.constructor._session === null) { 21 | this.constructor._session = new this.ReactiveDict(this._session); 22 | } 23 | this._session = this.constructor._session; 24 | this._setDefaultSessionVars(); 25 | }, 26 | 27 | reactiveVars() { 28 | return []; 29 | }, 30 | 31 | sessionVars() { 32 | return []; 33 | }, 34 | 35 | state() { 36 | let state = {}; 37 | for (let key in this._reactiveVars) { 38 | if (this._reactiveVars.hasOwnProperty(key)) { 39 | state[key] = this._reactiveVars[key].get(); 40 | } 41 | } 42 | for (let key in this._session.keys) { 43 | state[key] = this._session.get(key); 44 | } 45 | return state; 46 | }, 47 | 48 | /** 49 | * Initialize the reactive vars with default values. 50 | */ 51 | _setupReactiveVars() { 52 | let reactiveVars = {}; 53 | let reactiveVarMaps = this.reactiveVars(); 54 | reactiveVarMaps.unshift(reactiveVars); 55 | this._.extend.apply(null, reactiveVarMaps); 56 | this._.each(reactiveVars, this._generateReactiveVar, this); 57 | }, 58 | 59 | /** 60 | * Generates a method on this class instance for each reactive var 61 | * that can be used to get the value of it. 62 | */ 63 | _generateReactiveVar(defaultValue, varName) { 64 | let reactiveVar = new this.ReactiveVar(); 65 | reactiveVar.set(defaultValue); 66 | this._reactiveVars[varName] = reactiveVar; 67 | this[varName] = function() { 68 | return reactiveVar.get(); 69 | }; 70 | }, 71 | 72 | /** 73 | * Initialize the session vars with default values. 74 | */ 75 | _setDefaultSessionVars() { 76 | let sessionVars = {}; 77 | let sessionVarMaps = this.sessionVars(); 78 | sessionVarMaps.unshift(sessionVars); 79 | this._.extend.apply(null, sessionVarMaps); 80 | this._.each(sessionVars, this._generateSessionVar, this); 81 | }, 82 | 83 | /** 84 | * Generates a method on this class instance for each session var 85 | * that can be used to get the value of it. 86 | */ 87 | _generateSessionVar(defaultValue, varName) { 88 | let session = this._session; 89 | session.setDefault(varName, defaultValue); 90 | this[varName] = function() { 91 | return session.get(varName); 92 | }; 93 | }, 94 | 95 | _setReactiveVar(varName, value) { 96 | let reactiveVar = this._reactiveVars[varName]; 97 | if (!reactiveVar) { 98 | throw new Error(`Did you forget to setup reactive var <${varName}>?`); 99 | } 100 | reactiveVar.set(value); 101 | }, 102 | 103 | _setSessionVar(varName, value) { 104 | this._session.set(varName, value); 105 | } 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /source/module.js: -------------------------------------------------------------------------------- 1 | Space.ui = Space.Module.define('Space.ui', { 2 | 3 | requiredModules: ['Space.messaging'], 4 | 5 | onInitialize() { 6 | this.injector.map('ReactiveDict').to(ReactiveDict); 7 | } 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PACKAGE_DIRS='packages' 4 | 5 | if [ "$PORT" ]; then 6 | meteor test-packages ./ --port $PORT 7 | else 8 | meteor test-packages ./ 9 | fi 10 | -------------------------------------------------------------------------------- /tests/event.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.ui.Event", function() { 2 | 3 | it("extends Space.messaging.Event", function() { 4 | expect(Space.ui.Event).to.extend(Space.messaging.Event); 5 | }); 6 | 7 | }); 8 | -------------------------------------------------------------------------------- /tests/reactive.tests.js: -------------------------------------------------------------------------------- 1 | describe("Space.ui.Reactive", function() { 2 | 3 | beforeEach(function() { 4 | // Setup reactive stub vars 5 | this.first = new ReactiveVar(1); 6 | this.second = new ReactiveVar(1); 7 | this.throwError = new ReactiveVar(false); 8 | let context = this; 9 | 10 | // Setup a class that has computations based on these reactive vars 11 | this.MyReactiveClass = Space.Object.extend('MyReactiveClass', { 12 | mixin: Space.ui.Reactive, 13 | sum: null, 14 | product: null, 15 | computations() { 16 | return [this._calcSum, this._calcProduct, this._errorComp]; 17 | }, 18 | _calcSum() { 19 | this.sum = context.first.get() + context.second.get(); 20 | }, 21 | _calcProduct() { 22 | this.product = context.first.get() * context.second.get(); 23 | }, 24 | _errorComp() { 25 | if (context.throwError.get()) { 26 | throw new Error('test'); 27 | } 28 | } 29 | }); 30 | 31 | // Provide runtime dependencies and tell class that they are ready 32 | this.myReactive = new MyReactiveClass({ tracker: Tracker }); 33 | this.myReactive.onDependenciesReady(); 34 | }); 35 | 36 | describe("setting up computations", function() { 37 | 38 | it("autoruns them whenever any dep changes", function() { 39 | // Computations should run immediately 40 | expect(this.myReactive.sum).to.equal(1 + 1); 41 | expect(this.myReactive.product).to.equal(1 * 1); 42 | this.first.set(2); 43 | Tracker.flush(); 44 | // And re-run after any reactive dep changed 45 | expect(this.myReactive.sum).to.equal(2 + 1); 46 | expect(this.myReactive.product).to.equal(2 * 1); 47 | this.second.set(2); 48 | Tracker.flush(); 49 | // And re-run after any reactive dep changed 50 | expect(this.myReactive.sum).to.equal(2 + 2); 51 | expect(this.myReactive.product).to.equal(2 * 2); 52 | }); 53 | 54 | }); 55 | 56 | describe("cleaning up computations", function() { 57 | 58 | it("can cleanup all computations at once", function() { 59 | this.myReactive.stopComputations(); 60 | this.first.set(2); 61 | this.second.set(2); 62 | Tracker.flush(); 63 | // The computations should not have run anymore 64 | expect(this.myReactive.sum).to.equal(2); 65 | expect(this.myReactive.product).to.equal(1); 66 | }); 67 | 68 | }); 69 | 70 | describe("computation errors", function() { 71 | 72 | it("re-throws them as normal errors", function() { 73 | let computationError = () => { 74 | this.throwError.set(true); 75 | Tracker.flush(); 76 | }; 77 | expect(computationError).to.throw('test'); 78 | }); 79 | 80 | }); 81 | 82 | }); 83 | --------------------------------------------------------------------------------