Gruu

Why Gruu?

Gruu is a small and powerful JavaScript library for creating dynamic content. Using only JavaScript you can create user interfaces that change dynamically. Gruu is fully dependency free and uses ES6 Proxies to trigger changes in DOM.

Component

Components are simple JavaScript objects. Gruu is highly composable. Using components you can describe simple and complex parts of html. Example components are given below. Beside there is a html code that is going to be genereted as a result of the rendering of the components.

const hello = Gruu.createComponent({
  _type: 'span',
  id: 'hello-world',
  textContent: 'Hello World!'
})

const container = Gruu.createComponent({
  _type: 'div',
  className: 'container',
  children: [hello, {
    _type: 'p',
    textContent: 'Hello Again :)'
  }]
})
<div class="container">
  <span id="hello-world">
     Hello World!
  </span>
  <p>
     Hello Again :)
  </p>
</div>

Component container is a div with a class "container". It contains component hello which is a span with id "hello-world" and content "Hello World!". Component container also contains a component which is a paragraph with text "Hello Again :)". Created component once used cannot be used again. In order to use the same component multiple times, you have to create a factory function that returns a new component and execute it as many times as you want.

As you may have noticed a tag name is passed to componentes as a property _type. There are a few kinds of properties that components can have:

_type

Tag name. The only exception is _type: "text" which renders plain text instead of a html tag (If you provide a plain text instead of a component object it is going to be parsed to { _type: "text", textContent: plainText }). Any property starting with "_" is a property for Gruu internal usage.

_key

Wherever you generate children of a component dynamically it is advisable to use _key. This property assigned to children components helps Gruu to match corresponding components and speeds up calculations. Property _key should be unique within the array of children, not globally.

children

Components that are going to be rendered inside the given component. Children can be an array of components or a single component.

state

Internal state of the component.

*properties starting with $

Properties that are calculated dynamically. See more in section Subscriptions.

*other properties

Properties like innerHTML, textContent, className etc. are going to be assigned to the HTML Element.

DOM vs Phantom Components

There are two kinds of components: DOM Components and Phantom Components. All component that don't have _type property are Phantom.

DOM Components

They have their representaion in the HTML DOM.

const hello = Gruu.createComponent({
  _type: 'div',
  textContent: 'Hello World!'
})
Phantom Components

They exist in Gruu Virtual DOM, but are transparent for the HTML DOM.

const phantom = Gruu.createComponent({
  // no _type property
  children: [{
    _type: 'div',
    textContent: 'Inside phantom!'
  }]
})

Phantom Components are transparent for the HTML DOM meaning that its children HTML Elements are going to be appended to the closest parent (HTML Element) that is a DOM Component. Using DOM and Phantom Component allows to build complex HTML structures consisting of many elements.

Changing properties

You can manually change all properties of components except those starting with "_". The changes will be automatically applied to the HTML Elements. When it is necessary to change a property that starts with "_", Gruu handles it internally and automatically.

const counter = Gruu.createComponent({
  _type: 'div',
  textContent: 10
})

const button = (text, diff) => (
  Gruu.createComponent({
    _type: 'button',
    className: 'button',
    textContent: text,
    onclick () {
      counter.textContent += diff
    }
  })
)

const buttonInc = button('+', 1)
const buttonDec = button('-', -1)

const app = Gruu.createComponent({
  _type: 'div',
  className: 'app',
  children: [
    'Counter',
    {
      _type: 'div',
      children: [buttonDec, counter, buttonInc]
    }
  ]
})

Each time you click a button the textContent is going to be changed by value of diff variable (1 or -1). Function button is a factory that creates both buttonInc and buttonDec.

State

Gruu uses decentralised state model. There are many stores, each responsible for a different part of an application. Component can have their own local private stores or share data with other components using public stores. User inputs or HTTP communication mechanisms can modify stores. Other components that use data from these stores update each time the data changes.

Under a property state you can store any data and utility functions. Components can even only consist of state and be used by other components. The usage of state in components is presented in the next section.

Subscriptions

Subscriptions are links between components. They automatically update dynamic properties. When you use a property X of a component A to generate a property Y in a component B, Gruu will internally connect components A and B with property relation A.X => B.Y. It even works with nested properties e.g. A.Z.X => B.Y

const A = Gruu.createComponent({
  _type: 'div',
  state: {
    text: 'state of A'
  }
})
const B = Gruu.createComponent({
  _type: 'span',
  $textContent: () => A.state.text
})

Whenever the property A.state.text changes, the property textContent of the component B is going to be updated. The update will happen asynchronously.

Value of every property starting with "$" should be a function which returns a value that is going to be assigned to the property named by removing the "$" character e.g.

