Tram-One

IntroductionFeaturesInstallAPISlackGithubNPMStackBlitz
Tram-One is a light View Framework that comes with all the dependencies you need to start developing on the web.

Tram-One is an orchestration of common features, and relies only on plain javascript, so you don't have to bother learning / parsing / transpiling special templating languages.

This site is a one-stop-shop with everything you need to know about Tram-One. If you have any questions from this page or about Tram-One, or just want to say hi, join our Slack!
app.js
const Tram = require('tram-one')
const app = new Tram()

const html = Tram.html()
const page = () => {
  return html`
    <div>
      <h1> Fun Times on Tram-One! </h1>
      <h2> With Custom Elements </h2>
      <h2> Routing </h2>
      <h2> and Redux State Management </h2>
      <h1> Batteries Included! </h1>
    </div>
  `
}

app.addRoute('/', page)
app.start('.main')
With tools like Custom Element definitons, Redux-like State-Management, and Routing, you can start building web applications right away!
Tram-One supports creating custom tags, which can be used in your html just like any other html tag.
Elements are simple functions that take in attributes and children - the same interface as real HTML elements.
my-header.js
const Tram = require('tram-one')
const html = Tram.html()

module.exports = (attrs, children) => html`
  <div style=${attrs.style}>
    <h1> Hello ${children} </h1>
  </div>
`
We can pull in our new element by passing a registry into the Tram.html function. We define all the custom elements in the registry, and then can use them in our html.
page.js
const Tram = require('tram-one')
const html = Tram.html({
  'my-header': require('./my-header')
})

const page = () => html`
  <my-header style="color: blue;">
    Tram-One
  </my-header>
`
The registry allows you to define what the syntax for your tags look like. You could choose to use React style tags with capitalized letters, single-word tags to look more like standard html, or hyphenated tags similar to web components.
Page.js
const Tram = require('tram-one')
const html = Tram.html({
  'Header': require('./Header'),
  'Container': require('./Container')
})

const Page = () => html`
  <Container>
    <Header>Tram-One</Header>
  </Container>
`
Tram-One follows a Flux-like architecture model with Hover-Engine. If you're familiar with redux, it's very similar.
First, we build a set of actions. Each action is a function which takes the previous state, and returns an updated state.
vote-actions.js
module.exports = ({
  init: () => 0,
  upvote: (votes) => votes + 1,
  downvote: (votes) => votes - 1
})
To use these actions, we'll need to add them to our app.
app.js
const Tram = require('tram-one')
const app = new Tram()

app.addRoute('/', require('./page'))
app.addActions({ votes: require('./vote-actions') })
Now we can reference the store values, and call these actions on our page. Pages have access to the different stores, and an actions object with all the methods we defined before.
page.js
const Tram = require('tram-one')
const html = Tram.html()

module.exports = (store, actions) => html`
  <div>
    <h1> My New Blog Post </h1>
    <span> Total Likes: ${store.votes} </span>
    <button onclick=${actions.upvote}> Like </button>
    <button onclick=${actions.downvote}> Dislike </button>
  </div>
`
Tram-One supports routing, having different components render based on which route a user is on.

By default, a route that doesn't match is sent to whatever component lives on /404 . You can handle the route from there.
app.js
const cats = () => html`
  <h1> Cats Rule! </h1>
`

const dogs = () => html`
  <h1> Dogs Rule! </h1>
`

const unknown = () => html`
  <h1> Animals are Neat... </h1>
`

app.addRoute('/cats', cats)
app.addRoute('/dogs', dogs)
app.addRoute('/404', unknown)
The route paths can contain path and query parameters, which are passed into the page in the params object.
app.js
const animals = (store, actions, params) => html`
  <div>
    <h1> My favorite animal is the ${params.animal}! </h1>
  </div>
`

app.addRoute('/animals/:animal', animals)
Routes can be nested, so you can have the same surrounding elements for all of your pages (like a consistent navigation bar, or header).
app.js
const animals = (store, actions, params, subroute) => html`
  <div>
    <h1> Animals Rule! </h1>
    ${subroute}
  </div>
`

const turtles = () => html`
  <h2> My Favorite are Turtles </h2>
`

const alpaca = () => html`
  <h2> My Favorite are Alpaca </h2>
`

const route = Tram.route()
app.addRoute('/animals', animals, [
  route('/turtles', turtles),
  route('/alpaca', alpaca)
])
There are multiple ways to get started with Tram-One!
The fastest and best way to install Tram-One is by using Tram-One Express, a dedicated generator for building single-page apps using Tram-One.

It comes with example code, scripts, and tests to help you get started!

