I Created Cell.js to Tackle the Root of All Evil: Dependency

Published Jul 12, 2017Last updated Jan 07, 2018
I Created Cell.js to Tackle the Root of All Evil: Dependency

My name is Ethan. I’ve worked on a project called Jasonette, a framework that lets you build cross-platform native iOS & Android apps using just a JSON markup.

And most recently I released Cell.js, a new type of front-end web app framework.

The problem I wanted to solve

Once upon a time you could build a website using HTML, JavaScript, and CSS, with no additional tools. But this all changed in the last couple of years.

In 2017, just to use any JavaScript library, we need to "install" the library using npm install, compile all the 3rd party HTML templates (because templates are not supported by the browser natively), and package all the code into a single bundle.js file using webpack. And since we've already lost the war against build tools anyway, we might as well go all out and throw another build tool into the mix: Babel. With Babel, we write code in languages that won't work on the browser, but that's ok because Babel will transpile them all. So forth, so forth.

I am not really a fan of this trend because it just doesn't feel right. We're intentionally creating problems then inventing ways to solve them. And once we find solutions to these problems, we throw even more problems at them, because we now can. But the thing is, we're making everything more and more complicated during all this. And complication inevitably brings us a new set of problems, which we again set out to tackle. This is a never-ending downward spiral.

It would be great if we could get rid of all this and end up with a workflow that looks like this (the right side):

process.jpg

And this is why I started working on Cell.

For a quick background, I also maintain another open source project called Jasonette. The core idea of Jasonette is to make it super simple to build native mobile apps by getting rid of all the complicated and bureaucratic parts of making an app.

For example, instead of re-compiling your app every time you make an update, you simply need to change your app’s JSON markup from the server. No need to touch Android studio or Xcode. The entire app logic is directly streamed from your server to the app as JSON, which then self-constructs into a native app instantly. It basically works like web browsers, except they’re all native. This frees you from having to worry about all the unnecessary and complicated details that have nothing to do with the application logic.

A few months ago, I set out to work on a web version of Jasonette. I experimented with building on top of several popular JavaScript frameworks, but quickly realized that none of them fit my purpose. The whole point of Jasonette was to get rid of all the bureaucracy that have nothing to do with the actual app. With that said, if I were to build on top of any existing JavaScript framework, I would have already lost the war – these web app frameworks come with bureaucracy built-in, in the form of build tools, dependency management, code transpiring, template compiling, etc.

So why is it so complicated? There are clearly good reasons why we have these tools, right?

Well here’s my theory: I think the root of all evil is "dependency".

  • Because we depend on a syntax that's not yet supported (or will never be supported) by all browsers, we need to transpile them => See ES6, JSX, etc.
  • Because we depend on libraries that depend on other libraries, the dependency comes by default if we ever use any of these libraries. It’s like a Russian doll of dependencies.
  • Because most JavaScript frameworks are class based — meaning you have to inherit/extend them — you have to import/require them and instantiate. This type of tightly coupled pattern introduces yet another dimension of dependency.

The idea

Here's the idea. What if we go all the way back to where it all started and think about it. What if we didn't have dependencies to begin with? What if everything was decentralized and loosely coupled instead of through strict dependencies? Then maybe we can solve the problem in a more fundamental manner.

domtree.jpg

The idea behind Cell is:

  1. We can probably simplify a lot of things if we can get rid of dependencies from everything, from code to syntax to development workflow.
  2. We should be able to get rid of dependencies by focusing on building a minimal autonomous building block (HTML elements) instead of trying to build a full-stack monolithic framework. Each of these building blocks can function as a completely encapsulated application execution container.
  3. They can then be composed to create complex applications. Because each building block is self-contained, there's no inherent dependency.

I had been thinking a lot about this problem while working on my other project—Jasonette—and had a couple of interesting ideas on this. So I decided to try.

Inspiration

To build a decentralized-first framework, I looked for inspiration from emergent systems. And there's nothing better than the mother nature when it comes to emergent systems. More specifically, Cell was inspired by how real life cells work to construct complex life forms.

Coming back to the web application context, Cell.js has a clear goal: Create minimal autonomous HTML elements that can be composed to build complex systems. In this sense, Cell.js itself is not really comparable to other full-stack frameworks, although in some ways it is.

What I built: Cell, a self-driving app framework

Cell is a front-end web app framework. Web apps are built using the DOM, data, and an application logic that binds and controls them all.

Normally the job of a web app framework is to provide a centralized MVC (or similar) that takes care of all this throughout the entire app lifecycle.

Cell, on the other hand, isn't powered by a centralized MVC structure. Instead, it only focuses on creating an autonomous DOM, which means it builds HTML elements, and makes sure each element contains its own data and application context, so they can function on their own without external control.

