These are chat archives for jdubray/sam

23rd
Mar 2017
Rodrigo Carranza
@DrecDroid
Mar 23 2017 13:31
For those who want to render the same html in client and server, there is this lib called hyperscript, It removes the hassle of using ES6 templates.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 14:34
:thumbsup: - though I would not qualify ES6 literals as "hassle".
Rodrigo Carranza
@DrecDroid
Mar 23 2017 15:08
:D they're for me for DOM Element generation, for other things are really helpful, like building regex expressions or passing config variables.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 15:24
To be frank, I view it the other way around, hyperscript makes it difficult to work with designers, it's an extra step in your workflow that adds very little value. If you can do everything in HS, that's probably great but when your input is HTML/CSS/JQuery, I don't see how HS would make things better.
ford
@foxaal
Mar 23 2017 15:38
I don't know if it relates because I don't know HS, but it seems to me that ES6 is a giant step forward for JavaScript, just on import/export, arrow functions and .reduce alone.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 15:48
yes, overall a big step forward, hard to go back to ES5 once you tasted it
Rodrigo Carranza
@DrecDroid
Mar 23 2017 15:49
@jdubray The great thing about hyperscript in my case ( I need to render the same html the client will generate using ajax but in the server, I think the word is Isomorphic app?) but In the server I don't need to generate event handlers like onclick they will be added in the client; hyperscript strips passed handlers when calling .outerHTML or innerHTML. I think, designers would handle the generated html to test their design, or they'll sends the mockup to the programmer to implement It.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 15:49
In all the SAM samples I use Google Traceur to transpile ES5 to ES6, that's super lightweight, when you want to give it a try.
Rodrigo Carranza
@DrecDroid
Mar 23 2017 15:50
ES6 is so refreshing, what I like the most is the Proxy API, sadly It can't be transpiled nor polyfilled
Jean-Jacques Dubray
@jdubray
Mar 23 2017 15:52
@DrecDroid I am sure hyperscript has it's place. SAM is naturally isomorphic, I have showed it over and over. You can use the "intents" mechanism to wire actions (that could still run on the server via a generic dispatch). Intents would allow you to generate 100% of the HTML on the server.
ford
@foxaal
Mar 23 2017 16:37
@DrecDroid Ahh. I am glad to see your github top down SAM code. This seems like goodness. :)
Rodrigo Carranza
@DrecDroid
Mar 23 2017 16:47
Thanks :), I've modified a lot since then, I've moved to Typescript to facilitate the creation of Mutators(a.k.a. Acceptor), Representation(a.k.a State), and Naps. Also the Component class was modified a lot, I'll upload an update soon.
ford
@foxaal
Mar 23 2017 16:47
Yes, hyperscript. Right, I get your comment now better. So no "back ticks" for you.
Rodrigo Carranza
@DrecDroid
Mar 23 2017 16:48
I like backticks but not for DOM Element creation
ford
@foxaal
Mar 23 2017 16:50
@DrecDroid The backticks are good, but so teeny :) OK. Look forward to seeing the direction you take this.
Rodrigo Carranza
@DrecDroid
Mar 23 2017 16:51
This is how my SAM system module looks like.
Fred Daoud
@foxdonut
Mar 23 2017 17:05
@jdubray what are your thoughts on fractal architecture?
Jean-Jacques Dubray
@jdubray
Mar 23 2017 17:19
@foxdonut very interesting article, I'll put it on my reading list. Thank you for sharing.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 17:27
@DrecDroid I would be interested to see how you inject your action calls into the generated HTML
Rodrigo Carranza
@DrecDroid
Mar 23 2017 18:23

@jdubray I separate the Nap in two categories, normal Naps and AsyncNaps

    let _napAction : Action

    this.naps.some(
      nap => {
        _napAction = nap.receive(state)

        return _napAction != null ? true : false
      }
    )

   if(_napAction != null){
      _napAction.do(this.propose)
    }

