• Stars
    star
    132
  • Rank 274,205 (Top 6 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 10 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Build your Meteor app with Components.

Flow Components

This project is still work in progress and does not have unit and integration tests. We are directly pushing changes to master. There's no plan to change current APIs, but we don't have a guarantee.

Build your Meteor app with Components.

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.

We've also added some handy features which will help you control reactivity when building a large application.

Table of Contents

Why?

When we started building 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.

So, we thought a lot and played with a lot of UI frameworks and concepts. That includes React and Flux. After a lot of iterations and experiments, Flow Components is our component framework which is a part of the Flow Architecture for Meteor.

Getting Started

Let's create a very simple component. It's a typical Hello World example.

First add Flow Components into your app.

meteor add meteorhacks:flow-components

Then create directory on the client directory of your Meteor app and put these files.

Here's the JavaScript file, (component.js) which is the component.

var component = FlowComponents.define('hello-component', function(props) {
  console.log("A component created!");
  this.name = props.name;
  
  this.setRandomGreeting();
  // change the greeting for every 300 millis
  setInterval(this.setRandomGreeting.bind(this), 300);
});

component.prototype.setRandomGreeting = function() {
  var greetings = ["awesome", "nice", "cool", "kind"];
  var randomGreeting = greetings[Math.floor(greetings.length * Math.random())];
  this.set("greeting", randomGreeting)
};

component.state.message = function() {
  return this.name + ", you are " + this.get("greeting") + "!";
};

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.

<template name="hello-component">
  <div class="hello-component">
    Welcome Message: {{state$message}}
  </div>
</template>

Then, let's add a CSS file for our component.

.hello-component {
  font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial;
  font-size: 16px;
  margin: 10px;
}

That's all. We've created our first component. We can render it anywhere we like. Here's how to do it.

{{> render component="hello-component" name="Arunoda"}}
{{> render component="hello-component" name="Sacha"}}

Then, this is how it's looks like:

Flow Components in Action

Check this repo for the complete source code.

States

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.

There are couple of ways, you can get a state. This is the first way.

Creating states using component.state.

var component = FlowComponents.define('my-component');
component.state.message = function() {
  return "some value";
};

Above function is running in a reactive context. So, you can use any kind of reactive variables, Session variables and MiniMongo APIs inside it.

Context of the above function is the component itself. That's the main difference from a template helper.

Creating states using this.set

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.

var component = FlowComponents.define('my-component', function() {
  this.set("message", "some value");
});

this.set with large object

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. But there is a solution to avoid that. Here's how to do it:

var component = FlowComponents.define('my-component', function(params) {
  var fireAway = true;
  this.set("veryLargeObject", params.obj, fireAway);
});

Now, flow component does not do any cloning or equality checks. It simply invalidate all the computations looks for this key.

Accessing states inside a template

You can access the state anywhere in the template as below:

<template name='my-component'>
  Message is: {{ state$message }}
</template>

To access the state, just prefix it with state$. You can even access the state inside nested templates in a given component.

Accessing states inside a component with this.get

You can also access the state inside the component with this this.get API like this:

component.state.messageWithName = function() {
  var message = this.get("message");
  return "Arunoda, " + message;
};

this.get is reactive and available anywhere inside the component.

Actions

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.

This is how to create an action:

var component = FlowComponents.define('my-component');
component.action.changeMessage = function(someValue) {
  this.set("message", someValue);
};

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.

There are few ways to call an action. Let's look at them.

Via an DOM event handler

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.

Template['my-component'].events({
  "click button": function() {
    var message = $('.textbox').val();
    FlowComponents.callAction("changeMessage", message);
  }
});

Via Props (aka: Action Passing)

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.

Let's say we are rendering a component called input inside our main component:

{{> render component="input" onSubmit=action$changeMessage }}

Then inside the input component, it calls the onSubmit property like this:

var component = FlowComponents.define("input", function(props) {
  this.onSubmit = props.onSubmit;
});

component.action.submitMessage = function(message) {
  this.onSubmit(message);
};

We also describe this method as "action passing". This is the basic building block for creating nested components.

Actions and Promises

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.

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.

See:

var component = FlowComponents.define("input", function(props) {
  this.onSubmit = props.onSubmit;
});

component.action.submitMessage = function(message) {
  var self = this;
  self.set('loading', true);

  var promise = this.onSubmit(message);
  promise.catch(function(err) {
    self.set('errorMessage', err.message);
  }).then(function() {
    self.set('loading', false);
  });
};

Checkout this sample repo for a complete example.

Disable Promises Temporarily

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.

var component = FlowComponents.define("input", function(props) {
  this.onNewTooltip = props.onNewTooltip;
  this.dataFn = props.dataFn;
  this.autorun(this.formatTooltips);
});

component.prototype.formatTooltips = function() {
  var data = this.dataFn();
  var formattedData = this.noPromises(function() {
    var self = this;
    return data.map(function(item) {
      return self.onNewTooltip(item);
    });
  });

  return formattedData;
};

Props

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:

{{> render component="input" text="This is great" }}

Then you can access it inside the component like this:

var component = FlowComponents.define("input", function(props) {
  console.log("Text is: ", props.text);
});

You can set any number of props you like:

{{> render component="input"
      text="This is great"
      backgroundColor="#736634"
      fontSize=20
}}

You can pass a state into a prop like this:

{{> render component="input" text=state$message }}

We've previously talked about how to pass an action like this:

{{> render component="input" onSubmit=action$message }}

Prototypes

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.

var component = FlowComponents.define('hello-component', function(props) {
  // see how we are using the prototype
  this.setRandomGreeting();
  setInterval(this.setRandomGreeting.bind(this), 300);
});

// This is a prototype
component.prototype.setRandomGreeting = function() {
  var greetings = ["awesome", "nice", "cool", "kind"];
  var randomGreeting = messages[Math.floor(messages.length * Math.random())];
  this.set("greeting", randomGreeting)
};

In that, we've created a setRandomGreeting prototype and call in when a component is creating.

We've also calling it for every 300 milliseconds via the setInterval.

Extending Components with Mixins

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.

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 :)

