menu
Zorium

The CoffeeScript Web Framework

(╯°□°)╯︵ ┻━┻
v1.0.0

Example

z = require 'zorium'

class AppComponent
  constructor: ->
    @state = z.state
      name: 'Zorium'

  render: =>
    {name} = @state.getValue()

    z 'div.zorium',
      z 'p.text',
        "The Future is #{name}"

z.render document.body, new AppComponent()
# <div class="zorium"><p class="text">The Future is Zorium</p></div>

Features

Installation

npm install --save zorium

Note that zorium exports raw coffeescript.
See Webpack Configuration for recommended usage.

Contribute

npm install
npm test

Documentation - zorium-site

IRC: #zorium - chat.freenode.net

Tutorial

First Component

Zorium components make up the backbone of your application.
They should be modular, composable, and simple.
The render method of each component may be called many times, and must be idempotent.

z = require 'zorium'

module.exports = class HelloWorld
  render: ->
    z 'h1.z-hello-world',
      'hello world' # Change me

Stateful Components

Just rendering DOM doesn't help us much. Let's add some state to make a counter.

z = require 'zorium'

module.exports = class Counter
  constructor: ->
    @state = z.state
      count: 0

  handleClick: =>
    {count} = @state.getValue()
    @state.set count: count + 1

  render: =>
    {count} = @state.getValue()

    z '.z-counter',
      z 'h1', "#{count}"
      z 'button',
        style: fontSize: '16px'
        onclick: @handleClick
        'increment'

Composing Components

Alright, let's make things interesting. Components compose just like regular DOM elements.
There is no magic, just passing parameters to the render() method

z = require 'zorium'
Button = require 'zorium-paper/button'

class Brick
  render: ({size}) ->
    z '.z-brick',
      style: # normally this would go in a .styl file
        width: "#{size * 20}px"
        height: '20px'
        background: '#FF9800'
        margin: '20px'

module.exports = class House
  constructor: ->
    @$button = new Button
      isRaised: true
      color: 'blue'
    @$bricks = _.map _.range(10), -> new Brick()

  render: =>
    z '.z-house',
      z @$button, # A Material Design Button
        $children: 'Hello World'

      _.map @$bricks, ($brick, i) -> # Bricks!
        z $brick, {size: i + 1}

Streams

Finally, let's work some FRP awesomeness into our components.
Remember that streams should be cold, meaning that they only emit values when subscribe() is called.
Rx Observables are first-class citizens in Zorium

z = require 'zorium'
Rx = require 'rx-lite'

module.exports = class Elegant
  constructor: ->
    @state = z.state
      pureAwesome: Rx.Observable.defer ->
        Rx.Observable.interval(500)
      model: Rx.Observable.defer ->
        window.fetch '/demo'
        .then (res) -> res.json()

  render: =>
    {pureAwesome, model} = @state.getValue()

    z '.z-elegant',
      z 'h1',
        "Pure Awesome: #{pureAwesome}"
      if model? # Yes, you can use if statements!
        z 'pre',
          "model: #{JSON.stringify(model, null, 2)}"

Server-side rendering

Zorium is built from the ground-up to support server-side rendering.
All application code should simply work in a plain Node.js environment, without any special modification.
Network requests can run seamlessly server-side to generate a complete DOM (with caching).
For a more in-depth example, check out the Zorium Seed project.

class App
  render: ({req, res}) ->
    z 'html',
      z 'head',
        z 'title', 'Simply Zorium'
      z 'body',
        z '#zorium-root',
          switch path
            when '/'
              z 'div', 'Welcome'
            else
              res.status? 404 # server-side
              z 'div', 'Oh No! 404'

express = require 'express'
app = express()
app.use (req, res) ->
  z.renderToString z new App(), {req, res}
  .then (html) ->
    res.send '<!DOCTYPE html>' + html
  .catch (err) ->
    if err.html
      log.trace err
      res.send '<!DOCTYPE html>' + err.html
    else
      next err
app.listen 3000

Core API

z()

Basic DOM construction

###
@returns virtual-dom tree
###

z '.container' # <div class='container'></div>
z '#layout' # <div id='layout'></div>
z 'span' # <span></span
z 'a', {
  href: 'http://google.com' # <a href='http://google.com' style='border:1px solid red;'></a>
  style:
      border: '1px solid red'
}
z 'span', 'text!'  # <span>text!</span
z 'ul',           # <ul>
  z 'li', 'item 1'    # <li>item 1</li>
  z 'li', 'item 2'    # <li>item 2</li>
                  # </ul>

z 'div',    # <div>
  z 'div',     # <div>
    z 'span'      # <span></span>
    z 'img'       # <img></img>
               # </div>
            # </div>

Events

# <button>click me</button>
z 'button', {
  onclick: (e) -> alert(1)
  ontouchstart: (e) -> alert(2)
  # ...
}, 'click me'