The example below shows a TODO app. Components store and todo are connected with properties relation store.state.todo => todo.children. Every time the ADD button is clicked, the value of input is pushed to the store.state.todo array. Then it triggers todo.children rerender. Clicking on an element of the list reassigns the store.state.todo variable deleting the element. When number of TODO items exceeds 5, ADD button becomes disabled.

const store = Gruu.createComponent({
  state: {
    todo: ['buy milk', 'walk the dog']
  }
})

const input = Gruu.createComponent({
  _type: 'input',
  oninput (e) {
    this.value = e.currentTarget.value
  }
})

const addButton = Gruu.createComponent({
  _type: 'button',
  textContent: 'ADD',
  $disabled: () => store.state.todo.length > 5,
  onclick () {
    store.state.todo = [...store.state.todo, input.value]
    input.value = ''
  }
})

const todo = Gruu.createComponent({
  _type: 'ul',
  $children: () => store.state.todo.map((item, index) => ({
    _type: 'li',
    _key: item,
    textContent: item,
    onclick () {
      store.state.todo = [
        ...store.state.todo.slice(0, index),
        ...store.state.todo.slice(index + 1)
      ]
    }
  }))
})

const todoApp = Gruu.createComponent({
  _type: 'div',
  children: [input, addButton, todo]
})

Rendering

In order to render app written in Gruu you have to execute a function renderApp which takes a node where components will be rendered and an array of components to render.

const todoContainer = document.querySelector('#todo-app')
Gruu.renderApp(todoContainer, [todoApp])

Usage

Direct usage

<head>
  <script src="https://mareklabuz.github.io/gruu-docs/gruu.js"></script>
</head>

Package managers

Install:

$ npm install gruujs
$ yarn add gruujs

Import:

import Gruu from 'gruujs'

Widgets

Gruu is capable of creating full Single Page Applications. However it can be also used to create standalone widgets that are as easy to import as copy & paste :) Actually the main goal of Gruu was to provide a tool to create html content that changes dynamically.

Routing

GruuRouter allows to handle routing in Gruu application. It is an optional package, that is why it has be separated. GruuRouter introduces three objects:

router

It is an object used to control routing. Function router.goTo(path) navigates to the url provided as a path e.g. router.goTo('/about') or router.goTo('/users/53')

route

It is a factory function that creates a route component. The first argument is always a path. The second argument can be a component or a function. Path can be any string as long as it is a valid RegExp describing a URL pathname. However path can also contain URL parameters starting with ":" e.g. '/user/:id'.

Component as a second argument:

Meaning: Render the component whenever the current URL matches the provided path.
The match algorithm will run on every url change (each execution of router.goTo). Example:

const example = Gruu.createComponent({
  _type: 'div'
  children: [
    GruuRouter.route('/home', { _type: 'div', textContent: 'home' }),
    GruuRouter.route('/about', { _type: 'div', textContent: 'about' })
  ]
})

Function as a second argument:

Meaning: Render a component that is a result of the execution of the function whenever the current URL matches the provided path.
If you use url parameters, a function is going to be executed with an argument that is a javascript object containing parameters. The function should return a component that will be rendered. Example:

const example = Gruu.createComponent({
  _type: 'div'
  children: [
    GruuRouter.route('/users/:id', ({ id }) => ({ _type: 'div', textContent: `User: ${id}` }))
  ]
})

routeSub

Meaning: Execute the callback function whenever the current url matches the provided path.
It works similary to route, but as a second argument it accepts only functions that do not return anything. It can be used to control what is going on in your application basing on an URL. Function routeSub subscribes to router each time it is called. It returns a function that is used to unsubscribe. In the example below once an URL matches '/users/:id' the callback is executed and unsubscribed. It will not be executed again.

const store = Gruu.createComponent({
  state: {}
})

const unsubscribe = GruuRouter.routeSub('/users/:id', ({ id }) => {
  store.state.id = id
  unsubscribe()
})

Keep in mind that paths provided into route or routeSub are regular expressions searched globally. It means that path '/users' will match both '/users/10' and '/home/users'. In order to mark the beginning and the end of a path you can use ^ and $ e.g. path '^/home/users$'.

Routing App

const link = (id, name) => Gruu.createComponent({
  _type: 'div',
  textContent: name,
  onclick () {
    GruuRouter.router.goTo(`/user/${id}`)
  }
})

const row = ([key, value]) => Gruu.createComponent({
  _type: 'tr',
  children: [
    { _type: 'td', textContent: key },
    { _type: 'td', textContent: value }
  ]
})

