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.
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
As you may have noticed a tag name is passed to componentes as a property
Tag name. The only exception is
Wherever you generate children of a component dynamically it is advisable to use
Components that are going to be rendered inside the given component.
Internal
Properties that are calculated dynamically. See more in section Subscriptions.
Properties like
There are two kinds of components: DOM Components and Phantom Components.
All component that don't have
They have their representaion in the HTML DOM.
const hello =Gruu.createComponent ({_type : 'div',textContent : 'Hello World!' })
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.
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
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
Subscriptions are links between components. They automatically update dynamic properties.
When you use a property
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
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
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] })
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])
<head> <script src="https://mareklabuz.github.io/gruu-docs/gruu.js"></script> </head>
$ npm install gruujs $ yarn add gruujs
import Gruufrom 'gruujs'
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.
GruuRouter allows to handle routing in Gruu application. It is an optional package, that is why it has be separated. GruuRouter introduces three objects:
It is an object used to control routing. Function
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'.
Meaning: Render the component whenever the current URL matches the provided path.
The match algorithm will run on every url change (each execution of
const example =Gruu.createComponent ({_type : 'div'children : [GruuRouter.route ('/home', {_type : 'div',textContent : 'home' }),GruuRouter.route ('/about', {_type : 'div',textContent : 'about' }) ] })
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}` })) ] })
Meaning: Execute the callback function whenever the current url matches the provided path.
It works similary to
const store =Gruu.createComponent ({state : {} })const unsubscribe =GruuRouter.routeSub ('/users/:id', ({ id }) => { store.state.id = id unsubscribe() })
Keep in mind that paths provided into
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' }) ] } ] })
<head> <script src="https://mareklabuz.github.io/gruu-docs/gruu-router.js"></script> </head>
$ npm install gruujs-router $ yarn add gruujs-router
import GruuRouterfrom 'gruujs-router'
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
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!'] }) ] })
In JSX you don't pass
// 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>
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>
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
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)
$ npm install babel-plugin-gruu $ yarn add babel-plugin-gruu
{ "plugins": ["gruu"] }
Game "You were disconnected" created for js13kGames coding competition using Gruu
https://js13kgames.com/entries/you-were-disconnected