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
- First Class RxJS Observables
- Built for Isomorphism (server-side rendering)
- Fast! - virtual-dom
- Standardized Best Practices
- Material Design Components
- Production-ready Seed Project
- It's just CoffeeScript, no magic
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 DOMbeforeUnmount()
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()
- To get current value, call
- 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
- see this post for more information
###
@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:
- Webpack (packaging tool)
- RxJS (functional reactive programming)
- Gulp (build tool)
- Karma (unit testing)
- Stylus (css pre-processor)
- Istanbul (code coverage)
- WebdriverIO (functional testing)
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: ({@router}) -> 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
- Use the
- Always deconstruct
@state
andparams
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
orhas
(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
- client-side state can be used if explicitly checking for
- 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
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