Zorium Components

Basic

  • must have a render() method
  • can be used the same as a DOM tag
class HelloWorldComponent
  render: ->
    z 'span', 'Hello World'

$hello = new HelloWorldComponent()

z 'div',
  z $hello # <div><span>Hello World</span></div>

Parameters

Parameters can also be passed to the render method

class A
  render: ({name}) ->
    z 'div', "Hello #{name}!"

$a = new A()

z 'div',
  z $a, {name: 'Zorium'} # <div><div>Hello Zorium!</div></div>

Lifecycle Hooks

  • afterMount() called with element when inserted into the DOM
  • beforeUnmount() called before the element is removed from the DOM
class BindComponent
  afterMount: ($el) ->
    # called after $el has been inserted into the DOM
    # $el is the rendered DOM node

  beforeUnmount: ->
    # called before the element is removed from the DOM

  render: ->
    z 'div',
      z 'span', 'Hello World'
      z 'span', 'Goodbye'

z.state()

  • z.state() creates an Rx.BehaviorSubject with a set() method for partial updates
    • To get current value, call state.getValue()
  • Properties may be a Rx.Observable , whose updates propagate to state
  • Changes that occur in a components state cause a re-render
  • Observables on state are lazy.
    • i.e. They are subscribed-to when creating the virtual-dom tree
    • This is important because it allows for efficient lazy resource binding.
    • See State Management for more info.
###
@param {Object} initialValue
@returns {Rx.BehaviorSubject} (with set() method)
###

Rx = require 'rx-lite'
promise = new Promise()

state = z.state {
  a: 'abc'
  b: 123
  c: [1, 2, 3]
  d: Rx.Observable.fromPromise promise
}

state.getValue() is {
  a: 'abc'
  b: 123
  c: [1, 2, 3]
  d: null
}

promise.resolve(123)

# promise resolved
state.getValue().d is 123

# partial update
state.set
  b: 321

state.getValue() is {
  a: 'abc'
  b: 123
  c: [1, 2, 3]
  d: 123
}

In components

class Stateful
  constructor: ->
    @meSubject = new Rx.BehaviorSubject 'me'
    @state = z.state
      me: @meSubject

  render: =>
    {me} = @state.getValue()

    z 'button',
      onclick: =>
        @meSubject.onNext 'you'
      me

z.render()

Render a virtual-dom tree to a DOM node

###
@param {HtmlElement} $$root
@param {ZoriumComponent} $app
###

$$domNode = document.createElement 'div'
$component = z 'div', 'test'

z.render $$domNode, $component

z.renderToString()

Render a virtual-dom tree to a string
Completes after all states have settled to a value, or the request times out.
The default timeout is 250ms.
Errors may contain the last successful rendering (in case of timeout or error) on error.html

Note: Server-Side rendering is meant to stay separate from the API layer and only pre-render the DOM.
i.e. keep the client-server model you would use for a single-page application

###
@param {ZoriumComponent} $app
@param {Object} config
@param {Number} config.timeout - ms

@returns {Promise}
###

z.renderToString z 'div', 'hi'
.then (html) ->
  # <div>hi</div>

class Timeout
  constructor: ->
    @state = z.state
      never: Rx.Observable.empty()
  render: ({abc}) ->
    z 'div', 'never ' + abc

$instance = z new Timeout(), {abc: 'abc'}
z.renderToString $instance, {timeout: 100}
.catch (err) ->
  err.html # <div>never abc</div>

z.bind()

Bind a virtual-dom tree to a DOM node. This watches state changes and re-draws on update.

###
@param {HtmlElement} $$root
@param {ZoriumComponent} $app
###

$$domNode = document.createElement 'div'
$component = z 'div', 'test'

z.bind $$domNode, $component

z.untilStable()

Wait for all components in tree to have values for asyncronous data.

###
@param {ZoriumComponent} $component
@returns {Promise}
###

class Component
  constructor: ->
    @state = z.state
      model: Rx.Observable.defer ->
        window.fetch '/demo'
        .then (res) -> res.json()
  render: =>
    {model} = @state.getValue()
    z '.z-component',
      if model?
        "model: #{JSON.stringify(model, null, 2)}"

z.untilStable new Component()
.then -> 'stable'
.catch -> 'error'

z.ev()

  • helper method for accessing event DOM nodes
###
@params {Function} callback
@returns {Function}
###

z 'div',
  onclick: z.ev (e, $$el) ->
    # `e` is the original event
    # $$el is the event source element which triggered the event

z.classKebab()

  • helper method for defining css state using kebab-case
###
@params {Object} - truthy keys converted to kebab-case
@returns {String}
###

z 'div',
  className: z.classKebab {
    isActive: true
    isGreen: false
    isRed: true
  }