const routingApp = Gruu.createComponent({
  _type: 'div',
  className: 'routing-app',
  children: [
    {
      _type: 'div',
      children: data.map(({ id, name }) => link(id, name))
    },
    {
      _type: 'div',
      children: [
        GruuRouter.route('/user/:id', ({ id }) => {
          const user = data.find(u => u.id === id)
          return user && {
            _type: 'table',
            children: Object.entries(user).map(row)
          }
        }),
        GruuRouter.route('/(?!(user))', {
          _type: 'div',
          textContent: 'Select user'
        })
      ]
    }
  ]
})

Direct usage

<head>
  <script src="https://mareklabuz.github.io/gruu-docs/gruu-router.js"></script>
</head>

Package managers

Install:

$ npm install gruujs-router
$ yarn add gruujs-router

Import:

import GruuRouter from 'gruujs-router'

JSX

If you know React, you problably also know JSX. For those who don't know, it is a HTML-like syntax that allows you write HTML code in JavaScript. I have written a Babel plugin that transpiles code written in JSX to Gruu. Writing in JSX is faster than in a traditional way and the code is clearer. The JSX code written in React applications differs slightly from JSX in Gruu.

JSX Elements are simply syntactic sugar for Gruu.createComponent({ ... }) therefore you have to include Gruu to your scope while writing in JSX.

Properties

All properties of JSX Element except children, $children and _type are passed to Gruu.createComponent unchanged. Passing property _type is not necessary because it is taken from JSX Element Name. Below, there is an example of the JSX code and the result of transpilation.
const main = (
  <div
    className="container"
    style={{
      backgroundColor: 'red'
    }}
  >
    <span>hello!</span>
  </div>
)
const main = Gruu.createComponent({
  _type: 'div',
  className: 'container',
  style: {
    backgroundColor: 'red'
  },
  children: [
    Gruu.createComponent({
      _type: 'span',
      children: ['hello!']
    })
  ]
})

Children

In JSX you don't pass children as a property. Instead you can place children components between the opening and closing tags. As children you can pass simple texts, other components or dynamic children. Moreover you can mix these types of children within the same JSX Element. All static children passed to JSX Element are assigned to children property. Dynamic children (expressed as a function) are assigned to $children property.

// JSX Text
<span>Simple Text!</span>

// JSX Element
<div>
  <span>Hello!</span>
</div>

// Text Literal
<p>{'Text Literal!'}</p>

// Other components
<div>{component}</div>

// Array of components
<p>
  {['test before', component, 'test after']}
</p>

// Dynamic children ($children)
<div>{() => store.state.counter}</div>
// Mixed
<div>
  <p>Isn't</p>
  it awesome
  {() => [
    <div>to be</div>,
    'able to'
  ]}
  {['to', <p>do</p>]}
  {'this?'}
</div>

Phantom Components

In order to achive a phantom component, you have to define a JSX Element with a tag name $.

<div>
  <$
    state={{
      counter: 0
    }}
  >
    {function () {
      return this.state.counter
    }}
  </$>
</div>

Clock App

Below there is an example app written in JSX. Store component contains a state with a current Date. Function setInterval sets new Date every one second. Component clockApp is subscribed to the store, because it uses the variable date from the store's state. Component clockApp contains a few static children (that are rendered only once) and three hands of the clock. The second-hand changes its rotation property each second. The minute-hand and the hour-hand change only when it is necessary.

const store = <$ state={{ date: new Date() }} />

const hand = (height, width, deg) => (
  <div
    className="hand"
    style={{
      height: `${height}px`,
      width: `${width}px`,
      borderRadius: `${width}px`,
      top: `${200 - height}px`,
      transform: `translate(-50%, 0) rotate(${deg}deg)`,
    }}
  >
  </div>
)

const tick = deg => (
  <div
    className="tick"
    style={{
      transform: `translate(-50%, -50%) rotate(${deg}deg)`
    }}
  >
  </div>
)

const clockApp = (
  <div>
    <div className="clock"></div>
    <div className="disc"></div>
    <div className="whiteDisc"></div>
    {[0, 30, 60, 90, 120, 150].map(tick)}
    {() => {
      const seconds = store.state.date.getSeconds()
      const minutes = store.state.date.getMinutes()
      const hours = store.state.date.getHours()
      return [
        hand(100, 1, 360 * (seconds / 60)),
        hand(85, 3, 360 * (minutes + (seconds / 60)) / 60),
        hand(55, 6, 360 * (hours + (minutes / 60)) / 12)
      ]
    }}
  </div>
)

setInterval(() => {
  store.state.date = new Date()
}, 1000)

Usage

Install:

$ npm install babel-plugin-gruu
$ yarn add babel-plugin-gruu

Include in .babelrc:

{
  "plugins": ["gruu"]
}

Example

Game "You were disconnected" created for js13kGames coding competition using Gruu
https://js13kgames.com/entries/you-were-disconnected