Snippets

Jean-Jacques Dubray Isomorphic JavaScript with SAM | the Blog Sample

Created by Jean-Jacques Dubray last modified
<html>
<head>
	<title>SAM Sample | Blog Posts</title>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
	<meta name="description" content="">
	<meta name="author" content="">

	<title>Blog Template for Bootstrap Using the SAM Pattern</title>

	<!-- Bootstrap core CSS -->
	<link href="http://getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet">

	<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
	<link href="http://getbootstrap.com/assets/css/ie10-viewport-bug-workaround.css" rel="stylesheet">

	<!-- Custom styles for this template -->
	<link href="http://getbootstrap.com/examples/blog/blog.css" rel="stylesheet">

	<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
	<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
	<script src="http://getbootstrap.com/assets/js/ie-emulation-modes-warning.js"></script>

	<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
      <![endif]-->

  </head>
  <body>

  	<div class="blog-masthead">
  		<div class="container">
  			<nav class="blog-nav">
  				<a class="blog-nav-item active" href="#">Home</a>
  				<a class="blog-nav-item" href="http://sam.js.org">About</a>
  			</nav>
  		</div>
  	</div>

  	<div class="container">

  		<div class="blog-header">
  			<h1 class="blog-title">The SAM Pattern Blog</h1>
  			<p class="lead blog-description">The official blog sample impleting the SAM Pattern</p>
  		</div>

  		<div class="row">

  			<div class="col-sm-8 blog-main">

  				<div id="representation">
  					<blockquote>Problem is (follow me closely here, the science is pretty complicated), if I cut a hole in the Hab, the air won't stay inside anymore.</blockquote><br>
  					- Andy Weir, The Martian
  				</div>
  			</div>
  		</div>

  	</div>


  	<footer class="blog-footer">
  		<p>Blog template built for <a href="http://getbootstrap.com">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.</p>
  		<p>
  			<a href="#">Back to top</a>
  		</p>
  	</footer>


    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script>window.jQuery || document.write('<script src="http://getbootstrap.com/assets/js/vendor/jquery.min.js"><\/script>')</script>
    <script src="http://getbootstrap.com/dist/js/bootstrap.min.js"></script>
    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <script src="http://getbootstrap.com/assets/js/ie10-viewport-bug-workaround.js"></script>

    <!-- The file blog.js is the isomorphic part of the application -->
    <script type="text/javascript" src="blog.js"></script>

    <script type="text/javascript">

		// This sample was inspired from @rajaraodv
		// https://medium.com/@rajaraodv/a-guide-for-building-a-react-redux-crud-app-7fe0b8943d0f#.e7501tdde

		function present(data) {
			// client side
			//model.present(data) ;

			// server side
			$.post( "http://107.170.242.211:5425/app/v1/present", data) 
			.done(function( representation ) {
				$( "#representation" ).html( representation );
			}		
			);
		}

		function init() {
			// client side
			//view.display(view.init(model)) ;

			// server side
			$.get( "http://107.170.242.211:5425/app/v1/init", function( data ) {
				$( "#representation" ).html( data );
			}		
			);
		}



		// Display initial state
		init() ;


	</script>
</body>
</html> 
////////////////////////////////////////////////////////////////////////////////
// Model 
//
const COUNTER_MAX = 10 ;

var model = {
              posts: [
                {
                  id: 1,
                  title: "The SAM Pattern",
                  description: "SAM is a new reactive/functional pattern that simplifies Front-End architectures by clearly separating the business logic from the view and, in particular, strictly decoupling back-end APIs from the Front-End. SAM is technology independent and as such can be used to build Web Apps or Native Apps"
                },
                {
                  id: 2,
                  title: "Why I no longer use MVC Frameworks",
                  description: "The worst part of my job these days is designing APIs for front-end developers. "
                }
              ],
              itemId : 3 
            } ;

