├── .gitignore ├── LICENSE ├── README.md ├── browserify.js ├── lib └── client │ ├── action_hub.js │ ├── api.js │ ├── component.js │ ├── component_instance.js │ ├── lookup.js │ ├── render.js │ ├── state_map.js │ └── utils.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 MeteorHacks Pvt Ltd (Sri Lanka). 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flow Components 2 | 3 | > This project is still work in progress and does not have unit and integration tests. 4 | > We are directly pushing changes to master. 5 | > There's no plan to change current APIs, but we don't have a guarantee. 6 | 7 | Build your Meteor app with Components. 8 | 9 | Flow Components has borrowed a lot of concepts from React's component model and applies them on top Blaze. It's not a 1-to-1 mapping of React. 10 | 11 | We've also added some handy features which will help you control reactivity when building a large application. 12 | 13 | ## Table of Contents 14 | 15 | * [Why](#why) 16 | * [Getting Started](#getting-started) 17 | * [States](#states) 18 | * [Actions](#actions) 19 | * [Props](#props) 20 | * [Prototypes](#prototypes) 21 | * [Extending Components with Mixins](#extending-components-with-mixins) 22 | * [Extending Components with Nested Components](#extending-components-with-nested-components) 23 | * [Life Cycle Events](#life-cycle-events) 24 | * [Autoruns](#autoruns) 25 | * [State Functions](#state-functions) 26 | * [Content Blocks](#content-blocks) 27 | * [Referencing Child Components](#referencing-child-components) 28 | * [Refer All Child Components](#refer-all-child-components) 29 | * [Organizing Large Components](#organizing-large-components) 30 | * [Accessing Component DOM](#accessing-component-dom) 31 | * [How Flow Component different from XXX](#how-flow-components-different-from-xxx) 32 | * [Usage Guidelines](#usage-guidelines) 33 | 34 | ## Why? 35 | 36 | When we started building [Kadira.io](https://kadira.io), we had no idea how to architect a Meteor app. After working on it for almost 1.5 years, we realized we were doing it wrong. 37 | 38 | So, we thought a lot and played with a lot of UI frameworks and concepts. That includes [React](http://facebook.github.io/react/) and [Flux](https://facebook.github.io/flux/). After a lot of iterations and experiments, Flow Components is our component framework which is a part of the Flow Architecture for Meteor. 39 | 40 | ## Getting Started 41 | 42 | Let's create a very simple component. It's a typical Hello World example. 43 | 44 | First add Flow Components into your app. 45 | 46 | ~~~shell 47 | meteor add meteorhacks:flow-components 48 | ~~~ 49 | 50 | Then create directory on the client directory of your Meteor app and put these files. 51 | 52 | Here's the JavaScript file, (`component.js`) which is the component. 53 | 54 | ~~~js 55 | var component = FlowComponents.define('hello-component', function(props) { 56 | console.log("A component created!"); 57 | this.name = props.name; 58 | 59 | this.setRandomGreeting(); 60 | // change the greeting for every 300 millis 61 | setInterval(this.setRandomGreeting.bind(this), 300); 62 | }); 63 | 64 | component.prototype.setRandomGreeting = function() { 65 | var greetings = ["awesome", "nice", "cool", "kind"]; 66 | var randomGreeting = greetings[Math.floor(greetings.length * Math.random())]; 67 | this.set("greeting", randomGreeting) 68 | }; 69 | 70 | component.state.message = function() { 71 | return this.name + ", you are " + this.get("greeting") + "!"; 72 | }; 73 | ~~~ 74 | 75 | Now we need to create our template(`view.html`). It's name should be identical to the name of the component which is `hello-component`. 76 | 77 | ~~~html 78 | 83 | ~~~ 84 | 85 | Then, let's add a CSS file for our component. 86 | ~~~css 87 | .hello-component { 88 | font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial; 89 | font-size: 16px; 90 | margin: 10px; 91 | } 92 | ~~~ 93 | 94 | That's all. We've created our first component. We can render it anywhere we like. Here's how to do it. 95 | 96 | ~~~html 97 | {{> render component="hello-component" name="Arunoda"}} 98 | {{> render component="hello-component" name="Sacha"}} 99 | ~~~ 100 | 101 | Then, this is how it's looks like: 102 | 103 | ![Flow Components in Action](https://cldup.com/ma6Tiq9rXO.gif) 104 | 105 | Check [this repo](https://github.com/flow-examples/hello-component) for the complete source code. 106 | 107 | ## States 108 | 109 | State is a variable which is reactive. It can hold any JavaScript literal or an object. This is very similar to a template helper, but it's integrated into the component. 110 | 111 | There are couple of ways, you can get a state. This is the first way. 112 | 113 | #### Creating states using `component.state.` 114 | 115 | ~~~js 116 | var component = FlowComponents.define('my-component'); 117 | component.state.message = function() { 118 | return "some value"; 119 | }; 120 | ~~~ 121 | 122 | Above function is running in a reactive context. So, you can use any kind of reactive variables, Session variables and MiniMongo APIs inside it. 123 | 124 | Context of the above function is the component itself. That's the main difference from a template helper. 125 | 126 | #### Creating states using `this.set` 127 | 128 | You can also set an state with `this.set` API. This is an API of the component. So, you can use it anywhere withing the component. This is how to implement the above example via `this.set`. 129 | 130 | ~~~js 131 | var component = FlowComponents.define('my-component', function() { 132 | this.set("message", "some value"); 133 | }); 134 | ~~~ 135 | 136 | #### `this.set` with large object 137 | 138 | `this.set` is a useful function, but it does value cloning and equality checks. So, if you need to set a very large object this will be an issue. 139 | But there is a solution to avoid that. Here's how to do it: 140 | 141 | ~~~js 142 | var component = FlowComponents.define('my-component', function(params) { 143 | var fireAway = true; 144 | this.set("veryLargeObject", params.obj, fireAway); 145 | }); 146 | ~~~ 147 | 148 | Now, flow component does not do any cloning or equality checks. It simply invalidate all the computations looks for this key. 149 | 150 | #### Accessing states inside a template 151 | 152 | You can access the state anywhere in the template as below: 153 | 154 | ~~~html 155 | 158 | ~~~ 159 | 160 | To access the state, just prefix it with `state$`. You can even access the state inside nested templates in a given component. 161 | 162 | #### Accessing states inside a component with `this.get` 163 | 164 | You can also access the state inside the component with this `this.get` API like this: 165 | 166 | ~~~js 167 | component.state.messageWithName = function() { 168 | var message = this.get("message"); 169 | return "Arunoda, " + message; 170 | }; 171 | ~~~ 172 | 173 | `this.get` is reactive and available anywhere inside the component. 174 | 175 | ## Actions 176 | 177 | Components don't handle DOM events directly. But a component has actions. You can think actions are a kind of way to trigger tasks. To handle DOM elements, you need to call an action within a template event. 178 | 179 | This is how to create an action: 180 | 181 | ~~~js 182 | var component = FlowComponents.define('my-component'); 183 | component.action.changeMessage = function(someValue) { 184 | this.set("message", someValue); 185 | }; 186 | ~~~ 187 | 188 | Context of the message is the component itself. You can also access reactive content inside that, but the action won't re-run again for a change in reactive content. 189 | 190 | There are few ways to call an action. Let's look at them. 191 | 192 | #### Via an DOM event handler 193 | 194 | You can call an action inside an event handler. But, that event handler needs to be registered for a template of the given component. This is how to do that. 195 | 196 | ~~~js 197 | Template['my-component'].events({ 198 | "click button": function() { 199 | var message = $('.textbox').val(); 200 | FlowComponents.callAction("changeMessage", message); 201 | } 202 | }); 203 | ~~~ 204 | 205 | #### Via Props (aka: Action Passing) 206 | 207 | You can also pass an action to a child component via props. Then the child component can call that action just like invoking a JavaScript function. This is how to do it. 208 | 209 | Let's say we are rendering a component called `input` inside our main component: 210 | 211 | ~~~js 212 | {{> render component="input" onSubmit=action$changeMessage }} 213 | ~~~ 214 | 215 | Then inside the `input` component, it calls the `onSubmit` property like this: 216 | 217 | ~~~js 218 | var component = FlowComponents.define("input", function(props) { 219 | this.onSubmit = props.onSubmit; 220 | }); 221 | 222 | component.action.submitMessage = function(message) { 223 | this.onSubmit(message); 224 | }; 225 | ~~~ 226 | 227 | We also describe this method as "action passing". This is the basic building block for creating nested components. 228 | 229 | #### Actions and Promises 230 | 231 | We've have es6-promise support for Actions. When you call an actions either via `FlowComponents.callAction` or action passing, you'll get a promise always. Based on that promise you can model your component. 232 | 233 | Action definition, does not need to return a promise all the time. If you return a promise, Flow Component will pass it to the caller. If not, it'll create a empty promise. 234 | 235 | See: 236 | 237 | ~~~js 238 | var component = FlowComponents.define("input", function(props) { 239 | this.onSubmit = props.onSubmit; 240 | }); 241 | 242 | component.action.submitMessage = function(message) { 243 | var self = this; 244 | self.set('loading', true); 245 | 246 | var promise = this.onSubmit(message); 247 | promise.catch(function(err) { 248 | self.set('errorMessage', err.message); 249 | }).then(function() { 250 | self.set('loading', false); 251 | }); 252 | }; 253 | ~~~ 254 | 255 | Checkout this [sample repo](https://github.com/flow-examples/flow-component-promise) for a complete example. 256 | 257 | #### Disable Promises Temporarily 258 | 259 | Sometime we need to have actions simply sending the plain return rather than wrapping the return as a promise. For those scenarios, we can temporarily disable promise support like this. 260 | 261 | ~~~js 262 | var component = FlowComponents.define("input", function(props) { 263 | this.onNewTooltip = props.onNewTooltip; 264 | this.dataFn = props.dataFn; 265 | this.autorun(this.formatTooltips); 266 | }); 267 | 268 | component.prototype.formatTooltips = function() { 269 | var data = this.dataFn(); 270 | var formattedData = this.noPromises(function() { 271 | var self = this; 272 | return data.map(function(item) { 273 | return self.onNewTooltip(item); 274 | }); 275 | }); 276 | 277 | return formattedData; 278 | }; 279 | ~~~ 280 | 281 | 282 | ## Props 283 | 284 | Props is a way to pass values when rendering the component. A prop can be any valid JavaScript literal, object or a function. This is how to do it: 285 | 286 | ~~~html 287 | {{> render component="input" text="This is great" }} 288 | ~~~ 289 | 290 | Then you can access it inside the component like this: 291 | 292 | ~~~js 293 | var component = FlowComponents.define("input", function(props) { 294 | console.log("Text is: ", props.text); 295 | }); 296 | ~~~ 297 | 298 | You can set any number of props you like: 299 | 300 | ~~~html 301 | {{> render component="input" 302 | text="This is great" 303 | backgroundColor="#736634" 304 | fontSize=20 305 | }} 306 | ~~~ 307 | 308 | You can pass a state into a prop like this: 309 | 310 | ~~~html 311 | {{> render component="input" text=state$message }} 312 | ~~~ 313 | 314 | We've previously talked about how to pass an action like this: 315 | 316 | ~~~html 317 | {{> render component="input" onSubmit=action$message }} 318 | ~~~ 319 | 320 | ## Prototypes 321 | 322 | A Prototype is very similar to a prototype is JavaScript. Prototype is a common function (or property) you can access anywhere inside a component. We've used prototypes in the component we wrote in the Getting Started section. 323 | 324 | ~~~js 325 | var component = FlowComponents.define('hello-component', function(props) { 326 | // see how we are using the prototype 327 | this.setRandomGreeting(); 328 | setInterval(this.setRandomGreeting.bind(this), 300); 329 | }); 330 | 331 | // This is a prototype 332 | component.prototype.setRandomGreeting = function() { 333 | var greetings = ["awesome", "nice", "cool", "kind"]; 334 | var randomGreeting = messages[Math.floor(messages.length * Math.random())]; 335 | this.set("greeting", randomGreeting) 336 | }; 337 | ~~~ 338 | 339 | In that, we've created a `setRandomGreeting` prototype and call in when a component is creating. 340 | 341 | We've also calling it for every 300 milliseconds via the `setInterval`. 342 | 343 | ## Extending Components with Mixins 344 | 345 | Sometimes we create similar kind of components. Then, we've to copy and paste a lot of code including `prototypes`, `states` and `actions`. It's a bad way to manage it. 346 | 347 | That's why Mixins are going to help us. With mixins we can group a set of common code and extend it with an existing components. Let's say we need to add component level subscriptions to Flow, this is how to do it :) 348 | 349 | ~~~js 350 | ComponentSubs = { 351 | prototype: {}, 352 | action: {}, 353 | state: {} 354 | }; 355 | 356 | // calls when the component is creating 357 | ComponentSubs.created = function() { 358 | this._subs = []; 359 | }; 360 | 361 | // calls when the component is rendered 362 | ComponentSubs.rendered = function() {}; 363 | 364 | // calls when the component is destroyed 365 | ComponentSubs.destroyed = function() { 366 | this.stopSubscriptions(); 367 | }; 368 | 369 | ComponentSubs.prototype.subscribe = function() { 370 | var sub = Meteor.subscribe.apply(null, arguments); 371 | this._subs.push(sub); 372 | return sub; 373 | }; 374 | 375 | ComponentSubs.prototype.ready = function() { 376 | var ready = true; 377 | this._subs.forEach(function(sub) { 378 | ready = ready && sub.ready(); 379 | }); 380 | 381 | return ready; 382 | }; 383 | 384 | ComponentSubs.prototype.stopSubscriptions = function() { 385 | this._subs.forEach(function(sub) { 386 | sub.stop(); 387 | }); 388 | this._subs = []; 389 | }; 390 | 391 | ComponentSubs.state.isReady = function() { 392 | return this.ready(); 393 | }; 394 | 395 | ComponentSubs.action.stopSubscriptions = function() { 396 | this.stopSubscriptions(); 397 | }; 398 | ~~~ 399 | 400 | Now you can extend your component with the mixin we've created. Check this: 401 | 402 | ~~~js 403 | var component = FlowComponents.define("my-component", function() { 404 | // you can use like to this subscribe 405 | this.subscribe("mysubscription"); 406 | }); 407 | 408 | // extend your component with Mixins 409 | component.extend(ComponentSubs); 410 | 411 | // you can use it in an action like this 412 | component.action.loadMore = function() { 413 | this.stopSubscriptions(); 414 | this.subscribe("mysubscription", {limit: 200}); 415 | }; 416 | ~~~ 417 | 418 | You can use `isReady` state inside the template like this: 419 | 420 | ~~~html 421 | 428 | ~~~ 429 | 430 | ## Extending Components with Nested Components 431 | 432 | We can use Mixins to extend the functionalities for the component. But we can't use that to extend the user interface. That's where we can use nested components. 433 | 434 | There is no special APIs for that. But you can create a component which uses few other components inside that. 435 | 436 | You can also accept an component name from the `props` and render that. For an example, let's say we are building a loading component. So, we can allow to customize the loading spinner. Here is is: 437 | 438 | ~~~js 439 | var component = FlowComponents.define("loading", function(props) { 440 | this.set("loadingComponent", props.loadingComponent); 441 | }); 442 | ~~~ 443 | 444 | This is the UI for that: 445 | ~~~html 446 | {{#if state$loadingComponent}} 447 | {{> render component=state$loadingComponent }} 448 | {{else}} 449 | Loading... 450 | {{/if}} 451 | ~~~ 452 | 453 | ## Life Cycle Events 454 | 455 | A Component has few different events. Here they are: 456 | 457 | * created - After the component instant created 458 | * rendered - After the component rendered to the screen 459 | * destroyed - After the component destroyed 460 | 461 | You may need to use these events to customize your components. We need to use the created event almost every time. So, that's the callback you passed as the second argument when creating the components. 462 | 463 | ~~~js 464 | var component = FlowComponents.define("hello", function(props) { 465 | console.log("Component created with props:", props); 466 | }); 467 | ~~~ 468 | 469 | In the `created` event callback you can get the `props` as the first argument. 470 | 471 | For other two events, they can be access anywhere inside the component like with: 472 | 473 | * rendered - `this.onRendered(function() {})` 474 | * destroyed - `this.onDestroyed(function() {})` 475 | 476 | Check this example: 477 | 478 | ~~~js 479 | var component = FlowComponents.define("hello", function() { 480 | this.onRendered(function() { 481 | console.log("Rendered to the screen."); 482 | }); 483 | 484 | this.onDestroyed(function() { 485 | console.log("Component destroyed."); 486 | }); 487 | }); 488 | ~~~ 489 | 490 | Context of the callback you've passed to a life cycle event is the component itself. So, because of that something like this is possible. 491 | 492 | ~~~js 493 | var component = FlowComponents.define("hello", function() { 494 | this.onRendered(function() { 495 | console.log("Rendered to the screen."); 496 | 497 | this.onDestroyed(function() { 498 | console.log("Component destroyed."); 499 | }); 500 | }); 501 | }); 502 | ~~~ 503 | 504 | ## Autoruns 505 | 506 | Sometimes we may need to use autoruns inside a component. So, when you start an autorun it needs to stop when the component destroyed. We've a simple way to do that. See: 507 | 508 | ~~~js 509 | var component = FlowComponents.define("hello", function() { 510 | this.autorun(function() { 511 | var posts = Posts.fetch(); 512 | this.set("postCount", posts.length); 513 | }); 514 | }); 515 | ~~~ 516 | 517 | > Note: Context of the callback for `this.autorun` is also the component. That's why calling `this.set` is possible inside the autorun. 518 | 519 | ## State Functions 520 | 521 | State functions is a powerful tool which helps you build components while minimizing re-renders. Before we start, let's see why need it. Look at this usage of nested components: 522 | 523 | ~~~js 524 | var component = FlowComponents.define("parent", function() { 525 | var self = this; 526 | setInterval(function() { 527 | var usage = Math.ceil(Math.random() * 100); 528 | this.set("cpuUsage", usage); 529 | }, 100); 530 | }); 531 | ~~~ 532 | 533 | This is the template of parent: 534 | 535 | ~~~html 536 | 539 | ~~~ 540 | 541 | As you can see this is pretty straight forward. Parent component change the CPU usage for every 100 millis and then `guage` component will print it. So, what's the issue here? 542 | 543 | Since we get the state as `state$cpuUsage`, it's getting change every 100 millis. So, the `gauge` component will get changed at that time too. 544 | 545 | That means existing gauge component will be destroyed and re created again. Which is very expensive and we don't need to do something like this. That's where state functions are going to help you. Before that, let's look at how we've implemented our gauge component. 546 | 547 | ~~~js 548 | var component = FlowComponents.define("guage", function(props) { 549 | this.set('value', props.value); 550 | }); 551 | ~~~ 552 | 553 | This is the template: 554 | ~~~html 555 | 558 | ~~~ 559 | 560 | #### Converting it to use State Functions 561 | 562 | Let's change the parent template like this: 563 | 564 | ~~~html 565 | 568 | ~~~ 569 | 570 | Note that, we only change `state$cpuUsage` into `stateFn$cpuUsage`. With that, we wrap the `cpuUsage` state into a function. So, when it's get changed, it won't re-render the component. 571 | 572 | This is how to access the state it inside the `gauge` component. 573 | 574 | ~~~js 575 | var component = FlowComponents.define("guage", function(props) { 576 | this.autorun(function() { 577 | // see now it's a function 578 | var value = props.value(); 579 | this.set("value", value); 580 | }); 581 | }); 582 | ~~~ 583 | 584 | As you can see, now `value` prop is a function. Now it's only reactive within the autorun we've defined. So, now we can actually, control the reactivity as we need. 585 | 586 | Writing `this.autorun` for every prop seems like a boring task. That's why we introduced `this.setFn`. See how to use it. It does the exact same thing we did in the previous example. 587 | 588 | ~~~js 589 | var component = FlowComponents.define("guage", function(props) { 590 | this.setFn("value", props.value); 591 | }); 592 | ~~~ 593 | 594 | ## Content Blocks 595 | 596 | We can use content blocks to write nested components. Here's an example for a loading component which uses content blocks. 597 | 598 | ~~~html 599 | {{#render component="loading" loaded=stateFn$loaded }} 600 | {{>render component="data-viewer"}} 601 | {{else}} 602 | Loading... 603 | {{/render}} 604 | ~~~ 605 | 606 | This is how we can implement the `loading` component 607 | 608 | ~~~js 609 | var component = FlowComponents.define('loading', function(props) { 610 | this.setFn("loaded", props.loaded); 611 | }); 612 | ~~~ 613 | 614 | Here's the template: 615 | ~~~html 616 | 623 | ~~~ 624 | 625 | ## Referencing Child Components 626 | 627 | > This API is experimental. 628 | 629 | So, now we know how to use child components and we've seen some examples. Most of the time you can interact with them by passing actions and passing state functions. 630 | 631 | But sometimes, you may need to refer them individually access their states. Let's look at our myForm component. 632 | 633 | ~~~js 634 | var component = FlowComponents.define("myForm", function() { 635 | 636 | }); 637 | 638 | component.action.updateServer = function(name, address) { 639 | Meteor.call("update-profile", name, address); 640 | }; 641 | ~~~ 642 | 643 | This is the template for myForm. 644 | 645 | ~~~html 646 | 651 | ~~~ 652 | 653 | Here's the event handler for submit. 654 | 655 | ~~~js 656 | Template['myForm'].events({ 657 | "click button": function() { 658 | var name = FlowComponents.child("name").getState("value"); 659 | var address = FlowComponents.child("address").getState("value"); 660 | 661 | FlowComponents.callAction('updateServer', name, address); 662 | } 663 | }); 664 | ~~~ 665 | 666 | See, we could access individual child component by their id and get the state called value. But this API has following characteristics: 667 | 668 | * You can only access child components inside template helpers and event handlers only. 669 | * You can't access it inside the component. (We add this restriction to avoid unnecessary re-renders) 670 | * Unlike an "id" in CSS, here "id" is scope to a component. You can have child components with the same id in few different components. 671 | * You can nest child lookups. 672 | 673 | ## Refer All Child Components 674 | 675 | > This API is experimental. 676 | 677 | This API is just like `FlowComponents.child()`, but it gives all the child components inside the current component. 678 | 679 | ~~~js 680 | var allChildComponents = FlowComponents.children(); 681 | allChildComponents.forEach(function(child) { 682 | console.log(child.getState("value")); 683 | }); 684 | ~~~ 685 | 686 | ## Organizing Large Components 687 | 688 | Sometimes our components could have a large number of states, actions and prototypes. So, it's super hard to put them all in a single file. Luckily, there's a way to put those functions in different files. This is how to do it. 689 | 690 | First create the component and name it like `component.js` 691 | 692 | ~~~js 693 | var component = FlowComponents.define("myForm", function() { 694 | 695 | }); 696 | ~~~ 697 | 698 | Then create a file for `states` with the name `component_states.js`. 699 | 700 | ~~~js 701 | var component = FlowComponents.find("myForm"); 702 | component.state.title = function() { 703 | 704 | }; 705 | ~~~ 706 | 707 | It's very important to define the component before accessing it using `.find()`. That's why we've a naming convention like above. 708 | 709 | Likewise you can group `actions`, `states` and `prototypes` in anyway you like organize your component. 710 | 711 | ## Accessing Component DOM 712 | 713 | We've designed components in a way that to reduce the direct interfacing with the DOM. But in practice, it's hard to do. So, if you want to access the DOM directly inside the component, here are the APIs for that. All these API are scoped to the template of the component. 714 | 715 | * this.$(selector) - get a jQuery object for the given selector 716 | * this.find(selector) - get a one matching element for the given selector 717 | * this.findAll(selector) - get all matching elements to the given selector 718 | 719 | ## How Flow Components different from XXX 720 | 721 | Let's compare flow with some other components and UI related frameworks 722 | 723 | #### Blaze 724 | 725 | Flow Component does not replace or Blaze. Instead it's build on top of the Blaze. We are using Blaze templates to render the user interfaces. When using Flow Components, you may don't need to use template helpers and template instances anymore. But still, we use a lot of cool features of Blaze. 726 | 727 | #### React 728 | 729 | React is a completely different UI framework. There is no built in support for React with flow components. But, there are a few ways to integrate React with Meteor. Flow does not interfere with them. 730 | 731 | If there is a way to render react components inside a Blaze template, then it's possible to use React with Flow. That's because we are using Blaze templates to render the UI. 732 | 733 | #### Polymer / WebComponents 734 | 735 | Answer for this is just the same as for React. 736 | 737 | #### Ionic / [Meteoric](http://meteoric.github.io/) 738 | 739 | Answer for this is just the same as for React. 740 | 741 | #### Angular 742 | 743 | It might be possible to use Angular with Flow Components. But we haven't try that yet. 744 | 745 | #### Blaze 2 746 | 747 | There is an ongoing [proposal](https://meteor.hackpad.com/Proposal-for-Blaze-2-bRAxvfDzCVv) for Blaze 2. It has an it's own component system. It's still in the design phase. Try to look at it. 748 | 749 | We designed Flow Components for our internal use at MeteorHacks. We've build few projects with Flow. So, it's unlikely we'll switch to a new component system unless it has all of our features. 750 | 751 | ## Usage Guidelines 752 | 753 | We use Flow Components alot at MeteorHacks specially on Kadira and BulletProof Meteor. We follow some simple rules to manage components and do inter component communication. 754 | 755 | Here are the guidelines: https://meteorhacks.hackpad.com/Flow-Components-Guideline-gAn4odmDRw3 756 | -------------------------------------------------------------------------------- /browserify.js: -------------------------------------------------------------------------------- 1 | // We can remove this polyfill later on 2 | // When everything is ready 3 | var promise = require('es6-promise'); 4 | promise.polyfill(); -------------------------------------------------------------------------------- /lib/client/action_hub.js: -------------------------------------------------------------------------------- 1 | ActionHub = new EventEmitter(); 2 | ActionHub.setMaxListeners(0); -------------------------------------------------------------------------------- /lib/client/api.js: -------------------------------------------------------------------------------- 1 | FlowComponents = {}; 2 | // notify to avoid promises 3 | FlowComponents._avoidPromises = new Meteor.EnvironmentVariable(); 4 | // private store for only within the package namespace 5 | ComponentsStore = {}; 6 | 7 | // Component definition api 8 | FlowComponents.define = function define(name, constructor) { 9 | var stateDefs = {}; 10 | var actionDefs = {}; 11 | 12 | var ComponentInstanceClass = NewClass(ComponentInstance); 13 | var componentClass = ComponentsStore[name] = NewClass(Component); 14 | 15 | _.extend(componentClass.prototype, { 16 | name: name, 17 | constructor: constructor, 18 | created: [], 19 | rendered: [], 20 | destroyed: [], 21 | stateDefs: stateDefs, 22 | actionDefs: actionDefs, 23 | componentInstanceClass: ComponentInstanceClass 24 | }); 25 | 26 | return FlowComponents._makePublicComponentAPI(componentClass); 27 | }; 28 | 29 | // find a component by it's name 30 | FlowComponents.find = function find(name) { 31 | var componentClass = ComponentsStore[name]; 32 | if(!componentClass) { 33 | throw new Error("can't find a component: ", name); 34 | } 35 | 36 | return FlowComponents._makePublicComponentAPI(componentClass); 37 | }; 38 | 39 | FlowComponents._mixin = function _mixin(componentClass, mixinObject) { 40 | var componentClassProto = componentClass.prototype; 41 | 42 | if(mixinObject.action) { 43 | _.extend(componentClassProto.actionDefs, mixinObject.action); 44 | } 45 | 46 | if(mixinObject.state) { 47 | _.extend(componentClassProto.stateDefs, mixinObject.state); 48 | } 49 | 50 | if(mixinObject.prototype) { 51 | var instanceProto = componentClassProto.componentInstanceClass.prototype; 52 | _.extend(instanceProto, mixinObject.prototype); 53 | } 54 | 55 | // extend with life cycle hooks 56 | _.each(['created', 'rendered', 'destroyed'], function(event) { 57 | if(mixinObject[event]) { 58 | componentClass.prototype[event].push(mixinObject[event]); 59 | } 60 | }); 61 | 62 | return FlowComponents._makePublicComponentAPI(componentClass); 63 | }; 64 | 65 | // get a state of current component 66 | FlowComponents.getState = function getState() { 67 | var currentView = Blaze.currentView; 68 | if(currentView) { 69 | var component = GetComponent(currentView); 70 | if(component) { 71 | return component._getState.apply(component, arguments); 72 | } 73 | } 74 | }; 75 | 76 | // get a state of current component as a function 77 | FlowComponents.getStateFn = function getStateFn() { 78 | var currentView = Blaze.currentView; 79 | if(currentView) { 80 | var component = GetComponent(currentView); 81 | if(component) { 82 | return component._getStateFn.apply(component, arguments); 83 | } 84 | } 85 | }; 86 | 87 | FlowComponents.getProp = function getProp(key) { 88 | var currentView = Blaze.currentView; 89 | if(currentView) { 90 | var component = GetComponent(currentView); 91 | if(component) { 92 | return component._props[key]; 93 | } 94 | } 95 | }; 96 | 97 | // get a children of the current component 98 | FlowComponents.child = function child(childId, currentView) { 99 | var currentView = currentView || Blaze.currentView; 100 | if(currentView) { 101 | var component = GetComponent(currentView); 102 | var child = component._children[childId]; 103 | if(child) { 104 | return FlowComponents._buildProxy(child); 105 | } 106 | } 107 | }; 108 | 109 | FlowComponents.children = function children(currentView) { 110 | var currentView = currentView || Blaze.currentView; 111 | if(currentView) { 112 | var component = GetComponent(currentView); 113 | var children = []; 114 | _.each(component._children, function(child) { 115 | children.push(FlowComponents._buildProxy(child)); 116 | }); 117 | 118 | return children; 119 | } 120 | }; 121 | 122 | // build a proxy api for a child component with only 123 | // `get` and `child` apis 124 | FlowComponents._buildProxy = function _buildProxy(child) { 125 | var proxy = { 126 | getState: function() { 127 | return child._getState.apply(child, arguments); 128 | }, 129 | child: function(childId) { 130 | return FlowComponents.child(childId, child.getView()); 131 | }, 132 | children: function() { 133 | return FlowComponents.children(child.getView()); 134 | }, 135 | getProp: function() { 136 | return child._getProp.apply(child, arguments); 137 | } 138 | }; 139 | 140 | return proxy; 141 | }; 142 | 143 | FlowComponents._makePublicComponentAPI = function _makePublicComponentAPI(c) { 144 | var publicAPI = { 145 | state: c.prototype.stateDefs, 146 | action: c.prototype.actionDefs, 147 | prototype: c.prototype.componentInstanceClass.prototype, 148 | extend: extend 149 | } 150 | 151 | function extend() { 152 | var mixins = _.toArray(arguments); 153 | var publicAPIComponent; 154 | mixins.forEach(function(mixin) { 155 | publicAPIComponent = FlowComponents._mixin(c, mixin); 156 | }); 157 | 158 | return publicAPIComponent; 159 | } 160 | 161 | return publicAPI; 162 | }; 163 | 164 | FlowComponents.callAction = function callAction(action) { 165 | var args = _.toArray(arguments).slice(1); 166 | return this.applyAction(action, args); 167 | }; 168 | 169 | FlowComponents.applyAction = function callAction(action, args) { 170 | var currentView = Blaze.currentView; 171 | 172 | if(!currentView) { 173 | throw new Error(action + " needs to be called within view"); 174 | } 175 | 176 | var component = GetComponent(currentView); 177 | if(!component) { 178 | throw new Error("there is no component available for the action:" + action); 179 | } 180 | 181 | return component.emitPrivateAction(action, args); 182 | }; 183 | 184 | FlowComponents.onAction = function onAction(event, callback) { 185 | ActionHub.on(event, callback); 186 | var controller = { 187 | stop: function() { 188 | ActionHub.removeListener(event, callback); 189 | } 190 | }; 191 | 192 | return controller; 193 | }; -------------------------------------------------------------------------------- /lib/client/component.js: -------------------------------------------------------------------------------- 1 | var instantId = 0; 2 | 3 | Component = function Component(props, wrapperView, parent) { 4 | this.id = ++instantId; 5 | this._instance = new this.componentInstanceClass(this); 6 | this._view = this._createView(props); 7 | this._children = {}; 8 | this._props = props; 9 | this._parent = parent; 10 | 11 | this.flowContentBlock = wrapperView.templateContentBlock; 12 | this.flowElseBlock = wrapperView.templateElseBlock; 13 | this._blockMode = !!(this.flowContentBlock || this.flowElseBlock); 14 | } 15 | 16 | Component.prototype.getView = function getView() { 17 | return this._view; 18 | }; 19 | 20 | Component.prototype._createView = function(props) { 21 | var templateIntance = this._getTemplate(); 22 | var view = Blaze._TemplateWith({}, function() { 23 | return Spacebars.include(templateIntance); 24 | }); 25 | 26 | view.onViewCreated(this._init.bind(this, props, view)); 27 | view._onViewRendered(this._rendered.bind(this)); 28 | view.onViewDestroyed(this._destroyed.bind(this)); 29 | view._component = this; 30 | 31 | return view; 32 | }; 33 | 34 | Component.prototype._getStateFn = function(name) { 35 | var self = this; 36 | return function() { 37 | return function() { 38 | return self._getState(name); 39 | }; 40 | }; 41 | }; 42 | 43 | Component.prototype._getState = function(name) { 44 | var self = this; 45 | var args = _.toArray(arguments); 46 | args.pop(); 47 | args.shift(); 48 | 49 | if(typeof self.stateDefs[name] == "function") { 50 | return self.stateDefs[name].apply(self._instance, args); 51 | } else if(typeof self.stateDefs[name] !== "undefined") { 52 | return self.stateDefs[name]; 53 | } else { 54 | var value = self._instance._state.get(name); 55 | // If we are inside a component rendered as _blockMode, we should allow 56 | // to access the parent component's data context. 57 | // This is how we do it. 58 | if(value === undefined && this._blockMode && self._parent) { 59 | // we can check it from the parent. 60 | return self._parent._getState(name); 61 | } else { 62 | return value; 63 | } 64 | } 65 | }; 66 | 67 | Component.prototype._init = function _init(params, view) { 68 | var self = this; 69 | self._instance._view = view; 70 | 71 | // Invoke mixin's created callbacks 72 | _.each(self.created, function(fn) { 73 | fn.call(self._instance, params); 74 | }); 75 | // Invoke constructor 76 | // this needs to done after the mixin has been called 77 | // otherwise we can't access properties added by mixins 78 | self.constructor.call(self._instance, params); 79 | 80 | // Register mixins' rendered callbacks 81 | _.each(self.rendered, function(fn) { 82 | self._instance.onRendered(fn); 83 | }); 84 | 85 | // Register mixins' destroyed callbacks 86 | _.each(self.destroyed, function(fn) { 87 | self._instance.onDestroyed(fn); 88 | }); 89 | }; 90 | 91 | Component.prototype._getTemplate = function() { 92 | return Template[this.template || this.name]; 93 | }; 94 | 95 | Component.prototype._rendered = function() { 96 | _.each(this._instance._renderedCallback, function(cb) { 97 | // Even though we fired the rendered callback, 98 | // the DOM is not ready yet! 99 | // We can make sure DOM is ready with this trick. 100 | Tracker.afterFlush(cb); 101 | }); 102 | }; 103 | 104 | Component.prototype._destroyed = function() { 105 | _.each(this._instance._destroyedCallbacks, function(cb) { 106 | cb(); 107 | }); 108 | }; 109 | 110 | Component.prototype._getPrivateAction = function(actionName) { 111 | var action = this.actionDefs[actionName]; 112 | // we need to bind the instance like this 113 | // otherwise, we'll get the incorrect instance when getting action 114 | // from a parent 115 | if(action) { 116 | action = action.bind(this._instance); 117 | } 118 | 119 | // When we are inside a block we need to get the action from the parent 120 | if(!action && this._blockMode && this._parent) { 121 | action = this._parent._getPrivateAction(actionName); 122 | } 123 | 124 | return action; 125 | }; 126 | 127 | Component.prototype.emitPrivateAction = function(privateEvent, args) { 128 | var action = this._getPrivateAction(privateEvent); 129 | 130 | if(!action) { 131 | throw new Error("there is no such private action: " + this.name + ".action." + privateEvent); 132 | } 133 | 134 | // action is already bound to it's instance (done by _getPrivateAction) 135 | // that's why we set the context as null 136 | var response = action.apply(null, args); 137 | 138 | // if avoidpromises is true, we need pass the response directly 139 | if(FlowComponents._avoidPromises.get()) { 140 | return response; 141 | } 142 | 143 | if(response instanceof Promise) { 144 | return response; 145 | } else { 146 | // we need to send a promise anyway 147 | return Promise.resolve(response); 148 | } 149 | }; -------------------------------------------------------------------------------- /lib/client/component_instance.js: -------------------------------------------------------------------------------- 1 | var instantId = 0; 2 | 3 | ComponentInstance = function(component) { 4 | this._component = component; 5 | this.id = ++instantId; 6 | this._state = new StateMap(); 7 | this._destroyedCallbacks = []; 8 | this._renderedCallback = []; 9 | }; 10 | 11 | ComponentInstance.prototype.autorun = function(cb) { 12 | var self = this; 13 | var c = Tracker.autorun(function(computation) { 14 | cb.call(self, computation); 15 | }); 16 | self.onDestroyed(function() {c.stop()}); 17 | }; 18 | 19 | ComponentInstance.prototype.get = function(key) { 20 | return this._component._getState(key); 21 | }; 22 | 23 | ComponentInstance.prototype.set = function(key, value, fireAway) { 24 | return this._state.set(key, value, fireAway); 25 | }; 26 | 27 | ComponentInstance.prototype.setFn = function(key, fn, fireAway) { 28 | if(typeof fn !== 'function') return; 29 | 30 | this.autorun(function() { 31 | this.set(key, fn(), fireAway); 32 | }); 33 | }; 34 | 35 | ComponentInstance.prototype.onAction = function(action, callback) { 36 | callback = callback.bind(this); 37 | ActionHub.on(action, callback); 38 | var removeListener = _.once(function() { 39 | ActionHub.removeListener(action, callback); 40 | }); 41 | 42 | this.onDestroyed(removeListener); 43 | 44 | return { 45 | stop: removeListener 46 | }; 47 | }; 48 | 49 | ComponentInstance.prototype.onDestroyed = function(cb) { 50 | this._destroyedCallbacks.push(cb.bind(this)); 51 | }; 52 | 53 | ComponentInstance.prototype.onRendered = function(cb) { 54 | this._renderedCallback.push(cb.bind(this)); 55 | }; 56 | 57 | ComponentInstance.prototype.noPromises = function(cb) { 58 | var self = this; 59 | var response = FlowComponents._avoidPromises.withValue(true, function() { 60 | return cb.call(self); 61 | }); 62 | 63 | return response; 64 | }; 65 | 66 | ComponentInstance.prototype.find = function(query) { 67 | return this.$(query).get(0); 68 | }; 69 | 70 | ComponentInstance.prototype.findAll = function(query) { 71 | return this.$(query).toArray(); 72 | }; 73 | 74 | ComponentInstance.prototype.$ = function(query) { 75 | if(this._view._domrange) { 76 | return this._view._domrange.$(query); 77 | } else { 78 | return $(); 79 | } 80 | }; -------------------------------------------------------------------------------- /lib/client/lookup.js: -------------------------------------------------------------------------------- 1 | var originalLookup = Blaze.View.prototype.lookup; 2 | 3 | Blaze.View.prototype.lookup = function(name, options) { 4 | // FIXME: need a better implementation 5 | if(/^state\$/.test(name)) { 6 | var state = name.replace(/^state\$/, ""); 7 | return FlowComponents.getState.bind(null, state); 8 | } else if(/^stateFn\$/.test(name)) { 9 | var state = name.replace(/^stateFn\$/, ""); 10 | return FlowComponents.getStateFn(state); 11 | } else if(/^action\$/.test(name)) { 12 | var actionName = name.replace(/^action\$/, ""); 13 | return getAction(actionName); 14 | } else if(name == "prop") { 15 | return FlowComponents.getProp; 16 | } else if(name == "flowContentBlock") { 17 | return getBlock(name); 18 | } else if(name == "flowElseBlock") { 19 | return getBlock(name); 20 | } 21 | 22 | return originalLookup.call(this, name, options); 23 | }; 24 | 25 | function getBlock(blockName) { 26 | var currentView = Blaze.currentView; 27 | if(!currentView) return; 28 | 29 | var component = GetComponent(currentView); 30 | if(!component) return; 31 | 32 | return component[blockName]; 33 | } 34 | 35 | function getAction(actionName) { 36 | var component = GetComponent(Blaze.currentView); 37 | if(!component) { 38 | var msg = "There is no component to find an action named: " + actionName; 39 | throw new Error(msg); 40 | } 41 | 42 | // having two functions is important. 43 | // blaze run the first function, that's why we need to create a 44 | // nested function 45 | return function() { 46 | return function() { 47 | return component.emitPrivateAction(actionName, arguments); 48 | } 49 | }; 50 | } -------------------------------------------------------------------------------- /lib/client/render.js: -------------------------------------------------------------------------------- 1 | var ComponentId = 0; 2 | 3 | Template.render = new Template("Template.render", function() { 4 | var wrapperView = this; 5 | var parentComponent = GetComponent(wrapperView); 6 | var componentName = Spacebars.call(wrapperView.lookup("component")); 7 | var partialName = Spacebars.call(wrapperView.lookup("partial")); 8 | var props = Blaze._parentData(0); 9 | 10 | if(partialName) { 11 | var data = Blaze._parentData(1) || {}; 12 | var view = Blaze._TemplateWith(data, function() { 13 | return Spacebars.include(Template[partialName]); 14 | }); 15 | return view; 16 | } 17 | 18 | if(!componentName) { 19 | throw new Error("FlowComponent needs 'component' parameter"); 20 | } 21 | 22 | if(!ComponentsStore[componentName]) { 23 | throw new Error("No such FlowComponent: " + componentName); 24 | } 25 | 26 | var component = 27 | new (ComponentsStore[componentName])(props, wrapperView, parentComponent); 28 | var view = component.getView(); 29 | 30 | if(view) { 31 | setTimeout(function() { 32 | var id = props.id || "cid-" + (++ComponentId); 33 | if(parentComponent) { 34 | parentComponent._children[id] = component; 35 | view.onViewDestroyed(function() { 36 | delete parentComponent._children[id]; 37 | }); 38 | } 39 | }, 0); 40 | } 41 | 42 | return view; 43 | }); -------------------------------------------------------------------------------- /lib/client/state_map.js: -------------------------------------------------------------------------------- 1 | StateMap = function StateMap() { 2 | this._depsMap = {}; 3 | this._valueMap = {}; 4 | }; 5 | 6 | StateMap.prototype.set = function(key, value, fireAway) { 7 | var dep = this._getDep(key); 8 | var existingValue = this._valueMap[key]; 9 | 10 | // this is an optimization to pass big objects without 11 | // cloning and checking for equality 12 | if(fireAway) { 13 | this._valueMap[key] = {value: value, fireAway: true}; 14 | dep.changed(); 15 | return; 16 | } 17 | 18 | if(existingValue) { 19 | if(EJSON.equals(existingValue.value, value)) { 20 | return; 21 | } 22 | } 23 | 24 | this._valueMap[key] = {value: value}; 25 | dep.changed(); 26 | }; 27 | 28 | StateMap.prototype.get = function(key) { 29 | this._getDep(key).depend(); 30 | var info = this._valueMap[key]; 31 | if(!info) { 32 | return info; 33 | } else if(info.fireAway) { 34 | return info.value; 35 | } else { 36 | return EJSON.clone(info.value); 37 | } 38 | }; 39 | 40 | StateMap.prototype._getDep = function(key) { 41 | if(!this._depsMap[key]) { 42 | this._depsMap[key] = new Tracker.Dependency(); 43 | } 44 | 45 | return this._depsMap[key]; 46 | }; -------------------------------------------------------------------------------- /lib/client/utils.js: -------------------------------------------------------------------------------- 1 | GetComponent = function(topView) { 2 | var currView = topView; 3 | for(var lc=0; lc<100; lc++) { 4 | currView = Blaze._getParentView(currView); 5 | if(!currView) { 6 | return null; 7 | } 8 | 9 | var component = currView._component; 10 | if(component) { 11 | return component; 12 | } 13 | } 14 | }; 15 | 16 | NewClass = function (base) { 17 | // We don't have more than 3 args. 18 | // So, 3 args is pretty okay. 19 | var newClass = function(v1, v2, v3) { 20 | base.call(this, v1, v2, v3); 21 | } 22 | 23 | _.extend(newClass.prototype, base.prototype); 24 | return newClass; 25 | }; -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: 'Flow Components', 3 | version: '0.0.41', 4 | git: 'https://github.com/meteorhacks/flow-components', 5 | name: "meteorhacks:flow-components" 6 | }); 7 | 8 | Npm.depends({ 9 | "es6-promise": "2.1.1" 10 | }); 11 | 12 | Package.onUse(function (api) { 13 | api.versionsFrom('1.0'); 14 | api.use('blaze'); 15 | api.use('templating'); 16 | api.use('underscore'); 17 | api.use('raix:eventemitter@0.1.1'); 18 | api.use('cosmos:browserify@0.1.3', 'client'); 19 | 20 | api.addFiles('browserify.js', 'client'); 21 | api.addFiles('lib/client/utils.js', 'client'); 22 | api.addFiles('lib/client/state_map.js', 'client'); 23 | api.addFiles('lib/client/action_hub.js', 'client'); 24 | api.addFiles('lib/client/component.js', 'client'); 25 | api.addFiles('lib/client/component_instance.js', 'client'); 26 | api.addFiles('lib/client/api.js', 'client'); 27 | api.addFiles('lib/client/lookup.js', 'client'); 28 | api.addFiles('lib/client/render.js', 'client'); 29 | api.export(['FlowComponents']); 30 | }); 31 | --------------------------------------------------------------------------------