# <div class='is-active is-red'></div>

z.isSimpleClick()

  • helper method for checking if left click is not using modifier keys
###
@params {Event} e - click event
@returns {Boolean}
###

z 'a',
  href: 'http://google.com'
  onclick: (e) ->
    if z.isSimpleClick e
      e.preventDefault()
      console.log 'do something'

Best Practices

Zorium Seed

Zorium Seed is a starter-project for Zorium
It follows current best practices (both industry and Zorium),
and is the recommended starting point for any new Zorium project.

Special takeaways from the project:

Components

# /component/my_component/index.coffee
z = require 'zorium'

if window?
  require './index.styl'

module.exports = class MyComponent
  render: ->
    z '.z-my-component', 'hi'

Pages

# /pages/my_page/index.coffee
z = require 'zorium'

module.exports = class MyPage
  renderHead: ->
    z 'head',
      z 'title', 'title'
  render: ->
    z '.p-my-page', 'hi'

Routing

Use a LocationRouter instance, passed down through components for routing.

class HelloWorld
  constructor: ([email protected]}) -> null
  goToRed: =>
    @router.go '/red'

  render: =>
    z '.z-hello-world',
      z 'button',
        onclick: @goToRed

new HelloWorld({router: new LocationRouter()})

CoffeeScript

  • Follow the Clay CoffeeScript Style Guide
    • Use the coffeelint.json within the repo as well
    • Alternatively, install the Clay Atom plugin
  • Always deconstruct @state and params before using (e.g. {val} = @state.getValue())

Webpack Configuration

The following is the recommended base webpack configuration

  module:
    exprContextRegExp: /$^/ # allow for mixing node-require without bloat
    exprContextCritical: false
  loaders: [
    {test: /\.coffee$/, loader: 'coffee'}
    {test: /\.json$/, loader: 'json'}
    {test: /\.styl$/, loaders: [
      'style-loader'
      'css-loader',
      'postcss-loader',
      'stylus-loader?paths[]=node_modules'
    ]}
  ]
  postcss: -> [autoprefixer({})]
  resolve:
    extensions: ['.coffee', '.js', '.json', '']
npm install --save-dev coffee-loader json-loader style-loader autoprefixer-loader css-loader stylus-loader

In your application you can mix node-require's without bloating the output

Promise = if window?
  window.Promise
else
  # Avoid webpack include
  _Promise = 'bluebird'
  require _Promise

Naming

  • prefix component instances with $, e.g. $head = new Head()
  • prefix DOM nodes with $$, e.g. $$el = document.body
  • prefix component styles with z-*
  • prefix page styles with p-*
  • postfix pages with Page e.g. MePage = require 'pages/me'
    • Components don't need a postfix
  • folders and files use snake_case

Stylus

  • Namespace all components under z-<component>
  • Only use direct child selectors
  • If classing a deep child, its root with z-<component>_<deep>
  • all classes should be kebab-case (except when namespacing)
  • use z.classKebab() on the top-level element for state management
  • state classes should start with is or has (or similar)
  • state definitions should be blocked together

(Note to external component creators, use a different prefix from z-, e.g. zp- for zorium-paper)

class BigDrawer
  render: ->
    z '.z-big-drawer', # namespace
      className: z.classKebab { # state
        isRed: true
      }
      z '.blue',
        z '.pad', 'hello'
      z $button, {
        $content: z '.z-drawer_icon', # deep child
          className: z.classKebab {isRed: true} # state
          'click'
      }
.z-big-drawer // namespace
  > .blue // direct children only
    background: blue

    > .pad
      padding: 20px

  &.is-red // state
    > .blue
      background: red

.z-big-drawer_icon // deep child
  background: blue

  &.is-red // state
    background: red

State Management

If your app instantiates all components at run-time (it may not render them),
it is important that asyncronous network reuests are cold

e.g.

class Async
  constructor: ->
    @state = z.state
      async: Rx.Observable.defer -> # create a 'cold' observable
        Rx.Observable.fromPromise window.fetch('/model.json')

Server-side considerations

It is important to keep the following in mind when taking advantage of server-side rendering:

  • Do not store any local state
    • client-side state can be used if explicitly checking for window? before creating
  • Bots/crawlers will generate the page
    • This may be important for side-effects or metric gathering
  • Be highly security-conscious
    • Understand the potential for CPU-based DOS attacks, e.g. ReDoS

Animation

Animation state should be encoded in the component state

e.g.

class Animate
  constructor: ->
    @state = z.state
      isAnimating: false
  render: =>
    {isAnimating} = @state.getValue()

    z 'button.z-animate',
      className: z.classKebab {isAnimating}
      onclick: =>
        @state.set isAnimating: true
        setTimeout =>
          @state.set isAnimating: false
        , 1000
      'click me'
