Introducing Play, A React-Like Web Components Library

Check out this simple Markdown editor, demonstrating Play in action.

We’ve been fans of Web Components since they were first announced. We had the opportunity to experiment with them, and with Polymer, in particular. But we were frankly disappointed in the results. Our team, like many developers, found React, and React-inspired frameworks easier to use. What was missing was the simplicity of React, but for Web Components. So I built it.

Play is a lite weight, React-like library for building real Web Components. This is how you define a simple Hello, World component:

import {define} from "//play.pandastrike.com/v/1.0.0-alpha-00/play.js"

define "x-greeting",

  data:
    greeting: "Hello"

  template: ({greeting}) -> "<h1>#{greeting}, World!</h1>"

  events:
    h1:
      click: -> @data.greeting = "Goodbye"

That component can now by including a <x-greeting/> tag in your HTML, which will render as Hello, World. When you click on it, it changes to Goodbye, World, just as you might expect from looking at the code.

Play features one-way data flow (which, by the way, dates back to Smalltalk MVC, if not before, it just wasn’t called that), virtual DOM-style updates, declarative event handlers, and more. Play also makes it easy to separate the behavior and presentation of a component. And unlike non-native components, they’re truly encapsulated, just like their iOS and Android counterparts, thanks to ShadowDOM. And Play works right now in Chrome and should work any time now in all the major browsers.

Update: This should work in the latest versions of Safari now. And maybe Edge, but I haven’t tried it.

Before we dive into how to build components with Play, let’s explore how Web Components and, by extension, Play, work. Part of my goal with Play was to get out of the way of Web Components, since they are already pretty cool on their own. Let’s start with how you create a Web Component: by defining a class that inherits from HTMLElement.

class Greeting extends HTMLElement

  constructor: -> super()

Defining a component doesn’t do much, though. We also need to register it with the browser. In doing so, we also give it a tag name.

customElements.define "x-greeting", Greeting

We can now use it in our markup and the browser will use our Greeting class to instantiate the associated DOM element. That, by itself, is pretty cool, because now we can associate behavior and state with that element.

However, we also want to associate presentation—that is, HTML and CSS—with it. We could easily just attach a DOM tree like we might with any DOM element, but it wouldn’t be properly encapsulated. We need to attach a Shadow DOM element instead, which is hidden—encapsulated—from the parent document. We can do this with the attachShadow method.

class Greeting extends HTMLElement

  constructor: ->
  	super()
  	@attachShadow mode: "open"

We can’t do too much else, though, because we need to wait for the element to be wired up to the DOM tree. That’s what the connectedCallback is for.

class Greeting extends HTMLElement

  constructor: ->
  	super()
  	@attachShadow mode: "open"

  connectedCallback: ->
	 @shadowRoot.innerHTML = "<h1>Hello, World</h1>"

Now, when we use the x-greeting element in our markup, it will display a heading of “Hello, World.” What’s more, it’s not visible to the parent document, except via the Shadow DOM. We can’t go to the browser console and try to access it via document.querySelector. We can only access it via our element’s shadowRoot, which is a DocumentFragment instance. Similarly, any styles in the parent document won’t affect our Shadow DOM.

From there, everything pretty much works like a normal DOM element. We can add an event handler to change the greeting if you click on it.

class Greeting extends HTMLElement

  constructor: ->
  	super()
  	@attachShadow mode: "open"

  connectedCallback: ->
	 @shadowRoot.innerHTML = "<h1>Hello, World</h1>"
  	@shadowRoot
  	.querySelector "h1"
  	.addEventListener "click", =>
  		@shadowRoot.innerHTML = "<h1>Goodbye, World</h1>"

Basically, once you attach a Shadow DOM root to your element, you’re in business. Of course, everyone knows that assigning to innerHTML is a bad idea. And using addEventListener is tedious and error-prone and so on. But the thing is, we already have solutions to those problems. The only difference is we’re operating on our Shadow DOM root instead of the document root.

For DOM updates, Play uses the fantastic diffHTML library. For event listeners, Play places them on the Shadow root and uses the native DOM.match function to filter them based on selector. That way, we don’t have to put the listeners in our generated markup as attributes, like onclick. That means we can have clean, semantic markup, decoupled from behavior.

Play also establishes a few additional conventions so that it can take care of the details for you. First, state is kept in the data property. Second, rendering is done via a template method that takes the data object as an argument and returns an HTML string. How you generate this string is up to you. This is diffed against the existing DOM tree and patched. Third, event handlers are specified by the events property, which should be a dictionary of selectors, like h1.

Our original Hello, World example follows these conventions. The define function takes care of creating the component, registering it, attaching the shadow DOM, wrapping the data element in a Proxy to re-render when changes are made, calling the template function, diffing and patching the DOM, attaching the event handlers, and so on.

define "x-greeting",

  data:
    greeting: "Hello"

  template: ({greeting}) -> "<h1>#{greeting}, World!</h1>"

  events:
    h1:
      click: -> @data.greeting = "Goodbye"

We’re not using JSX here, but if you really like typing pointing brackets, there’s nothing stopping you from using it. But you could also use JavaScript templates, Pug templates wrapped as JavaScript functions, or whatever else. All Play knows is that it must call the template function and pass it the component’s state.

Unlike Polymer, Play loads components as JavaScript modules, so you can load them more or less the way you load any other JavaScript library. Play uses normal stylesheets (or link tags) to allow a parent to style a component, so there’s nothing tricky to learn there, although you may need to unlearn a few things, like tricks for CSS scoping, because you won’t need them anymore!

The current version comes in at 1K gzipped, but that doesn’t count the Web Components polyfill (26K), and diffHTML (10K). That’s comparable to React and to Polymer, only Play gives you the best of both worlds—the simplicity of React with browser-native encapsulation, thanks to Web Components.

Of course, Play is lacking the community, support, and ecosystem of React or Polymer, but we’ll continue to develop it and blog about it. And if you want to check it out or contribute, please check it out!