xander

Single page app framework

Usage no npm install needed!

<script type="module">
  import xander from 'https://cdn.skypack.dev/xander';
</script>

README

xander

CircleCI

Overview

Frontend Framework for React and Formula.

Usage

Installation

npm install --save react formula xander

Examples

Quick start

A minimal xander app with home and 404 page.

index.js

// Import the boot function to intialize xander.
import React from "react";
import { render } from "xander";

// Import style onto the page.
require("./app.css");

// Define routes for the app.
let routes = [
  {
    path: "/",
    component: props => <div>Hello, World.</div>
  },
  {
    path: "*",
    component: props => <div>404 NOT FOUND</div>
  }
];

// Render your app to the DOM.
render(
  {
    routes
  },
  document.getElementById("root")
);

minimal example

With React's render

Render xander with React's render function.

// Import the boot function to intialize xander.
import { app } from "xander";
import React from "react";
import ReactDOM from "react-dom";
import routes from "./routes";

let App = app({ routes });
// Define routes for your app.
// Boot the app into a root DOM element. Map your URLs to component to render.
ReactDOM.render(<App />, document.getElementById("root"));

Components

Link Component

A link component to hyperlink your app without annoying page refreshes.

import {Link} from 'xander'
<Link to="/buckets">Go to my buckets</Link>

Eval Component

The Eval component calculates the result of a formula expression.

import {Eval} from 'xander'
<Eval exp="SUM(A, B)" values={ A: 2, B: 2 } />

Rule Component

The Rule component renders HTML describing a formula expression.

import {Rule} from 'xander'
<Rule exp="SUM(A, B)" />

Loadable / loader HOCs

The Loadable HOC works with webpack to split your app into chunks that load dynamically.

import { loadable } from "xander";
let routes = [
  {
    path: "/",
    component: loadable({
      loader: () => import("./home"),
      loading: (props) => <div>Loading...</div>
      delay: 500 // 0.500 seconds
    })
  }
];

Container Component

The Container component renders the router's current component.

import { Link } from "xander";
render(<Container />);

Connect Component

The Connect HOC component syncs the store with React state.

import { connect, Container } from "xander";
connect(Container);

Stores

Router Store

A minimal router, backed by the history API.

import { router } from "xander";
router.open("/buckets/1");

Use redirect to modify URL without adding an entry to the history state.

router.redirect("/buckets");

Load routes and related configuration without app or render.

import { router } from "xander";
router.loadRoutes([
  {
    path: "/",
    component: require("./pages/home")
  }
]);

Window Store

The window store keeps track of window size and scroll location; syncs with DOM.

import { loadWindowStore } from "xander";
loadWindowStore();

Custom Stores

Create custom stores to store your app's data.

import { createStore } from "xander";
createStore(key, reducerOrSpec, actionsAndQueries);

// example store, access via the key `todos` in react props.
let todosStore = createStore('todos', {
  getInitialState: () => []
  addTodo: (state, todo) => state.concat(todo),
  removeTodo: (state, id) => state.filter(d => d.id !== id)
})

// usagexander
todosStore.addTodo({ id: 1, desc: "Make new framework" })
todosStore.addTodo({ id: 2, desc: "Write killer app" })
todosStore.addTodo({ id: 3, desc: "Analyze competition" })
todosStore.removeTodo(3)
todosStore.subscribe((state, action) => console.log('todos changes', state, action))
todosStore.dispatch('addTodo', { id: 4, desc: "Write product examples" })

createStore( key, reducerOrSpec, actionsOrSelectors )

A store responds to actions by returning the next state.

const inc = 'inc'
import {createStore} from 'xander';

// a simple counting store
var store = createStore( "count", (state=0, action) => {
  switch (action.type)
  case inc:
    return state + 1;
  case incN:
    return state + action.data;
  default:
    return state;
}, {
  inc: (state) => dispatch('inc'),
  incN: (state, count) => dispatch('incN', count),
})

// the store includes a reference to dispatch
store.dispatch('inc')

// optionally, define action creators into the store.
store.inc()

Optionally, you may define a store with a specification.

const inc = "inc";
import { createStore } from "xander";

// a simple counting store
var countStore = createStore("count", {
  // life-cycle method for initialization.
  getInitialState: () => 0,
  // handles { type: 'inc' }
  inc: state => state + 1,
  // handles { type: 'incN' }
  incN: (state, n) => state + n
});

// object spec makes action creators automatically...
countStore.inc();
countStore.incN(10);

Store Properties

Here is a list of store properties that are part of the public API.

name comment
name The name of the store
dispatch Access to dispatch function
dispatchToken A number used to identity the store
subscribe A function to tegister a listener
getState A function to access state
setState Replace the store's state
replaceReducer Replace the store's reducer

dispatch( action )

The entry point to effecting state changes in the app is when an action is dispatch.

Dispatch accepts action as object, promise, or type/data; returns promise.

// Import the dispatch function.
var { dispatch } = require( 'xander' )

// Dispatch action as object
dispatch( { type: 'openPath', '/user/new' } )
.then( action => console.log('Going', action.data) )

// Dispatch action as promise
dispatch( Promise.resolve({ type: 'get', mode: 'off the juice' }) )

// Dispatch action with type:string and data:object.
dispatch( 'loadSettings', { a: 1, b: 2 } )

WaitFor Example

import { createStore } from "xander";

// creates a key="A" in the root store, connected to a reducer function.
let storeA = createStore(
  "a1",
  (state = 0, action) => (action.type === "setA" ? action.data : state)
);

let storeB = createStore(
  "b1",
  (state = 0, action) => (action.type === "setB" ? action.data : state)
);

// Store with dependencies on state in storeA and storeB.
let storeC = createStore("c1", (state = 0, action, waitFor) => {
  // Ensure storeA and storeB reducers run prior to continuing.
  waitFor([storeA.dispatchToken, storeB.dispatchToken]);

  // Side effect! Reads state from other stores after they are updated.
  return storeA.getState() + storeB.getState();
});

getState( )

Returns an object with the store's state by key.

import { getState } from "xander";
getState();

getStores( )

Returns an object with the stores by key.

import { getStores } from "xander";
getStores();

replaceState( state )

Rehydrate the root state.

import { replaceState } from "xander";

replaceState({
  MyCountStore: 1
});

subscribe( listener )

Listen to changes to all stores. This will trigger once each time createStore or dispatch is invoked.

var unsubscribe = subscribe( (state, action) => {
  // got change
})

// stop listening
unsubscribe()

Please note that action will be undefined when createStore is invoked.