.z-animate
  width: 40px
  transition: width 1s

  &.is-animating
    width: 100px

Paper - Material Design

Install

Source

npm install -S zorium-paper

Use these webpack loaders

npm install -S style-loader css-loader autoprefixer-loader stylus-loader coffee-loader json-loader
{ test: /\.coffee$/, loader: 'coffee' }
{ test: /\.json$/, loader: 'json' }
{
  test: /\.styl$/
  loader: 'style!css!autoprefixer!stylus?' +
          'paths[]=bower_components&paths[]=node_modules'
}

And make sure you have the Roboto font

<link href='http://fonts.googleapis.com/css?family=Roboto:400,300,500' rel='stylesheet' type='text/css'>

Shadows

Shadow methods are found in base.styl

@require 'zorium-paper/base'

zp-shadow-1()
zp-shadow-2()
zp-shadow-3()
zp-shadow-4()
zp-shadow-5()

Fonts

Font methods are found in base.styl

@require 'zorium-paper/base'

zp-font-display4()
zp-font-display3()
zp-font-display2()
zp-font-display1()
zp-font-headline()
zp-font-title()
zp-font-subhead()
zp-font-body2()
zp-font-body1()
zp-font-caption()
zp-font-button()

Colors

Colors are found in colors.json and can be used in both Stylus and CoffeeScript.
See Google Material Design Colors for all available colors.

Text colors refer to the font-color which should be used on top of that primary color.
e.g. A background of $red500 should have text colored $red500Text

json('zorium-paper/colors.json')

$black // #000000
$black12 // rgba(0, 0, 0, 0.12)
$black26 // rgba(0, 0, 0, 0.26)
$black54 // rgba(0, 0, 0, 0.54)
$black87 // rgba(0, 0, 0, 0.87)

$white // #FFFFFF
$white12 // rgba(255, 255, 255, 0.12)
$white30 // rgba(255, 255, 255, 0.30)
$white70 // rgba(255, 255, 255, 0.70)

$red50 // #FFEBEE
$red100 // #FFCDD2
$red200 // #EF9A9A
$red300 // #E57373
...

$red100Text // rgba(0, 0, 0, 0.87)
$red500Text // #FFFFFF
...

$lightBlue50Text // rgba(0, 0, 0, 0.87)
$lightBlue100Text // rgba(0, 0, 0, 0.87)
...

Button

###
@constructor
@param {ZoriumComponent} $content
@param {Function} onclick
@param {String} type - e.g. `submit` for forms
@param {Boolean} [isDisabled=false]
@param {Boolean} [isRaised=false]
@param {String} color - a material design color name

render()
@param {ZoriumComponent} $children
###

Button = require 'zorium-paper/button'

$button = new Button({
  color: 'red'
  isRaised: true
  isDisabled: true
})

z $button,
  $children: 'click me'

Checkbox

###
@constructor
@param {Rx.Subject} [isChecked=Rx.BehaviorSubject(false)]
@param {String} color - a material design color name
@param {Boolean} [isDisabled=false]
###

Rx = require 'rx-lite'
Checkbox = require 'zorium-paper/checkbox'
paperColors = require 'zorium-paper/colors.json'

isChecked = new Rx.BehaviorSubject(false)
$checkbox = new Checkbox({isChecked, color: 'blue', isDisabled: false})

Input

###
@constructor
@param {Rx.Observable} [value=Rx.Observable.just('')] - value stream
@param {Rx.Observable} [error=Rx.Observable.just(null)]
@param {Rx.Subject} [value=Rx.BehaviorSubject('')] - change stream
@param {Rx.Subject} [error=Rx.BehaviorSubject(null)]
@param {String} color - a material design color name
@param {String} label
@param {String} type
@param {Boolean} isFloating
@param {Boolean} isDisabled
@param {Boolean} autocapitalize
###

Rx = require 'rx-lite'
Input = require 'zorium-paper/input'
paperColors = require 'zorium-paper/colors.json'

value = new Rx.BehaviorSubject('')
error = new Rx.BehaviorSubject(null)
$input = new Input({
  value
  error
  label: 'label text'
  color: 'blue'
  isDisabled: true
})

Radio Button

###
@constructor
@param {Rx.Subject} [isChecked=Rx.BehaviorSubject(false)]

@param {Object} colors
@param {String} [colors.c500=$black]
@param {Boolean} [isDisabled=false]
@param {Boolean} [isDark=false]
###

Rx = require 'rx-lite'
RadioButton = require 'zorium-paper/radio_button'
paperColors = require 'zorium-paper/colors.json'

isChecked = new Rx.BehaviorSubject(false)
$radio = new RadioButton({isChecked})

z $radio,
  colors:
    c500: paperColors.$blue500
  isDisabled: true
  isDark: true