artsy/stitch

Name: stitch

Owner: Artsy

Description: Helps your Component and Template dependencies peacefully coexist

Created: 2017-08-23 23:58:52.0

Updated: 2018-05-04 23:39:55.0

Pushed: 2018-05-04 23:39:54.0

Homepage:

Size: 244

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

@artsy/stitch

Build Status

Helps your Component and Template dependencies peacefully coexist

Overview

At Artsy we have a number of Node.js applications that have been in production for a while. Force, for example, was built when CoffeeScript and Jade made a lot of sense; CoffeeScript fixed many of the problems with JavaScript pre-ES6, and Jade made working with HTML much more elegant. As time progressed and new technologies became a thing these solutions starting feeling more burdensome to continue building features with and many of our developers longed to start using next-generation tools like React.js. But how? Short of a rebuild, which resource constraints often prohibit, merging old and new tech can be difficult, particularly on the view layer.

This library is an attempt to help those making incremental transitions away from templating solutions like Jade, EJS and numerous others, towards React (or other React-like libs). What it does is provide a flexible set of conventions for dealing with layout code typically seen in Express, but will work just about anywhere that raw HTML is sent down the wire to the client.

If you're using a templating library supported by consolidate.js, this library could be useful to you.

Installation
 install @artsy/stitch
Example Scenarios / Who This Library is Aimed At

Out of the box, Stitch aims for flexibility.

Usage

Table of Contents

(If you want to jump right in, see the full example project.)

Basic Example

In its most basic form, this library is a single function that accepts a path to your layout, some data, and returns a stitched-together string of rendered, raw html that can be sent to the client:

>
title}}
v>
s
t html = await renderLayout({
yout: 'templates/layout.handlebars',
ta: {
title: 'Hello!'



ole.log(html)

> Outputs:

>
llo!
v>

By adding “blocks” you can begin assembling more complex layouts. Blocks represent either a path to a template or a function / React class that returns a string:

emplates/layout.handlebars

l>
ead>
<title>
  {{title}}
</title>
head>
ody
{{{body}}}
body>
ml>
s
ndex.js

t html = await renderLayout({
yout: 'templates/layout.handlebars',
ta: {
title: 'Hello World!',

ocks: {
body: (props) => {
  return (
    <h1>
      {props.title}
    </h1>
  )
}



ole.log(html)

> Outputs:

l>
ead>
<title>Hello World!</title>
head>
ody>
<h1>
  Hello World!
</h1>
body>
ml>

You can add as many blocks as you need, which are accessible by key. Each block is evaluated and compiled, and then injected into the layout. Keep in mind that any field that accepts a path to a template can also accept a component (as either a Class or a stateless functional component), and template formats can be mixed and matched. This allows for a great amount of flexibility within seemingly incompatible view systems.

Express.js and Pug

NOTE! Each of the following use async / await, which is available by default in Node >= 7.6.0. If your environment doesn't yet support async / await see the Troubleshooting section below for an example that uses ES6 Promises.

ndex.js

rt express from 'express'
rt { renderLayout } from '@artsy/stitch'

rt Body from './components/Body'
rt Footer from './components/Footer'

t app = express()

get('/', async (req, res, next) => {
y {
const html = await renderLayout({
  layout: 'templates/mainLayout.pug',
  data: {
    title: 'Hello World!',
    description:
      'An example showing how to take a view structure based in .pug and interpolate with React'
  },
  blocks: {
    head: 'templates/head.pug',
    body: Body,
    footer: Footer
  }
})

res.send(html)
catch (error) {
next(error)



listen(3000, () => {
nsole.log('Listening on port 3000.')

ug
templates/mainLayout.pug


ad
!= head
dy
!= body

footer
  != footer
ug
templates/head.pug

e= title
( property='og:title', content= title )
( name='description', content= description )
s
omponents/Body.js

rt default function Body({ title, description }) {
turn (
<div>
  <h3>{title}</h3>
  <p>{description}</p>
</div>


s
omponents/Footer.js

rt default function Footer() {
turn <h4>Hello footer!</h4>

Layouts and other complex UI configurations

Where this system really begins to shine is when there is a base layout that is extended by sub-layouts. For example, say you have an Express app that mounts a number of sub-apps, and each sub-app has its own layout that “extends” the main layout using blocks (or includes, etc):

templates/mainLayout.pug


ad
include partials/head
block head

dy
include partials/body
block body
ug
apps/home/templates/homeLayout.pug

nds ../../../templates/mainLayout.pug

k head
 head

k body
 locals.isAuthenticated
div
  | Logged-in

 body
s
pps/home/routes.js



t html = await renderLayout({
sePath: __dirname,
yout: 'templates/homeLayout.pug',
ta: {
title: 'Hello how are you?',
description: 'Views extending views extending views...'


 Can also define locals, which are passed down to components under
 the `props.locals` key
cals: {
isAuthenticated: req.locals.userIsAuthenticated

ocks: {
head: 'templates/head.pug',
body: Body



send(html)
Isomorpic (or “Universal”) rendering

This is covered in more depth in the isomorphic-react-pug-webpack example but in short, since the data object is injected into the template before rendering takes place, making it available on the client is as easy as JSON.stringifying it:

 templates/layout.pug


dy
script.
  var __BOOTSTRAP__ = {JSON.stringify(data)}

!= app
s
t html = await renderLayout({
yout: 'templates/layout.pug',
ocks: {
app: App

ta: {
name: 'Z'



send(html)
s
rt default class App extends Component {
mponentDidMount () {
console.log(`Hello ${this.props.name}, <App /> is mounted on the client.`)


nder () {
return (
  <div>
    Hello {this.props.name}
  </div>
)



s
lient.js

t.render(<App {...window.__BOOTSTRAP__} />)
Precompiling Templates

What to do if you have a bunch of old-school Backbone views that you don't want to throw out while moving over to React, but would still like to inject their markup into your React app for mounting later on the client? This library makes that easy:

pps/login/index.js



t html = await renderLayout({
yout: 'templates/loginLayout.pug',
ocks: {
app: App

mplates: {
login: 'templates/login.pug'


s
omponents/Body.js

rt React from 'react'
rt Login from './Login'

rt default function App(props) {
nst { templates: { login } } = props

turn (
<div>
  <Login template={login} />
</div>


s
omponents/Login.js

rt React, { Component } from 'react'
rt LoginView from '../views/LoginView'

rt default class Login extends Component {
mponentDidMount () {
this.loginView = new LoginView()
this.loginView.render()


mponentWillUnmount () {
this.loginView.remove()


nder () {
return (
  <div>
    <div dangerouslySetInnerHtml={{
      __html: this.props.template
    }}>
  </div>
)


tml
emplates/login.handlebars

 id='login'>
utton>
Login
button>
v>
s
iews/LoginView.js

LoginView = Backbone.View.extend({
: '#login',

ents: {
'click button': 'handleLoginClick'


ndleLoginClick: function () {
...


nder: function() {
...



le.exports = LoginView

The template is precompiled, and the html is available from within your React components to mount and unmount as necessary under the props.templates key.

Custom Renderers

If you would prefer to use a rendering engine other than React, no problem – just pass in a custom render function that returns a string:

rt renderToString from 'preact-render-to-string'

t html = await renderLayout({
yout: 'templates/loginLayout.pug',
nfig: {
componentRenderer: renderToString

ocks: {
body: App


Additionally, if you would like to override any default template engines (e.g., what is returned by require('handlebars')) you can do so by updating config.engines:

t html = await renderLayout({
yout: 'templates/loginLayout.pug',
nfig: {
engines: {
  handlebars: (filePath, locals) => {
    return customHandlebarsRenderer(filePath, locals)
  },
  pug: (filePath, locals) => {
    return customPugRenderer(filePath, locals)
  }
}

ocks: {
body: App


Styled Components Support

If your React app uses styled-components, ensure you've installed babel-plugin-styled-components and enable server-side rendering via config:

rt styled from 'styled-components'

t html = await renderLayout({
yout: 'templates/layout.pug',
nfig: {
styledComponents: true

ocks: {
body: (props) => {
  const Layout = styled.div`
    background: purple;
    border: 1px solid black;
    color: white;
    width: 100%;
  `

  return (
    <Layout>
      Hello Styled Components!
    </Layout>
  )
}


Lastly, make sure to mount your styles in your layout template:


ad
!= css

dy
#react-root
  != body
Full API
t html = await renderLayout({

*
 Sets a base path from which to fetch templates.

 @type {String}
/
sePath: process.cwd(),

*
 Path to layout template file / component.

 @type {String|Component}
/
yout: <REQUIRED>

*
 Block sections to pass to template / components. A "block" typically
 represents a discreet section (header, body, footer, etc) but can be
 anything.
/
ocks: {
/**
 * @type {String|Component}
 */


*
 Locals represent Express.js locals as they flow through the `req` / `res`
 cycle. These values could be passed to the following section, `data`, but
 for the sake of clarity and isolation they're typically passed along here.

 Accessible from within components via `props.locals` and from within a
 template via `locals`
/
cals: {
/**
 * @type {*}
 */


*
 Data that is passed to templates and components. Embedded directly
 within template code, but also accessible via `data` (useful for
 JSON.stringify'ing and passing down the wire for rehydration). In a
 component, it represents `props` and is accessible as such.
/
ta: {
/**
 * @type {*}
 */


*
 Templates / Components that are precompiled and passed along as rendered
 html strings. From within your component, template html is accessible via
 `props.templates`
/
mplates: {
/**
 * @type {String|Component}
 */


nfig: {

/**
 * Configuration for layout renderer. Right now components are rendered via
 * ReactDOM, but any kind of engine can be passed in and accommodated
 * assuming it returns a string of rendered markup.
 *
 * @type {Function}
 */
componentRenderer: ReactDOM.renderToString,

/**
 * If you would like to override any default template engines pass in a
 * key matching the extension and a function, e.g.,
 *
 * engines: {
 *   pug: (filePath, locals) => string
 * }
 */
engines: {
  /**
   * @type {Function}
   */
},

/**
 * If your project uses <StyledComponents /> and you would like to extract
 * styles from your component during server-side renders, set this to true.
 *
 * See https://www.styled-components.com/docs/advanced#server-side-rendering
 * for more information.
 *
 * @type {Boolean}
 */
styledComponents: false


Troubleshooting

Help! My view doesn't render and there's an Unexpected token ( in my console.

All of the examples assume that you've enabled async / await, which comes by default in versions of Node >= 7.6.0. If you're getting this error, it's likely you're using an older version that doesn't yet support this feature. But no fear - async / await is just a wrapper around Promises:

erLayout({ layout: 'layout.ejs' })
hen((html) => {
res.send(html)

atch((error) => {
next(error)

Async / await is supported in my version of Node, but I'm not seeing anything in the browser and no errors in the console!

Did you remember to use the await keyword before renderLayout? It's easy to forget :)


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.