You can check out the documentation here:
https://github.com/Tram-One/tram-one-express
install.sh
npx tram-one-express my-tram-app
You can install Tram-One by itself via npm. To see other npm details, checkout out the page on npm.
https://www.npmjs.com/package/tram-one
install.sh
npm install --save tram-one
You can include Tram-One in an html page by adding a script tag pointing to the umd module on the npm content delivery network unpkg.
https://unpkg.com/tram-one/dist/tram-one.umd.js

This is nice because it does not require a build system, and allows you to quickly see Tram-One running in your browser. However to build larger applications, it's recommended that you use one of the above solutions.
index.html
<html>
  <head>
    <script src="https://unpkg.com/tram-one/dist/tram-one.umd.js">
    </script>
  </head>
  <body>
    <div class="main"></div>
    <script>
      const Tram = window['tram-one']
      const html = Tram.html()
      const app = new Tram()

      const page = () => {
        return html`<div><h2>UMD Page Example</h2></div>`
      }

      app.addRoute('/page', page)
      app.start('.main', '/page')
    </script>
  </body>
</html>
Tram-One has a simple interface to help you build your web app.
Many of the building blocks for Tram-One apps are basic functions. Tram-One provides helper functions to make these easier to build and connect.
Because the following functions are static, you call the function off of the Tram-One dependency. You do not need to have an instance of a Tram-One app to use these functions.
element.js
const Tram = require('tram-one')
// const app = new Tram() <-- not needed
const html = Tram.html()
Tram.html returns a function that can be used to transform template literals into Node DOM trees. It can take in an optional registry, which is a mapping of tag names to functions that render your custom tags.
page.js
const Tram = require('tram-one')
const html = Tram.html({
  'my-header': require('./my-header'),
  'my-footer': require('./my-footer')
})

const title = 'Tram One Rules!'
const style = 'color: blue;'

const page = () => {
  return html`
    <div style=${style}>
      <my-header> ${title} </my-header>
      <my-footer/>
    </div>
  `
}
Whether you are on a Node Server, or running on the web, Tram.html returns a DOM tree, so you can interact with it just like any other DOM.
dom.js
const html = Tram.html()
const page = html`
  <div>
    <h1 id="header">Hello!</h1>
    <a href="/home">Home Page</a>
  </div>
`
page.querySelector('#header') // --> HTMLHeadingElement
page.textContent // --> "Hello! Home Page"
Tram.svg is the same as Tram.html, but will create elements in the appropriate SVG namespace. Use this method if you're building components that are SVG.
logo.js
const svg = Tram.svg()
const logo = () => svg`
  <svg viewBox="0 0 864 864">
    <g>
      <circle fill="#FDF491" cx="100" cy="100" r="20"/>
    </g>
  </svg>
`
Tram.dom is the generic version of Tram.html and Tram.svg . It is the driving function that builds document trees, and can be used whenever you need to use a namespace other than XHTML and SVG.
toolbar.js
const xul = Tram.dom(
  'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'
)
const toolbarButton = () => xul`
  <toolbarbutton label="Menu" type="menu">
    <menupopup>
      <menuitem label="Action 1"/>
      <menuitem label="Action 2"/>
    </menupopup>
  </toolbarbutton>
`
Tram.route is a static method which is a shorthand for creating the subroutes for nested routes in the app.addRoute method. The resulting function takes in path, component, and subroutes, mimicking the interface used in app.addRoute.
router.js
const route = Tram.route()
app.addRoute('/', homePage, [
  route('/images/:page', imagePage, [
    route('/download', downloadImagePage),
    route('/preview', previewImagePage)
  ]),
  route('/about', aboutPage)
])


// navigating to /images/10/download will resolve as
homePage(store, actions, params,
  imagePage(store, actions, params,
    downloadImagePage(store, actions, params)
  )
)
new Tram() returns an instance of a Tram-One app. The constructor takes in an optional options object, for different configurations.

defaultRoute is the path that Tram-One will go to when no others match. This can be useful as an error page, or to force all routes to go to a home page.
By default, this is '/404', so adding an error page to '/404' will cause all invalid routes to navigate to that page.

webStorage is the object that state will persist in.
By default, this is undefined, so app state will not persist between page reloads. You can set it to localStorage, sessionStorage, or your own plain javascript object.

webEngine is an object that Tram-One will write and update store and actions to. This is recommended for when you want to pull store values or actions without passing them through each tag.
By default, this is undefined. A common pattern is to set this to window (or a subfield of window), so that it is always accessible, however you can set it to any javascript object. Whatever it is set to, Tram-One will assign store and actions to this object.
app.js
const Tram = require('tram-one')

const app = new Tram({
  defaultRoute: '/',
  webStorage: localStorage,
  webEngine: window
})

const html = Tram.html()

const home = (state) => {
  return html`<div>This is my shiny app!</div>`
}

