diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..87aaf4b --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "stage-0", "react"], + "plugins": ["transform-runtime"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f9fc21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_STORE +node_modules +static +.module-cache +*.log* + diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ecbd96b --- /dev/null +++ b/Readme.md @@ -0,0 +1,75 @@ +# Ventura Next Poc + +Un proof of concept di una struttura a plugin per una applicazione react + +## Contiene + +- [x] [Webpack](https://webpack.github.io) +- [x] [React](https://facebook.github.io/react/) +- [x] [Redux](https://github.com/reactjs/redux) +- [x] [Babel](https://babeljs.io/) + +## Setup + +``` +$ npm install +``` + +## Running + +``` +$ npm start +``` + +## Come funziona + +Nel file package.json alcune dipendenze possono essere specificate anche dentro l'array *venturaPlugins*. Questo marca queste dipendenze come plugin compatibili con il sistema descritto in seguito, cio non toglie che questi plugin saranno gestibili come pacchetti npm indipendenti. +```json +"venturaPlugins": [ + "Settings", + "Clients", + "Maps" +], +``` +Attraverso webpack viene letto l'array dei plugin e messo in una costante *VENTURA_PLUGINS* che sarà disponibile nell'applicazione. + +```javascript +new webpack.DefinePlugin({ + VENTURA_PLUGINS: JSON.stringify(require("./package.json").venturaPlugins) +}) +``` + +*App*, che è il nostro componente connesso con lo stato di redux, legge la costante VENTURA_PLUGINS e richiede i plugin durante la fase di boot dell'applicazione +```javascript +const plugins = VENTURA_PLUGINS.map(plugin => { + return require('components/' + plugin + '/index.js').default +}); +``` +Successivamente salva i nostri plugin nello stato di redux +```javascript +actions.addPlugins(plugins); +``` +A questo punto, il nostro redux store è informato dei plugin che sono presenti. + +Per esempio, un plugin che voglia aggiungere una voce al menu dell'applicazione, può passare tramite props il componente che implementa questa voce di menu al componente builtin *SideBar* + +In questo POC i plugin sono fondamentalmente delle voci di menu a cui è associato un altro +componente che viene visualizzato nella sezione principale. + +Ogni plugin caricato nella sidebar può essere associato una serie di componenti dipendenti, in questo caso cliccando su un elemento della sidebar viene caricato il componente selezionato in *MainSection*, nel nostro esempio viene caricato un h1 diverso. + +## Caricamento Lazy dei plugin + +Utilizzando [**bundle-loader**](https://github.com/webpack/bundle-loader) di webpack è possibile ottenere il caricamento Lazy dei plugin che quindi, inizialmente non fanno parte del bundle ma vengono caricati dinamicamente in un secondo momento. + +```javascript +const plugin = 'Orders'; +const waitForChunk = require('bundle?lazy!./../../../plugins/Orders/index.js') + +waitForChunk((file) => { + const newPlugin = file.default + actions.addPlugins([newPlugin]); +}); +``` + +Schiacciando sul bottone Add Order in alto a destra del POC, viene caricato un nuovo bundle js che aggiunge una nuova voce di menu, Orders nel nostro caso, caricando anche il suo componente figlio. diff --git a/client/actions/todos.js b/client/actions/todos.js new file mode 100644 index 0000000..5594d6e --- /dev/null +++ b/client/actions/todos.js @@ -0,0 +1,9 @@ + +import { createAction } from 'redux-actions' + +export const addPlugins = createAction('add plugins') +export const showPlugin = createAction('show plugin') +export const editTodo = createAction('edit todo') +export const completeTodo = createAction('complete todo') +export const completeAll = createAction('complete all') +export const clearCompleted = createAction('clear complete') diff --git a/client/components/Clients/index.js b/client/components/Clients/index.js new file mode 100644 index 0000000..e009368 --- /dev/null +++ b/client/components/Clients/index.js @@ -0,0 +1,20 @@ + +import React, { Component } from 'react' +import style from './style.css' +import ClientsMain from './main' + + +class Clients extends Component { + renderPlugin = () => { + this.props.actions.showPlugin(); + }; + render() { + return ( +
  • + Clients +
  • + ); + } +} + +export default Clients diff --git a/client/components/Clients/main.js b/client/components/Clients/main.js new file mode 100644 index 0000000..d4ba598 --- /dev/null +++ b/client/components/Clients/main.js @@ -0,0 +1,17 @@ + +import React, { Component } from 'react' +import style from './style.css' + + +class ClientsMain extends Component { + + render() { + return ( +

    + Clients +

    + ); + } +} + +export default ClientsMain diff --git a/client/components/Clients/style.css b/client/components/Clients/style.css new file mode 100644 index 0000000..e02e374 --- /dev/null +++ b/client/components/Clients/style.css @@ -0,0 +1,5 @@ +.main { + flex: 1; + border-bottom: 1px solid #fff; + color: #fff; +} diff --git a/client/components/Footer/index.js b/client/components/Footer/index.js new file mode 100644 index 0000000..840c1b3 --- /dev/null +++ b/client/components/Footer/index.js @@ -0,0 +1,66 @@ + +import React, { Component } from 'react' +import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from 'constants/filters' +import classnames from 'classnames' +import style from './style.css' + +const FILTER_TITLES = { + [SHOW_ALL]: 'All', + [SHOW_ACTIVE]: 'Active', + [SHOW_COMPLETED]: 'Completed' +} + +class Footer extends Component { + renderTodoCount = () => { + const { activeCount } = this.props + const itemWord = activeCount === 1 ? 'item' : 'items' + + return ( + + {activeCount || 'No'} {itemWord} left + + ) + }; + + renderFilterLink = (filter) => { + const title = FILTER_TITLES[filter] + const { filter: selectedFilter, onShow } = this.props + + return ( + onShow(filter)}> + {title} + + ) + }; + + renderClearButton = () => { + const { completedCount, onClearCompleted } = this.props + if (completedCount > 0) { + return ( + + ) + } + }; + + render() { + return ( + + ) + } +} + +export default Footer diff --git a/client/components/Footer/style.css b/client/components/Footer/style.css new file mode 100644 index 0000000..8cb9a8d --- /dev/null +++ b/client/components/Footer/style.css @@ -0,0 +1,96 @@ + +.normal { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.normal:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.count { + float: left; + text-align: left; +} + +.count strong { + font-weight: 300; +} + +.clearCompleted, +html .clearCompleted:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + visibility: hidden; + position: relative; +} + +.clearCompleted::after { + visibility: visible; + content: 'Clear completed'; + position: absolute; + right: 0; + white-space: nowrap; +} + +.clearCompleted:hover::after { + text-decoration: underline; +} + +@media (max-width: 430px) { + .normal { + height: 50px; + } + + .filters { + bottom: 10px; + } +} \ No newline at end of file diff --git a/client/components/Header/index.js b/client/components/Header/index.js new file mode 100644 index 0000000..2caf7dd --- /dev/null +++ b/client/components/Header/index.js @@ -0,0 +1,14 @@ + +import React, { Component } from 'react' + +class Header extends Component { + render() { + return ( +
    +

    React redux plugin POC

    +
    + ) + } +} + +export default Header diff --git a/client/components/MainSection/index.js b/client/components/MainSection/index.js new file mode 100644 index 0000000..8eca5e4 --- /dev/null +++ b/client/components/MainSection/index.js @@ -0,0 +1,23 @@ + +import React, { Component } from 'react' +import style from './style.css' + + +class MainSection extends Component { + render() { + const { + plugin + } = this.props; + return ( +
    + { + Object.keys(plugin).length > 0 && JSON.stringify(plugin) !== JSON.stringify({}) + ? plugin + :

    Dashboard

    + } +
    + ); + } +} + +export default MainSection diff --git a/client/components/MainSection/style.css b/client/components/MainSection/style.css new file mode 100644 index 0000000..1ff1947 --- /dev/null +++ b/client/components/MainSection/style.css @@ -0,0 +1,7 @@ + +.main { + flex: 3; + padding: 2em; + background: darkseagreen; + color: #fff; +} diff --git a/client/components/Maps/index.js b/client/components/Maps/index.js new file mode 100644 index 0000000..9889c4b --- /dev/null +++ b/client/components/Maps/index.js @@ -0,0 +1,20 @@ + +import React, { Component } from 'react' +import style from './style.css' +import MapsMain from './main' + + +class Maps extends Component { + renderPlugin = () => { + this.props.actions.showPlugin(); + }; + render() { + return ( +
  • + Maps +
  • + ); + } +} + +export default Maps diff --git a/client/components/Maps/main.js b/client/components/Maps/main.js new file mode 100644 index 0000000..01ee55a --- /dev/null +++ b/client/components/Maps/main.js @@ -0,0 +1,17 @@ + +import React, { Component } from 'react' +import style from './style.css' + + +class MapsMain extends Component { + + render() { + return ( +

    + Maps +

    + ); + } +} + +export default MapsMain diff --git a/client/components/Maps/style.css b/client/components/Maps/style.css new file mode 100644 index 0000000..e02e374 --- /dev/null +++ b/client/components/Maps/style.css @@ -0,0 +1,5 @@ +.main { + flex: 1; + border-bottom: 1px solid #fff; + color: #fff; +} diff --git a/client/components/Settings/index.js b/client/components/Settings/index.js new file mode 100644 index 0000000..375a566 --- /dev/null +++ b/client/components/Settings/index.js @@ -0,0 +1,20 @@ + +import React, { Component } from 'react' +import style from './style.css' +import SettingsMain from './main' + +class Settings extends Component { + renderPlugin = () => { + console.log(SettingsMain); + this.props.actions.showPlugin(); + }; + render() { + return ( +
  • + Settings +
  • + ); + } +} + +export default Settings diff --git a/client/components/Settings/main.js b/client/components/Settings/main.js new file mode 100644 index 0000000..7462e0f --- /dev/null +++ b/client/components/Settings/main.js @@ -0,0 +1,17 @@ + +import React, { Component } from 'react' +import style from './style.css' + + +class SettingsMain extends Component { + + render() { + return ( +

    + Settings +

    + ); + } +} + +export default SettingsMain diff --git a/client/components/Settings/style.css b/client/components/Settings/style.css new file mode 100644 index 0000000..e02e374 --- /dev/null +++ b/client/components/Settings/style.css @@ -0,0 +1,5 @@ +.main { + flex: 1; + border-bottom: 1px solid #fff; + color: #fff; +} diff --git a/client/components/SideBar/index.js b/client/components/SideBar/index.js new file mode 100644 index 0000000..7654a4e --- /dev/null +++ b/client/components/SideBar/index.js @@ -0,0 +1,23 @@ + +import React, { Component } from 'react' +import style from './style.css' +import Settings from 'components/Settings' + +class SideBar extends Component { + renderPlugins = () => { + return this.props.plugins.map((Component, i) => { + return ; + }) + }; + render() { + return ( +
    +
      + {this.renderPlugins()} +
    +
    + ); + } +} + +export default SideBar diff --git a/client/components/SideBar/style.css b/client/components/SideBar/style.css new file mode 100644 index 0000000..fb7a276 --- /dev/null +++ b/client/components/SideBar/style.css @@ -0,0 +1,15 @@ +.main { + flex: 1; + background: #333; + color: #fff; +} +.ul { + padding: 0; + list-style-type: none; +} +.li { + flex: 1; + padding: 1em; + border-bottom: 1px solid #fff; + color: #fff; +} diff --git a/client/constants/filters.js b/client/constants/filters.js new file mode 100644 index 0000000..1547670 --- /dev/null +++ b/client/constants/filters.js @@ -0,0 +1,4 @@ + +export const SHOW_ALL = 'show_all' +export const SHOW_COMPLETED = 'show_completed' +export const SHOW_ACTIVE = 'show_active' diff --git a/client/containers/App/index.js b/client/containers/App/index.js new file mode 100644 index 0000000..361674d --- /dev/null +++ b/client/containers/App/index.js @@ -0,0 +1,68 @@ + +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import Header from 'components/Header' +import MainSection from 'components/MainSection' +import SideBar from 'components/SideBar' +import * as TodoActions from 'actions/todos' +import style from './style.css' + +class App extends Component { + componentDidMount() { + const { actions, children } = this.props + // EXTERNAL_PLUGINS comes from webpack config + const plugins = EXTERNAL_PLUGINS.map(plugin => { + const waitForChunk = require('bundle?lazy!components/' + plugin + '/index.js') + waitForChunk((file) => { + const newPlugin = file.default + actions.addPlugins([newPlugin]); + }); + }); + } + addPluginRuntime = () => { + const { actions, children } = this.props + const plugin = 'Orders'; + const waitForChunk = require('bundle?lazy!./../../../plugins/Orders/index.js') + + waitForChunk((file) => { + const newPlugin = file.default + actions.addPlugins([newPlugin]); + }); + + } + render() { + const { plugin, plugins, actions, children } = this.props + console.log(plugins); + return ( +
    + +
    +
    + + +
    +
    + ) + } +} + +function mapStateToProps(state) { + return { + plugins: state.plugins, + plugin: state.plugin + } +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators(TodoActions, dispatch) + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App) diff --git a/client/containers/App/style.css b/client/containers/App/style.css new file mode 100644 index 0000000..8dea626 --- /dev/null +++ b/client/containers/App/style.css @@ -0,0 +1,56 @@ + +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + appearance: none; + font-smoothing: antialiased; +} + +body { + margin: 0 auto; + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; + line-height: 1.4em; + color: #4d4d4d; + background: #f5f5f5; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.container { + position: relative; + margin: 0 auto; + width: 1200px; + max-width: 100%; +} +.mainContainer { + display: flex; + justify-content: center; + min-height: 600px; +} +.button { + position: absolute; + right: 0; + padding: 0.5em 1em; + background: gold; +} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..acc645d --- /dev/null +++ b/client/index.html @@ -0,0 +1,27 @@ + + + + + React redux plugin + + +
    + + + + + + diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..5e4450a --- /dev/null +++ b/client/index.js @@ -0,0 +1,22 @@ + +import { Router, Route, browserHistory } from 'react-router' +import { syncHistoryWithStore } from 'react-router-redux' +import { Provider } from 'react-redux' +import ReactDOM from 'react-dom' +import React from 'react' + +import App from 'containers/App' +import configure from 'store' + +const store = configure() +const history = syncHistoryWithStore(browserHistory, store) + +ReactDOM.render( + + + + + + , + document.getElementById('root') +) diff --git a/client/middleware/index.js b/client/middleware/index.js new file mode 100644 index 0000000..3810c74 --- /dev/null +++ b/client/middleware/index.js @@ -0,0 +1,6 @@ + +import logger from './logger' + +export { + logger +} \ No newline at end of file diff --git a/client/middleware/logger.js b/client/middleware/logger.js new file mode 100644 index 0000000..e83552b --- /dev/null +++ b/client/middleware/logger.js @@ -0,0 +1,5 @@ + +export default store => next => action => { + console.log(action) + return next(action) +} \ No newline at end of file diff --git a/client/reducers/index.js b/client/reducers/index.js new file mode 100644 index 0000000..5f78f88 --- /dev/null +++ b/client/reducers/index.js @@ -0,0 +1,11 @@ + +import { routerReducer as routing } from 'react-router-redux' +import { combineReducers } from 'redux' +import plugins from './plugins' +import plugin from './plugin' + +export default combineReducers({ + routing, + plugins, + plugin +}) diff --git a/client/reducers/plugin.js b/client/reducers/plugin.js new file mode 100644 index 0000000..d07ae0d --- /dev/null +++ b/client/reducers/plugin.js @@ -0,0 +1,12 @@ + +import { handleActions } from 'redux-actions' + +const initialState = {} + +export default handleActions({ + 'show plugin' (state, action) { + return { + ...action.payload + } + }, +}, initialState) diff --git a/client/reducers/plugins.js b/client/reducers/plugins.js new file mode 100644 index 0000000..973f3e5 --- /dev/null +++ b/client/reducers/plugins.js @@ -0,0 +1,10 @@ + +import { handleActions } from 'redux-actions' + +const initialState = [] + +export default handleActions({ + 'add plugins' (state, action) { + return [...state, ...action.payload] + }, +}, initialState) diff --git a/client/store/index.js b/client/store/index.js new file mode 100644 index 0000000..543ff14 --- /dev/null +++ b/client/store/index.js @@ -0,0 +1,26 @@ + +import { createStore, applyMiddleware } from 'redux' + +import { logger } from 'middleware' +import rootReducer from 'reducers' + +export default function configure(initialState) { + const create = window.devToolsExtension + ? window.devToolsExtension()(createStore) + : createStore + + const createStoreWithMiddleware = applyMiddleware( + logger + )(create) + + const store = createStoreWithMiddleware(rootReducer, initialState) + + if (module.hot) { + module.hot.accept('reducers', () => { + const nextReducer = require('reducers') + store.replaceReducer(nextReducer) + }) + } + + return store +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..31d5f91 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "react-redux-plugin", + "version": "1.0.0", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "webpack-dev-server -d --history-api-fallback --hot --inline --progress --colors --port 3000", + "build": "NODE_ENV=production webpack --progress --colors" + }, + "license": "MIT", + "plugins": [ + "Settings", + "Clients", + "Maps" + ], + "devDependencies": { + "babel-core": "^6.5.2", + "babel-loader": "^6.2.3", + "babel-plugin-transform-runtime": "^6.5.2", + "babel-preset-es2015": "^6.5.0", + "babel-preset-react": "^6.5.0", + "babel-preset-stage-0": "^6.5.0", + "babel-runtime": "^6.5.0", + "bundle-loader": "^0.5.4", + "classnames": "^2.2.3", + "css-loader": "^0.23.1", + "file-loader": "^0.8.5", + "postcss-loader": "^0.8.1", + "react": "^15.0.0", + "react-dom": "^15.0.0", + "react-hot-loader": "^1.3.0", + "react-redux": "^4.4.0", + "react-router": "^2.0.0", + "react-router-redux": "^4.0.0", + "redux": "^3.3.1", + "redux-actions": "^0.9.1", + "rucksack-css": "^0.8.5", + "style-loader": "^0.13.0", + "webpack": "^1.12.14", + "webpack-dev-server": "^1.14.1", + "webpack-hot-middleware": "^2.7.1" + } +} diff --git a/plugins/Orders/index.js b/plugins/Orders/index.js new file mode 100644 index 0000000..1b183f4 --- /dev/null +++ b/plugins/Orders/index.js @@ -0,0 +1,20 @@ + +import React, { Component } from 'react' +import style from './style.css' +import OrdersMain from './main' + + +class Orders extends Component { + renderPlugin = () => { + this.props.actions.showPlugin(); + }; + render() { + return ( +
  • + Orders +
  • + ); + } +} + +export default Orders diff --git a/plugins/Orders/main.js b/plugins/Orders/main.js new file mode 100644 index 0000000..1fd91be --- /dev/null +++ b/plugins/Orders/main.js @@ -0,0 +1,17 @@ + +import React, { Component } from 'react' +import style from './style.css' + + +class OrdersMain extends Component { + + render() { + return ( +

    + Orders +

    + ); + } +} + +export default OrdersMain diff --git a/plugins/Orders/style.css b/plugins/Orders/style.css new file mode 100644 index 0000000..e02e374 --- /dev/null +++ b/plugins/Orders/style.css @@ -0,0 +1,5 @@ +.main { + flex: 1; + border-bottom: 1px solid #fff; + color: #fff; +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..3dfb008 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,78 @@ +var rucksack = require('rucksack-css') +var webpack = require('webpack') +var path = require('path') + +module.exports = { + context: path.join(__dirname, './client'), + entry: { + jsx: './index.js', + html: './index.html', + vendor: [ + 'react', + 'react-dom', + 'react-redux', + 'react-router', + 'react-router-redux', + 'redux' + ], + }, + output: { + path: path.join(__dirname, './static'), + filename: 'bundle.js', + }, + module: { + loaders: [ + { + test: /\.html$/, + loader: 'file?name=[name].[ext]' + }, + { + test: /\.css$/, + include: /client/, + loaders: [ + 'style-loader', + 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ] + }, + { + test: /\.css$/, + exclude: /client/, + loader: 'style!css' + }, + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + loaders: [ + 'react-hot', + 'babel-loader' + ] + }, + ], + }, + resolve: { + modulesDirectories: [ + 'client', + 'node_modules', + ], + extensions: ['', '.js', '.jsx'] + }, + postcss: [ + rucksack({ + autoprefixer: true + }) + ], + plugins: [ + new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), + new webpack.DefinePlugin({ + 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') } + }), + new webpack.DefinePlugin({ + EXTERNAL_PLUGINS: JSON.stringify(require("./package.json").plugins) + }) + ], + devServer: { + contentBase: './client', + hot: true + } +}