node package manager

naka

naka Build Status js-standard-style

Work in progress

A minimalist front end framework.

How is naka different?

Naka seeks to blend the best parts of other popular front end frameworks while remaining opinionated and minimal. It heavily relies on morphdom, bel, and yo-yo (naka/morphdom/bel/yo-yo => NMBY stack). Naka uses a minimal redux flow with global state (via send-action). A lot of time has also been spent on integrating testing and creating a powerful CLI.

This framework is heavily based on choo (read: steals from choo). It primarily diverges in the redux data flow, only supporting the traditional flow (no subscriptions or effects). Naka also uses a container/component pattern where components should be purely presentational while containers handle actions/state/etc.

Server side rendering has been supported from the beginning, allowing users without a JS environment to also receive a first class experience. Routing is also supported so urls and browser history ensure a consistent and predictable experience.

What does naka offer?

  • an ergonomic cli
  • an emphasis on testing
  • a miniscule footprint
  • routing api
  • components
  • event handling
  • 100% js (no jsx or hbs)
  • global state
  • live reloading in dev
  • production build step via the cli
  • universal rendering
  • pluggable api

Where'd the name come from?

When writing this readme, I randomly opened Iterating Grace to look for name inspiration. I happened on the page listing Koons Crooks' list of prior companies. One was named naka. It was short, sweet, and was a nice word in other languages. Naka also happens to be pleasant to type.

Installation

npm install -g naka

Usage

naka -h
 
  A minimalist front end framework
 
  Usage
    $ naka <command> <options...>
 
  Commands
    $ naka new <name> - Create a new app
    $ naka generate <blueprint> - Item to generate
    $ naka serve - Serve the app
    $ naka test - Run the test suite
    $ naka build <options...> - Build the app
 
  Examples
    $ naka -h
    $ naka new awesome-app
    $ naka t
    $ naka generate model user
    $ naka build -prod

Components

A naka component is nothing more than a function that returns a template string.

const h = require('naka/html')
const toPercent = require('to-percent')
 
module.exports = (params, state, dispatch) => {
  const foo = state.foo
 
  return h`
    <div>
      <h1 class="f4 mt2">
        ${foo.title} <br>
        <small>
          ${toPercent(foo.bar)}
        </small>
      </h1>
      <button onclick=${e => dispatch('foo.actions.myAction')}>
        Hi
      </button>
    </div>
  `
}

A component always comes with a test

import test from 'ava'
import comp from './'
 
const state = { foo: { title: 'baz', bar: 0.12 } }
test('parses the percentage', t => {
  const compHtml = comp(state)
  t.true(compHtml.includes('12%'))
})
 
 
test('adds an h1', t => {
  const compHtml = comp(state)
  t.true(compHtml.includes('<h1>baz'))
})

Reducers

Reducers receive data and use that information to modify the global state. A reducer can also be used to set the initial state. When state changes, the new DOM is drawn and updated with yo-yo.

module.exports = {
  name: 'counter',
 
  state: {
    count: 0
  },
 
  setCount: (action, state) => {
    state.counter.count = action.count
    return state
  }
}

Of course, the reducer also comes with a test.

import test from 'ava'
import isPresent from 'is-present'
 
import hello from './'
 
const state = {
  hello: {
    count: 42
  }
}
 
test('initializes with state', t => {
  t.true(isPresent(hello.state.count))
})
 
test('setCount reduces state', t => {
  const newState = hello.setCount(
    { count: 100 },
    state
  )
 
  t.is(newState.hello.count, 100)
})

Actions

Actions receive an event, the current state, and the dispatch method. They do work, often times an API call, and then dispatch a reducer with any relevant information. Actions don't modify state directly.

module.export = {
  name: 'counter',
 
  decrement: (action, state, dispatch) => (
    dispatch('counter.reducers.setCount', {
      count: state.counter.count - 1
    })
  ),
 
  increment: (action, state, dispatch) => (
    dispatch('counter.reducers.setCount', {
      count: state.counter.count + 1
    })
  )
}

The test 👯 😎

import test from 'ava'
import isPresent from 'is-present'
 
import hello from './'
 
const state = {
  hello: {
    count: 42
  }
}
 
test('decrement calls the correct reducer', t => {
  hello.decrement(undefined, state, (reducer, _action) => {
    t.is(reducer, 'hello.reducers.setCount')
  })
})
 
test('decrement returns the correct count', t => {
  hello.decrement(undefined, state, (_reducer, action) => {
    t.is(action.count, state.hello.count - 1)
  })
})
 
test('increment calls the correct reducer', t => {
  hello.increment(undefined, state, (reducer, _action) => {
    t.is(reducer, 'hello.reducers.setCount')
  })
})
 
test('increment returns the correct count', t => {
  hello.increment(undefined, state, (_reducer, action) => {
    t.is(action.count, state.hello.count + 1)
  })
})

Constructing the app

Every generated app has an index.js file, this is the entry point of the app. Before initializing the app, you must register your models and routes. Then, the app is started by calling app.init().

const naka = require('naka')
const app = naka()
 
app.register(require('./reducers/hello'), 'reducer')
app.register(require('./actions/hello'), 'action')
 
app.router(route => [
  route('/', require('./containers/app')),
  route('/users', require('./containers/users'))
])
 
app.init()

Architecture

Naka attempts to use convention to remove boilerplate. All elements have their own directory with an index.js and element-name-test.js. This ensures that the test lives next to the module that it tests.

app-name/
  |
  |
  containers/
      |
      |
      my-container/
          |
          |
          `index.js
          `my-container-test.js
  |
  |
  components/
      |
      |
      my-component/
          |
          |
          `index.js
          `my-component-test.js
  |
  |
  actions/
      |
      |
      my-action/
          |
          |
          `index.js
          `my-action-test.js
  |
  |
  reducers/
      |
      |
      my-reducer/
          |
          |
          `index.js
          `my-reducer-test.js
  |
  |
  `index.html
  `package.json
  `app.js
  `routes.js

License

MIT

Related

There are numerous libraries that naka uses and draws inspiration from. Thank you to all the contributors that have been involved.

Dependencies

Inspiration

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Crafted with <3 by John Otander (@4lpine).


This package was initially generated with yeoman and the p generator.