model.present = function(data,next) {
    data = data || {} ;
    
    if (data.deletedItemId !== undefined) {
        var d = -1 ;
        model.posts.forEach(function(el,index) {
            if (el.id !== undefined) {
                if (el.id == data.deletedItemId) {
                    d = index ;       
                }
            }
        });
        if (d>=0) {
            model.lastDeleted = model.posts.splice(d,1)[0] ;
        }
    }
    
    if (data.lastEdited !== undefined) {
        model.lastEdited = data.lastEdited ;
    } else {
        delete model.lastEdited ;
    } 
    
    if (data.item !== undefined) {
        if (data.item.id !== null) {
            // has been edited
            model.posts.forEach(function(el,index) {
                if (el.id !== undefined) {
                    if (el.id == data.item.id) {
                        model.posts[index] = data.item ;       
                    }
                }
            });
            
        } else {
            // new item
            data.item.id = model.itemId++ ;
            model.posts.push(data.item) ;
        }
    }
    
    state.render(model,next) ;
}


////////////////////////////////////////////////////////////////////////////////
// View
//
var view = {} ;

// Initial State
view.init = function(model) {
    return view.ready(model) ;
} ;

// State representation of the ready state
view.ready = function(model) { 
    model.lastEdited = model.lastEdited || {} ;
    var titleValue = model.lastEdited.title || 'Title' ;
    var descriptionValue = model.lastEdited.description || 'Description' ;
    var id = model.lastEdited.id || '' ;
    var cancelButton = '<button id="cancel" onclick="JavaScript:return actions.cancel({});\">Cancel</button>\n' ;
    var valAttr = "value" ;
    var actionLabel = "Save" ;
    var idElement = ', \'id\':\''+id+'\'' ;
    if (id.length === 0) { cancelButton = '' ; valAttr = "placeholder"; idElement = "" ; actionLabel = "Add"}
    var output = (
            '<br><br><div class="blog-post">\n\
               '+model.posts.map(function(e){
                   return(
                        '<br><br><h3 class="blog-post-title" onclick="JavaScript:return actions.edit({\'title\':\''+e.title+'\', \'description\':\''+e.description+'\', \'id\':\''+e.id+'\'});">'+e.title+'</h3>\n'
                       +'<p class="blog-post-meta">'+e.description+'</p>'
                       +'<button onclick="JavaScript:return actions.delete({\'id\':\''+e.id+'\'});">Delete</button>') ;
                   }).join('\n')+'\n\
             </div>\n\
             <br><br>\n\
             <div class="mdl-cell mdl-cell--6-col">\n\
               <input id="title" type="text" class="form-control"  '+valAttr+'="'+titleValue+'"><br>\n\
               <input id="description" type="textarea" class="form-control" '+valAttr+'="'+descriptionValue+'"><br>\n\
               <button id="save" onclick="JavaScript:return actions.save({\'title\':document.getElementById(\'title\').value, \'description\': document.getElementById(\'description\').value'+idElement+'});">'+actionLabel+'</button>\n\
               '+cancelButton+'\n\
             </div><br><br>\n'
        ) ;
    return output ;
} ;


//display the state representation
view.display = function(representation,next) {
    var stateRepresentation = document.getElementById("representation");
    stateRepresentation.innerHTML = representation;

    // next(representation) ;
} ;



////////////////////////////////////////////////////////////////////////////////
// State
//
var state =  { view: view} ;

model.state = state ;

// Derive the state representation as a function of the systen
// control state
state.representation = function(model,next) {
    var representation = 'oops... something went wrong, the system is in an invalid state' ;

    if (state.ready(model)) {
        representation = state.view.ready(model) ;
    } 
    
    state.view.display(representation,next) ;
} ;

// Derive the current state of the system
state.ready = function(model) {
   return true ;
} ;




// Next action predicate, derives whether
// the system is in a (control) state where
// a new (next) action needs to be invoked

state.nextAction = function(model) {} ;

state.render = function(model,next) {
    state.representation(model,next)
    state.nextAction(model) ;
} ;


////////////////////////////////////////////////////////////////////////////////
// Actions
//


var actions = {} ;

actions.edit = function(data) {
	data.lastEdited = {title: data.title,  description: data.description, id: data.id } ;
    present(data) ;
    return false ;
} ;

actions.save = function(data) {
    data.item = {title: data.title, description: data.description, id: data.id || null} ;
    present(data) ;
    return false ;
} ;