architecture.jpg

Getting started

With Cell, there is no "installation." Instead, you use it just like any other front-end JavaScript library. All you need to do is include <script src="https://www.celljs.org/cell.js"></script> in your HTML and start writing.

Cell doesn't have any API methods or classes. Instead, you write apps by writing JavaScript objects that follow a small set of rules. Here’s a fully-functional Cell app example.

<html>
<script src="https://www.celljs.org/cell.js"></script>
<script>
SynchronizedInput = {
  $cell: true, $type: "body", style: "padding: 30px;",
  $components: [
    { $type: "div", id: "h", $text: "Type something below" },
    { $type: "input", onkeyup: function(e){ document.querySelector("#h").$text = this.value} }
  ]
}
</script>
</html>

Looking at the code, you'll notice we have no HTML markup (or template) and only have a single variable. Cell's job is to automatically transform this variable into a DOM tree.

How does Cell know which variable to transform? All you have to do is follow certain rules to let Cell know. Here's the first one:

When you declare a variable and want to let Cell turn it into an element, you simply attach a "$cell" attribute to the object. As you can see, that's exactly what the code above does.

There are a couple of other special keywords like $cell. All special keywords are prefixed by $ (There are 6 in total). In this example we see $type, $components, and $text, so let me explain those.

$type is used to describe the node type, such as "input", "div", etc.

$text is used to describe the text inside a node.

$component is an array of Cell objects to describe the children. You can use $components to nest elements.

And the rest of the attributes translate 1:1 to DOM attributes.

Using just these rules you can create a complete DOM tree with a single variable, complete with event handlers. Here's what the above code looks like when you open in a browser:

sync.gif

However, the real power for Cell lies in its ability to store data and application directly on each element. This is also easy.

If you're interested, you can learn more at https://www.celljs.org and check out actual functional examples at https://play.celljs.org.

Technologies used

The whole point of Cell was that it should be an atomic unit that's 100% independent. So there really is no 3rd party technology used. It's all pure JavaScript with zero external library. Cell relies 100% on the browser API and web standards.

That said, we do utilize 3rd party libraries for unit tests and linting. These are very important for Cell because of its emergent nature. One little error in the Cell core code may snowball into a completely unexpected result, so we need to make sure every part of the Cell functions as intended.

For testing, we use Mocha, jsdom, and Sinon.JS. Sinon.JS was a later addition but has become absolutely essential. It's mostly used to make sure some state change automatically triggers another function, and also to verify that each function is called exactly the expected number of times. One little bug may cause a huge unexpected result. This emergent behavior is the strength of the library but at the same time is the most challenging part because it's hard to debug sometimes. This is why I've written a lot of unit tests.

Technical challenges

Cell is written in ES5 — this is very important because unlike ES6, ES5 currently works in all browsers, which means there’s no need to transpile using Babel. I fought very hard to keep all kinds of dependencies out of Cell. It had to be 100% dependency free if I wanted to get rid of webpack, npm, Babel, etc. from the picture altogether, and instead let users use the library simply by including a single line of <script src>. Also some ES6 features are not implemented in IE (and will never be implemented) so I didn't use them, no matter how powerful they were.

One of the challenges was to devise a way to listen to an object's attribute change event. Cell takes advantage of JavaScript's Object.defineProperty API (introduced in ES5) to monitor any change in an object's attributes. But for a while, I looked into a new ES6 feature called "proxy" to deal with this problem. Proxy is more powerful because you can listen to all attribute change events without having to specify all the keys you want to listen to.

However, the proxy feature is not supported on IE browsers. So I had to come up with a way to use Object.defineProperty to listen to attribute changes without using a proxy. This was a difficult feat but I somehow got it to work.

Key learnings

One lesson I learned is: you don't always have to be dogmatic about best practices and anti-patterns. In fact, you should try to actively rethink why something is considered an anti-pattern or a best practice because these are contextual concepts. They only make sense in the existing world. If what you're trying to do is build something different, it may make sense to question your fundamental beliefs about everything. I went through multiple moments like this while working on Cell because sometimes the only way to move forward was to make decisions that most experienced developers will think as anti-patterns.

This is one of the reasons why Cell's source code does not use any of the traditional programming concepts but use cell biology concepts like gene, genotype, phenotype, nucleus, membrane, etc. Using an entirely different set of terminology helped me consciously reframe a lot of problems I faced.

If you like the idea behind Cell, please feel free to try it out and also contribute if you like. You can check it out at https://www.celljs.org and the GitHub repo here.

Discover and read more posts from ethan
get started