normal Naps return an Action and prevent other Naps in the Nap pipeline to be executed. This is useful for example when you require a login you could have a Nap like this

class LoginRequired extends AnyNap {
  receive( state ){
    if(!state.logged_in && state.view !== 'login_view'){
      return new actions.GoUrl(routes.login())
    }
  }
}

export default [ new LoginRequired() ]

but there are Naps that doesn't return an Action immediatly, they require an event like a onclick to happen in order to call an Action. The View is an example of an AsyncNap. To connect to the system, one should register the Nap to the System using sam.registerNap method.

import { System } from '__libs__/sam'

import RootView from '__admin__/components/root'
import mutators from '__admin__/step1'
import reprs from '__admin__/step2'
import _naps from '__admin__/step3'

import * as actions from '__admin__/actions'
import config from './config'

let view = new RootView(document.getElementById('app'))

let sam = new System({ debug_mode : config.DEBUG })
              .registerRepresentation(...reprs)
              .registerMutators(...mutators)
              .registerNaps(..._naps, view.nap)

view.init()
sam.action(new actions.GoUrl(document.location.pathname, false))

an AsyncNap looks like this:

export abstract class AsyncNap extends Nap{  
  action: ActionExecutor

  constructor(
    public for_state ?: string
  ){
    super(for_state)
  }

  abstract receiveImpl(state : any) : void

  receive(state) : null{
    this.receiveImpl(state)
    return null
  }

}

by default when instantiated the asyncNap.action is undefined. It is defined inside the sam.registerNaps method, look how only AsyncNaps action property is set, because normal Naps doesn't have this property.

registerNaps(...naps : Nap[]){
    this.naps = [...this.naps, ...naps]

    this
    .naps
    .filter(nap=>nap instanceof AsyncNap)
    .forEach((nap : AsyncNap)=>{
      nap.action = this.action
    })

    return this
  }

the ComponentRoot have the nap as a prop

export class ComponentRoot extends Component{
  _action : (...args ) => void
  nap : AsyncNap = new ViewNap(this)

  init(){}

  constructor(mountPoint){
      super()
      this.rootComponent = this
      this.root = this.mount(mountPoint)
  }
}

and any other Component can get the action through a getter. The following is inside Component class

  get action(){
    return this.rootComponent.nap.action
  }

so inside any Component I can call:

   this.action(new MyAction(args))
Jean-Jacques Dubray
@jdubray
Mar 23 2017 18:48
@DrecDroid just to be clear, nap should return only one action at all time. It should be an error condition if two actions could be triggered for any given state.
Rodrigo Carranza
@DrecDroid
Mar 23 2017 18:56
Of course, Views should only trigger actions on event no immediately, I considered this separation of normal Naps from Async Naps, to find a way for Systems to interact with each other.
Jean-Jacques Dubray
@jdubray
Mar 23 2017 18:58
Views are not connected to nap() in any way
It is the state function which drives nap()
Sorry, I am still having difficulty to see where given a DOM element you add the hook to call an action <input onclick="..." type="submit">Submit</input>
I was expecting the view to fill out these events
based on your earlier post
Jean-Jacques Dubray
@jdubray
Mar 23 2017 19:05
That is an action triggered from the state:
class LoginRequired extends AnyNap {
  receive( state ){
    if(!state.logged_in && state.view !== 'login_view'){
      return new actions.GoUrl(routes.login())
    }
  }
}
Rodrigo Carranza
@DrecDroid
Mar 23 2017 19:26
this is an example of a Component:
import h_ from 'hyperscript'
let h = h_

import { Component } from '__libs__/component'

import * as actions from '__client__/actions'
import * as routes from '__client__/router'


export default class CategoryView extends Component {

  category_items : Element

  init(){
    this.root.appendChild( h('div.category-details') )
    this.root.appendChild( h('div.category-items') )

    this.category_items = this.one('.category-items')
  }