actions.delete = function(data) {
    data = {deletedItemId: data.id} ;
    present(data) ;
    return false ;
} ;

actions.cancel = function(data) {
    present(data) ;
    return false ;
} ;
// 

var express = require('express');
var bodyParser = require('body-parser') ;
var morgan = require('morgan') ;
var postman = require('./postman') ;


var config = {} ;
config.port = 5425 ;
config.loginKey = 'abcdef0123456789' ;
config.adminDirectory = './console/bower_components'
config.username = 'sam' ;
config.password = 'nomvc' ;

var serverResponses = {
    tooBusy: function(req,res) {
        res.writeHead(429, { 'Content-Type': 'text/plain' });
        res.end("Server is too busy, please try again later") ;
    },
    
    unauthorized: function(req,res) {
        res.header('Content-Type', 'text/html') ;
        res.status(401).send('<htnl><body>Unauthorized access</body></html>') ;  
    },
    
    serverError: function(req,res) {
        res.header('Content-Type', 'text/html') ;
        res.status(500).send('<htnl><body>Server Error</body></html>') ;  
    }
} ;

var authnz = {

    authorized: function (cookies) {
        if (cookies.authorized > 0) {
            return true ;
        }
        return false ;
    },
    
    del: function(req, res, cookie) { 
        if (cookie !== undefined) {
            res.clearCookie(cookie);  
        }
    },
    
    set: function(req, res, cookie) { 
        if (cookie !== undefined) {
             res.cookie(cookie, +new Date(), { maxAge: 3600000, path: '/' }); 
        }
    },
    
    isSet: function(req, res, cookie) { 
        if (cookie !== undefined) {
             return res.cookies[cookie]; 
        }
    },
    
    validateCredentials: function(username,password) {
        return ((username === config.username) && (password === config.password)) ;
    }

} ;

var app = express();
app.use(morgan('combined')) ;
app.use(bodyParser.raw()) ;
app.use(bodyParser.urlencoded({ extended: true }));

var cookieParser = require('cookie-parser') ;
app.use(cookieParser()) ;

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

app.use('/html', express.static(config.adminDirectory));


var v = '/v1' ;
var r = 'app' ;
var a = 'api' ;
var apis = {
    login: '/'+r+v+'/login',
    logout: '/'+r+v+'/logout',
    present: '/'+r+v+'/present',
    init: '/'+r+v+'/init'
} ;


var postman = require('./postman') ;

postman.addAPI(r, 'login', config.loginKey) ;
app.post(apis.login,function(req,res) { 
    var username = req.body.username,
        password = req.body.password ;
    
    if (authnz.validateCredentials(username,password)) {
            console.log('Authorized') ;
            authnz.set(req,res,'authorized') ;
            res.status(200).send({authorized:true, user: {firstName:'Paul', lastName:'Smith', tel:'+1-425-555-1212'}}) ;
    } else {
        console.log('Unauthorized access') ;
        res.status(200).send({authorized:false}) ;
    }
}) ;

postman.addAPI(r, 'logout', config.loginKey) ;
app.get(apis.logout,function(req,res) { 
    authnz.del(req,res,'authorized') ;
    res.status(200).send({authorized:false}) ;
}) ;


// The file blog.js is the isomorphic part of the application
require('./blog.js') ;

postman.addAPI(r, 'present', config.loginKey) ;
app.post(apis.present,function(req,res) { 
    var data = req.body ;
    model.present(data, function(representation) {
        res.status(200).send(representation) ;
    }) ;
}) ;


postman.addAPI(r, 'init', config.loginKey) ;
app.get(apis.init,function(req,res) { 
    res.status(200).send(view.init(model)) ;
}) ;



app.listen(config.port, function() {
    console.log("registering app on port: "+config.port) ;
    //setTimeout(register(),2000) ; 
});
{
  "name": "sam-isomorphic blog-sample",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "*",
    "cookie-parser": "",
    "express": "4.13.x",
    "morgan": "1.6.x"
  },
  "preferGlobal": false,
  "private": true
}

Comments (0)