app.addRoute('/', home)
The following functions should be called on an instance of a Tram-One app (see the app constructor above).

These functions all return an instance of the app, so that it is easy to chain one after the other.
app.js
const Tram = require('tram-one')
const app = new Tram()

app
  .addActions(...)
  .addRoute(...)
  .addRoute(...)
  .addRoute(...)
  .start(...)
app.addActions adds a set of actions that can be triggered in the instance of a Tram-One app. First, we need to define a set of actions that can mutate a store.
vote-actions.js
module.exports = {
  init: () => 0,
  up: (vote) => vote + 1,
  down: (vote) => vote - 1
}
We can access the current value of the store via the state object on the page, and each action defined will be a method on the actions object.
page.js
module.exports = (state, actions) => {
  const upvote = () => {
    actions.up()
  }

  const downvote = () => {
    actions.down()
  }

  return html`
    <div>
      <h1> Votes: ${state.votes} </h1>
      <button onclick=${upvote}>UPVOTE</button>
      <button onclick=${downvote}>DOWNVOTE</button>
    </div>
  `
}
Finally, we hook it all together with the app.addActions method, which defines what key we can use to get the current value of our action group.
app.js
const app = new Tram()
const html = Tram.html()

app
  .addRoute('/', require('./page'))
  .addActions({votes: require('./vote-actions')})
app.addListener adds a function that triggers on every action call. This can be used to debug the state of the store as actions are called. This should not be used to update the DOM, only trigger side-effects.

It takes in one argument, a function, which will have the new store value, actions, the actionName that was called, and actionArguements (if any).
app.js
// we want to debug the store as votes come in
const debug = (store, actions, actionName) => {
  console.log(actionName, 'was triggered!')
  console.log('NEW STATE:', store)
}

app.addListener(debug)
app.addRoute will associate a path with a top level component.
path should be a matchable string for the application.
page should be a top level component.
subroutes should be a list of route objects.


Pages (or top level components) are functions, which will have the store, actions, params from the url, and a subroute component (if one is resolved).

The params object will have any path parameters and query params.
home.js
module.exports = () => html`
  <div>This is my shiny app!</div>
`
color.js
module.exports = (store, actions, params) => {
  const style = `
    background: ${params.color};
    width: 100px;
    height: 100px;
  `
  return html`<div style=${style} />`
}
notFound.js
module.exports = () => html`
  <div>Oh no! We couldn't find what you were looking for</div>
`
app.js
const app = new Tram()

app.addRoute('/', require('./home'))
app.addRoute('/:color', require('./color'))
app.addRoute('/404', require('./notFound'))
app.start will kick off the app. Once this is called the app is mounted onto the selector.
selector can be a node or a css selector (which is fed into document.querySelector).
pathname can be an initial path, if you don't want to check the browser's current path.
subroutes should be a list of route objects.

This method only works on the client. If you are running Tram on a server, then use .toString().

Note: setting pathname is great for testing , but prevents other routes from being reached.
index.html
<html>
  <head>
    <title>Tram One</title>
  </head>
  <body>
    <div class="main"></div>
    <script src="/app.js"></script>
  </body>
</html>
app.js
const app = new Tram()
const html = Tram.html()

const homePage = () => {
  return html`<div>This is my shiny app!</div>`
}

app.addRoute('/', homePage)
app.start('.main')
WARNING: INTENDED FOR INTERNAL USE ONLY
app.mount app.mount matches a route from pathName, passes in a store and actions object, and either creates a child div, or updates a child div under selector. This was created to clean up the code in the library, but may be useful for testing.
DO NOT CALL THIS DIRECTLY FOR YOUR APP
app.toNode returns a HTMLNode of the app for a given route and store. The function matches a route from pathName, and either takes in a store, or uses the default store (that's been created by adding reducers).

While initially created to clean up the code in the library, this can be useful if you want to manually attach the HTMLNode that Tram-One builds to whatever.
app.js
const homePage = (store) => html`
<div>
  <h1> My New Blog Post </h1>
  <span> Total Likes: ${store.votes} </span>
</div>
`

app.addRoute('/', homePage)
const homeDOM = app.toNode('/', {votes: 10})
console.log(homeDOM) // --> HTMLDivElement
app.toString returns a string of the app for a given route and store. It has the same interface at app.toNode, and basically just calls .outerHTML on the node.

This can be useful if you want to do server-sider rendering or testing.
app.js
const homePage = (store) => html`
<div>
  <h1> My New Blog Post </h1>
  <span> Total Likes: ${store.votes} </span>
</div>
`

app.addRoute('/', homePage)
const homeDOM = app.toString('/', {votes: 10})
console.log(homeDOM) // --> <div><h1> ... Likes: 10</span></div>