  receive(state){    
    if(state.state === 'view_changed'){
      let category = state.data.category

      category
      .posts
      .sort((b,a) => a.timestamp - b.timestamp)
      .forEach( post => {
        let href = routes.post_view(category.slug, post.slug)

        let element = h('a',
          {
            href,
            onclick : (e)=>{
              this.action(new actions.GoUrl(href))
              e.preventDefault()
              return false;
            }
          },
          h('img.post-thumbnail', { src : post.img_thumb })
        )

        this.category_items.appendChild(element)
      })      
    }
  }
}
Jean-Jacques Dubray
@jdubray
Mar 23 2017 19:44
This looks a bit complicated to me
Jean-Jacques Dubray
@jdubray
Mar 23 2017 19:52
This sample for instance is using intents (which you can of course still use with hyperscript):
view.intents = { edit: 'edit', save: 'save', delete: 'delete', cancel: 'cancel' } ;

// State representation of the ready state
view.ready = function(model,intents) { 
    model.lastEdited = model.lastEdited || {} ;
    intents = intents || view.intents ;
    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.'+intents['cancel']+'({});\">Cancel</button>\n' ;
    var valAttr = "value" ;
    var actionLabel = "Save" ;
    var idElement = ', \'id\':\''+id+'\'' ;
    if (id.length === 0) { cancelButton = '' ; valAttr = "placeholder"; idElement = "" ; actionLabel = "Add"}
you could either directly the intents to a dispatcher or have something like that:
function dispatch(data) {

            $.ajax({
              url: endpoint+"/app/v1/dispatch",
              contentType: "application/json; charset=utf-8",
              type: 'POST',
              dataType: 'json',
              data: JSON.stringify(data),
              success: function(representation){

                $( "#representation" ).html( representation );

              },
              failure: function(errMsg) {
                  alert(errMsg);
              }
            });

        }

        function init() {

            actions.do = dispatch 

            $.get( endpoint+"/app/v1/init", function( data ) {
                $( "#representation" ).html( data );
            }        
            );
        }
Rodrigo Carranza
@DrecDroid
Mar 23 2017 21:12

I don't know, It looks a bit messy for me, I like to get the help from the analyzer and concatenating javascript code inside the html string looks a bit PHPy, I have separated everything in classes(first I had everything in functions and interfaces but classes gets better analyzer support), so I have Action, Mutator and Nap abstract classes. The Action class receives the propose function from the SAM system, the propose function can be called with an instance of Proposal class. The proposal should have a body, and could have an action property set. The Mutator has a for_action field, if it is set then It is executed only for proposal with that action name, if it is not set It is executed for every proposal, the same for Representation but this time it looks for the state.state value, and Nap also looks for the state.state value because the Representation could or not modify that value. So there are other classes like AnyMutator, AnyRepresentation, AnyNap that executes always, the AsyncNap is a variation of the AnyNap but that receives the action executor from the system.

The core components of the system are:

abstract class Action {
  constructor(public name?:string){}
  abstract do(propose : Propose)
}

abstract class Mutator{
  constructor(public for_action ?: string){}
  abstract receive(store, state, proposal) : void
}

abstract class Representation {
  constructor(public for_state ?: string){}
  abstract receive(store, state) : void
}

abstract class Nap {
  constructor(public for_state ?: string){}
  abstract receive(state : any) : Action | null
}

everything is a subclass of one of them, and the View have an AsyncNap inside It to communicate with the system. The only part that have no shape(but one could) are the model and the state objects. The state is not the same as the representation, the state is an object created in every sam loop and is derivation of the model, that's what is ultimately received by the View through the AsyncNap. Look how Mutator.receive has three parameters, Representation.receive only has two, and finally the Nap only receives the state. The View is not itself a Nap, It receives the state from the AsyncNap and knows how to trigger actions because It has a reference to the sam.action action executor. I've done a diagram before about this, but I think I have to make a new one with all the classes I have defined.