[![npm version](https://badge.fury.io/js/roderic.svg)](https://badge.fury.io/js/roderic) [![Build Status](https://travis-ci.org/stopsopa/roderic.svg?branch=master)](https://travis-ci.org/stopsopa/roderic)


Keywords
webpack, react, redux
License
MIT
Install
npm install roderic@0.0.1430

Documentation

Build Status npm version NpmLicense

Table of Contents

(TOC generated using markdown-toc)

debugging

There are few buildin tools to explore different data structure, all of them are available thanks to library inspc: npm version.

All of tools from this library are load by default. There is no need to import inspc. Just start using straight away methods like:

log(...)
log.t(...)
log.i(...)
log.dump(obj, [length = 2])
logw()

console

/bin/bash console.sh  # will print all registered console scripts

Each command should have it's own help page under --help parameter

reading closest .env

It is usually for standalone scripts. In order get access to environment variables in roderic see chapter: accessing .env parameters

require('dotenv-up'){
    override    : false,
    deep        : 3,
}, false, 'preprocessor.js');

.. this expression will import all variables from .env (try to find up to 3 directories up) and add/override to process.env

See more: dotenv-up npm version library...

accessing .env parameters

Just use process.env but it needs to be properly prepared to work correctly,

For the frontend use just execute below code right on top of every entry file (eg: front.entry.js)

import configPublic from 'public.config';

It will make sure to create individual environment variables from .env file if it doesn't exist natively in process.env. It will also export parameters to window.process.env and also it will export function to access variables to window.env

There is another special tool available to access environment variables that also provide fallback feature for non existing one:

# example .env file content:
# EXISTING=exvalue

import configPublic, { env } from 'public.config';

log.dump({
    existing: env('EXISTING'), // exvalue
    nonexisting: env('SOMETHING ELSE', 'fallback') // fallback
})

this tool can be used in the browser and also on the server side

WARNING: Keep in mind though that any environment variable that starts from PROTECTED_ will not be available in the browser

Environment variales should be always available also under process.env (both environments: on the server and in the browser). It's a good thing though to interact with process.env oboject in try catch blocks. Don't supress error though and at least print somenthing on the screen if something goes wrong like:

try {

    if (typeof process.env.HIDE_TREE !== 'undefined') {

        // do something
    }
} catch (e) {

    log.dump({
        HIDE_TREE_cantdetermine: e
    })
}

determining "dev" || "prod"

In normal case use process.env.NODE_ENV. There are though sometimes cases where you migth work beyound scope of webpack (for example all of the time while you will work in React scope) but you have to still be aware if webpack was executed in prod or dev then relay on generated preprocessor/dotenv.json and do:

import configPublic from 'public.config';

const isProd        = configPublic.isProd();    

express response extensions:

res.jsonNoCache({...json});
    
    it makes internally:
    
        return this.set({
            'Cache-Control' : 'no-store, no-cache, must-revalidate',
            'Pragma'        : 'no-cache',
            'Content-type'  : 'application/json; charset=utf-8',
            'Expires'       : new Date().toUTCString(),
        }).json(json);
        
res.jsonError(errstring);

    it makes internally:
        
        return this.status(409).jsonNoCache({
            error: errstring
        });
        
res.notFound(msg);

    it makes internally:
        
        return this.status(400).jsonNoCache({
            error: msg
        });
        
res.accessDenied(msg);

    it makes internally:
        
        return this.status(403).jsonNoCache({
            error: msg
        });

See also token field in Controllers section.

see: express-extend-res.js

access to request & response in react component (SSR):

...
render() {

    const {
        staticContext,
    } = this.props;        

    // wrap it ALWAYS in catch try because it will crash in browser
    const userAgent = (function () {try {return staticContext.req.headers['user-agent']}catch(e){}}());
}
...

Get client ip address

To get ip address of client use :

req.clientIp

Temporary disabling prerender

This feature is for testing purposes (mainly):

If you add anywhere in currenlty loaded page url GET parameter called "noprerender" - (with or without value, it doesn't matter, existance of the parameter is tested internally) then prerendering on the particular page will be disabled, which means all states will be loaded as a separate fetch request in the browser.

Example:

# original url - prerender enabled:
http://localhost:1025/article/slug-to-the-article

# in order to diable prerender modify link like follow
http://localhost:1025/article/slug-to-the-article?noprerender

# or any combination of
http://localhost:1025/article/slug-to-the-article?noprerender=true
http://localhost:1025/article/slug-to-the-article?noprerender=false
http://localhost:1025/article/slug-to-the-article?noprerender=1
http://localhost:1025/article/slug-to-the-article?noprerender=0
http://localhost:1025/article/slug-to-the-article?otherparam=value&noprerender=1

You can create browser bookmark to faster switch between modes:

javascript:var k=(function(p,h,k){p.has(k)?p.delete(k):p.set(k,'');p=String(p);p?(p='?'+p):(p='');location.href=(location.href.split('#')[0]).split('?')[0]+p+h;}(new URLSearchParams(location.search),location.hash,'noprerender'))

Raw code:

var k = (function (p, h, k) {
    p.has(k) ? p.delete(k) : p.set(k, '');
    p = String(p);
    p ? (p = '?' + p) : (p = '');
    location.href = (location.href.split('#')[0]).split('?')[0] + p + h;
}(new URLSearchParams(location.search), location.hash, 'noprerender'))

RESTful api (transport)

import {
    fetchJson,
    fetchData,
} from 'transport';

const json = {data: 'value'}

const promise = fetchJson('/api/admin/buckets/set', {
    method: 'post',
    body: json,
    always: () => dispatch(loaderOff())
});

Method will automaticly send auth cookie.

Most importantly: Method can be used from node.js and from browser just as it is shown on the example above.

Controllers

Controllers are build on top of Express Router Middleware.

Request object in controller have special field auth which is an object that represent payload of JWT security token. See route /config in users.js controller for example to check how to use it (look for "req.auth.id").

JWT security

access to JWT payload in the controller

req.auth 
// - will be undefined if token not valid or not logged in
// - will be an object if token valid an has payload

// so just do:

if (req.auth) {
    // user logged in
}

// or

if (req.auth && req.auth.role.indexOf('admin') > -1) {
    // user has 'admin' role
    // WARNING: always use two conditions in if statement like above because you can't 
    //          execute req.auth.role.indexOf(...) if req.auth = undefined
}

Router

There is special (CLI) command to list all registered controllers with their corresponding urls:

/bin/bash console.sh routes
/bin/bash console.sh routes --json

Ping endpoint is

/__ping

will return 200 status code if it will be able to find more then 0 users in the users table, otherwise 409 status code.

Helpers tempates

datepicker

Unfortunately library "react-datepicker" is not prepared to work correctly in SSR mode so the "solution" is like follows: (This trick make useful few more libraries despite SSR being involved so it might be considered more general solution for component that don't render anything important from SEO perspective)

import DatePicker from 'react-datepicker';

import node from 'detect-node';

{!node && <DatePicker
    locale="en"
    showTimeSelect
    timeIntervals={15}
    timeFormat="HH:mm"
    dateFormat="YYYY-MM-dd HH:mm"
    onChange={date => editField('published_at', date)}
    selected={form.published_at ? new Date(form.published_at) : null}
    isClearable={true}
    placeholderText="Not specified"
/>}

Semantic UI helpers

See more in the reference of the Button element

Button as a Router Link

To provide "Open Link in New Tab" feature under right click menu on button.

import {
    NavLink,
    Link
} from 'react-router-dom';

import {
    Icon,
    Button,
} from 'semantic-ui-react';

<Button
    // onClick={loginSignOut}
    icon size="mini"
    as={Link}
    to="/signout"
>
    <Icon inverted color='black' name='log out' />
</Button>

Button as a regular link

import {
    Icon,
    Button,
} from 'semantic-ui-react';

<Button
    // onClick={loginSignOut}
    icon size="mini"
    as="a"
    href="/signout"
>
    <Icon inverted color='black' name='log out' />
</Button>

Building SSR component (static fetchData)

import React, { Component } from 'react';

import { withRouter } from 'react-router';

import { connect } from 'react-redux';

import {
    getLanguages,
    getLangUrl,
    getMenu,
    getMainSection,
} from "../../_redux/reducers";

import * as actions from '../../_redux/actions';

import {
    section,
} from '../parts/Footer';

const buildLoad = ({
    store,
    getRouterParams,
    getRouterMatch,
    getGetParameters,
}) => {

    const {
        lang,
    } = getRouterParams();

    return Promise.all([
        store.dispatch(actions.languagesRequestFront(lang)),
        store.dispatch(actions.menuRequest(lang)).then(menu => {

            if (getMainSection(store.getState(), section)) {

                log('footer already loaded');

                return;
            }

            let menuData = [];

            try {

                menuData = menu.find(m => m.title === 'Footer block').children || [];
            }
            catch (e) {

                log('Footer block e', e)
            }

            if (menuData.length) {

                const slug = menuData[0].slug;

                return store.dispatch(actions.mainFetch({
                    section,
                    json: {
                        lang,
                        slug,
                        // ...json,
                        // ...query,
                        // ...getParameters
                    },
                    url     : `/endpoint/page`,
                    extract : json => json.page,
                }))
            }
        })
    ]);
};

class Header extends Component {
    constructor (...props) {

        super(...props);

        this.state = {
            show: false,
        }
    }
    //static fetchData = ({
    //    store,
    //    getRouterParams,
    //    getRouterMatch,
    //    getGetParameters,
    //}) => {
    //    return buildLoad({
    //        store,
    //        getRouterParams,
    //        getRouterMatch,
    //        getGetParameters,
    //    });
    //}
    static fetchData = (...args) => buildLoad(...args);
    getData = () => this.props.load();
    componentDidMount() {

        const { history: { action } } = this.props;

        (  ( this.props.languages === false ) || action === 'PUSH' ) && this.getData();
    }

    render() {

        const {
            languages = [],
            selectedLanguage,
            menu,
        } = this.props;

        const {
            show,
        } = this.state;

        if (languages === false && ! selectedLanguage ) {

            return null;
        }

        const mainMenu = menu ? ((menu.find(m => m.title === "Main menu") || {}).children) : []

        return (
            <div>
                <div className="menu">
                    <div className="burger" onClick={() => {
                        this.setState({show: !show})
                    }}>
                        <div />
                        <div />
                        <div />
                    </div>
                    {/* logo */}
                    <div className="phone"> 
                        phone: 345 678 987
                    </div>
                </div>
                {/* <select
                    style={{float: 'right'}}
                    onChange={e => this.handleChange(e.target.value)}
                    value={selectedLanguage.shortname}
                >
                    {languages.map(lang => (
                        <option
                            key={lang.id}
                            value={lang.shortname}
                            // selected={lang.id === selectedLanguage.id}
                        >{lang.name}</option>
                    ))}
                </select> */}
                {(mainMenu && mainMenu.length) && <Level level={mainMenu} className={show ? 'show' : ''}/>}
                
            </div>
        );
    }
}

const mapStateToProps = (state, params) => {

    let lang;

    try {
        
        lang = params.match.params.lang;
    }
    catch (e) {

    }

    return ({
        menu                : getMenu(state),
        languages           : getLanguages(state),
        selectedLanguage    : getLangUrl(state, lang),
    });
};

const mapDispatchToProps = (dispatch, props) => {

    const {
        history,
    } = props;

    return {
        load(extra = {}) {
            return buildLoad(dispatch.extract({
                component: Header, // just for error logs
                dispatch,
                props,
                // extra: {...pass something extra to buildLoad here if you need},
            }));
        },
        onCreate() {
            history.push('/admin/tags/create')
        },
        onEdit(id) {
            history.push('/admin/tags/edit/' + id)
        },
        deleteItem(id) {
            dispatch(actions.tagsDelete(id))
        }
    };
};

export default withRouter(connect(
    mapStateToProps,
    mapDispatchToProps
)(Header));

Extra tools

I've separated some tools because they are doing some more universal things that can be used beyound Roderic project.

The library can be found here nlab: npm version