ComponentSubs = {
  prototype: {},
  action: {},
  state: {}
};

// calls when the component is creating
ComponentSubs.created = function() {
  this._subs = [];
};

// calls when the component is rendered
ComponentSubs.rendered = function() {};

// calls when the component is destroyed
ComponentSubs.destroyed = function() {
  this.stopSubscriptions();
};

ComponentSubs.prototype.subscribe = function() {
  var sub = Meteor.subscribe.apply(null, arguments);
  this._subs.push(sub);
  return sub;
};

ComponentSubs.prototype.ready = function() {
  var ready = true;
  this._subs.forEach(function(sub) {
    ready = ready && sub.ready();
  });

  return ready;
};

ComponentSubs.prototype.stopSubscriptions = function() {
  this._subs.forEach(function(sub) {
    sub.stop();
  });
  this._subs = [];
};

ComponentSubs.state.isReady = function() {
  return this.ready();
};

ComponentSubs.action.stopSubscriptions = function() {
  this.stopSubscriptions();
};

Now you can extend your component with the mixin we've created. Check this:

var component = FlowComponents.define("my-component", function() {
  // you can use like to this subscribe
  this.subscribe("mysubscription");
});

// extend your component with Mixins
component.extend(ComponentSubs);

// you can use it in an action like this
component.action.loadMore = function() {
  this.stopSubscriptions();
  this.subscribe("mysubscription", {limit: 200});
};

You can use isReady state inside the template like this:

<template name="my-component">
  {{#if state$isReady}}
    <!-- do something with your data -->
  {{else}}
    Loading...
  {{/if}}
</template>

Extending Components with Nested Components

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.

There is no special APIs for that. But you can create a component which uses few other components inside that.

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:

var component = FlowComponents.define("loading", function(props) {
  this.set("loadingComponent", props.loadingComponent);
});

This is the UI for that:

{{#if state$loadingComponent}}
  {{> render component=state$loadingComponent }}
{{else}}
  Loading...
{{/if}}

Life Cycle Events

A Component has few different events. Here they are:

  • created - After the component instant created
  • rendered - After the component rendered to the screen
  • destroyed - After the component destroyed

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.

var component = FlowComponents.define("hello", function(props) {
  console.log("Component created with props:", props);
});

In the created event callback you can get the props as the first argument.

For other two events, they can be access anywhere inside the component like with:

  • rendered - this.onRendered(function() {})
  • destroyed - this.onDestroyed(function() {})

Check this example:

var component = FlowComponents.define("hello", function() {
  this.onRendered(function() {
    console.log("Rendered to the screen.");
  });

  this.onDestroyed(function() {
    console.log("Component destroyed.");
  });
});

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.

var component = FlowComponents.define("hello", function() {
  this.onRendered(function() {
    console.log("Rendered to the screen.");

    this.onDestroyed(function() {
      console.log("Component destroyed.");
    });
  });
});

Autoruns

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:

var component = FlowComponents.define("hello", function() {
  this.autorun(function() {
    var posts = Posts.fetch();
    this.set("postCount", posts.length);
  });
});

Note: Context of the callback for this.autorun is also the component. That's why calling this.set is possible inside the autorun.

State Functions

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:

var component = FlowComponents.define("parent", function() {
  var self = this;
  setInterval(function() {
    var usage = Math.ceil(Math.random() * 100);
    this.set("cpuUsage", usage);
  }, 100);
});

This is the template of parent:

<template name="parent">
  {{> render component="gauge" value=state$cpuUsage }}
</template>

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?

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.

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.

var component = FlowComponents.define("guage", function(props) {
  this.set('value', props.value);
});

This is the template:

<template name="guage">
  Value is: {{ state$value }}
</template>

Converting it to use State Functions

Let's change the parent template like this:

<template name="parent">
  {{> render component="gauge" value=stateFn$cpuUsage }}
</template>

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.

This is how to access the state it inside the gauge component.

var component = FlowComponents.define("guage", function(props) {
  this.autorun(function() {
    // see now it's a function
    var value = props.value();
    this.set("value", value);
  });
});

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.

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.

var component = FlowComponents.define("guage", function(props) {
  this.setFn("value", props.value);
});

Content Blocks

We can use content blocks to write nested components. Here's an example for a loading component which uses content blocks.

{{#render component="loading" loaded=stateFn$loaded }}
  {{>render component="data-viewer"}}
{{else}}
  Loading...
{{/render}}

This is how we can implement the loading component

var component = FlowComponents.define('loading', function(props) {
  this.setFn("loaded", props.loaded);
});

Here's the template:

<template name="loading">
  {{#if state$loaded}}
    {{> flowContentBlock }}
  {{else}}
    {{> flowElseBlock }}
  {{/if}}
</template>

Referencing Child Components

This API is experimental.

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.

But sometimes, you may need to refer them individually access their states. Let's look at our myForm component.

var component = FlowComponents.define("myForm", function() {

});

component.action.updateServer = function(name, address) {
  Meteor.call("update-profile", name, address);
};

This is the template for myForm.

<template name="myFrom">
  {{> render component="input" id="name" }}
  {{> render component="input" id="address" }}
  <button>Submit</button>
</template>

Here's the event handler for submit.

Template['myForm'].events({
  "click button": function() {
    var name = FlowComponents.child("name").getState("value");
    var address = FlowComponents.child("address").getState("value");

    FlowComponents.callAction('updateServer', name, address);
  }
});

See, we could access individual child component by their id and get the state called value. But this API has following characteristics:

  • You can only access child components inside template helpers and event handlers only.
  • You can't access it inside the component. (We add this restriction to avoid unnecessary re-renders)
  • 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.
  • You can nest child lookups.

Refer All Child Components

This API is experimental.

This API is just like FlowComponents.child(), but it gives all the child components inside the current component.

var allChildComponents = FlowComponents.children();
allChildComponents.forEach(function(child) {
  console.log(child.getState("value"));
});

Organizing Large Components

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.

First create the component and name it like component.js

var component = FlowComponents.define("myForm", function() {

});

Then create a file for states with the name component_states.js.

var component = FlowComponents.find("myForm");
component.state.title = function() {

};

It's very important to define the component before accessing it using .find(). That's why we've a naming convention like above.

Likewise you can group actions, states and prototypes in anyway you like organize your component.

Accessing Component DOM

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.

  • this.$(selector) - get a jQuery object for the given selector
  • this.find(selector) - get a one matching element for the given selector
  • this.findAll(selector) - get all matching elements to the given selector

How Flow Components different from XXX

Let's compare flow with some other components and UI related frameworks

Blaze

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.

React

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.

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.

Polymer / WebComponents

Answer for this is just the same as for React.

Ionic / Meteoric

Answer for this is just the same as for React.

Angular

It might be possible to use Angular with Flow Components. But we haven't try that yet.

Blaze 2

There is an ongoing proposal for Blaze 2. It has an it's own component system. It's still in the design phase. Try to look at it.

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.

Usage Guidelines

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.

Here are the guidelines: https://meteorhacks.hackpad.com/Flow-Components-Guideline-gAn4odmDRw3

More Repositories

1

cluster

Clustering solution for Meteor with load balancing and service discovery
JavaScript
631
star
2

npm

Complete NPM integration for Meteor
JavaScript
508
star
3

meteord

MeteorD - Docker Runtime for Meteor Apps for Production Deployments
Shell
439
star
4

meteor-ssr

Server Side Rendering for Meteor
JavaScript
264
star
5

sikka

Sikka - A Firewall for Meteor Apps
JavaScript
258
star
6

kadira

Performance Monitoring for Meteor
JavaScript
217
star
7

meteor-aggregate

Proper MongoDB aggregations support for Meteor
JavaScript
189
star
8

picker

Server Side Router for Meteor
JavaScript
182
star
9

meteor-down

Load testing for Meteor
JavaScript
175
star
10

search-source

Reactive Data Source for Search
JavaScript
146
star
11

unblock

this.unblock inside publications :D
JavaScript
87
star
12

meteor-inject-initial

Allow injection of arbitrary data to initial Meteor HTML page
JavaScript
80
star
13

meteor-async

Set of async utilities to work with NPM modules inside Meteor
JavaScript
63
star
14

goddp

DDP Server implemented with golang
Go
47
star
15

meteorx

Exposing some of the internal Meteor API prototypes
JavaScript
41
star
16

mup-frontend-server

Frontend Server for Meteor Up
Nginx
37
star
17

kube-init

Easiest way to deploy a Kubernetes Cluster to learn Kubernetes
Shell
34
star
18

zones

Zone.js integration for meteor
JavaScript
30
star
19

meteorhacks.github.io

MeteorHacks Website
HTML
29
star
20

hyperkube

Hyperkube
Shell
20
star
21

inject-data

A way to inject data to the client with initial HTML
JavaScript
18
star
22

kadira-profiler

CPU Profiling support for Kadira
JavaScript
14
star
23

bddp

DDP like binary protocol implemented using cap'n proto
Go
12
star
24

repeeet

making repeeet tweeting super simple
JavaScript
11
star
25

code-standards

Code Standards for MeteorHacks projects
9
star
26

js-pipes

MongoDB aggregation pipeline implementation in JavaScript
JavaScript
9
star
27

meteor-customer-io

Customer.io Integration for Meteor
JavaScript
9
star
28

cluster-performance

Performance Test for Cluster
JavaScript
8
star
29

docker-librato

Forward all stats from all running docker containers to Librato
JavaScript
8
star
30

kdb

ACID High Performance Time Series DB for any kind of storage - No it isn't
Go
7
star
31

meteor-todo-app

Simple Todo App with Meteor
JavaScript
5
star
32

gocluster

Meteor Cluster protocol implemented for Go
Go
4
star
33

meteor-collection-utils

Expose some underline collection apis
JavaScript
4
star
34

find-faster-chat-demo

Simple Chat To Demo Meteor Fast Finder Use
JavaScript
3
star
35

ddptest

Test DDP servers
JavaScript
3
star
36

zones-simple-example

Simple Example Meteor App with Zones
JavaScript
2
star
37

mongo-search

Simple API to use MongoDB Full Text search with Meteor
2
star
38

kmdb

metric database powered by kdb
Go
2
star
39

meteor-down-backdoor

Backdoor Meteor package for MeteorDown
JavaScript
2
star
40

kadira-binary-deps

Binary Dependencies for Kadira
JavaScript
2
star
41

simple-rpc-go

A really simple and fast binary RPC middleware
Go
2
star
42

kmdb-node

NodeJS client for kmdb
JavaScript
2
star
43

nsqd-to-librato

Send NSQ stats to librato
JavaScript
2
star
44

lean-components-example

lean-components-example
JavaScript
2
star
45

meteor-zone-example

Example meteor app for meteor-zone package
JavaScript
1
star
46

node-histo-utils

A set of utilities to create, merge and manage histograms
JavaScript
1
star
47

blaze-ext

Create blaze templates with .blaze extension
1
star