Skip to content

Instantly share code, notes, and snippets.

@samuelgoto
Last active August 9, 2017 23:38
Show Gist options
  • Save samuelgoto/d8d510032e93e773454eb2ad24fdb9a2 to your computer and use it in GitHub Desktop.
Save samuelgoto/d8d510032e93e773454eb2ad24fdb9a2 to your computer and use it in GitHub Desktop.

Introduction

This is an exploration of a DST to be embedded in javascript designed specifically to build and manipulate the HTML DOM.

Largely inspired by a {Kotlin, JFX, Protobuf, JSON-ish}-like syntax (to intermingle well with javascript code) and a JSX-like DOM building algorithm.

Basic idea

Introduce syntax to javascript to intermingle tree-like docs and code and vice versa. Something like this:

let doc = div {
  // comments inside document

  a         // child
  "order"   // child
  b         // child
  "matters" // child
  c         // child
  
  d(attribute: value) { // child
    e { // child's child
      f(g: false) // child's child's child with attribute
    }
  }
  
  // if-expressions
  if (cond) {
    a
  } else {
    b
  } 
  
  // for-expressions
  for (u in g) {
    a {
      b { `u.b` }
    }
  }
  
  // function calls and expressions
  var a = g();
  
  // inline callbacks
  div(onclick : function() { alert("hi"); }) {
    
  }
  
  
  // async document building
  await fetch("data.pb").map(u => {
    div {
      span {
        `u.b`
      }
    }
  })
};

Example

hello world

var doc = div {
  // New syntax introduced to build documents.
  div {
    // This is a text node and a child
    "hello world"
    
    // Also, comments allowed!!!
  }
};

attributes

var doc = div {
  form {
    "Enter your name:"
    input(enabled: false) {
    }
  }
};

callbacks

var doc = div {
  div {
     "hello world", 
     // inline callbacks
     onclick = function() {
        alert("hi");
      }
  }
};

expressions

var people = ["goto", "bnutter"];

var doc = div {
  // for-expressions enable you to iterate and add multiple nodes to the parent node.
  for (person in people) {
    div {
      p { a(href: `/users/{{person.id}}`) { `{person.name}` } },
    }
  },
  
  // arrow functions
  people.map(person => { div { p { `{{person.name}}`} })
  
  // try-catch expressions
  try { avatar(user); } catch {  { div { "invalid user id" }} },
  
  // TODO(goto): if-then-else expressions
  // TODO(goto): select operator, switch-like expressions

};

async-await

var doc = div {
  // async-await
  var users = await fetch("people.xml");
  users.map(user => div { `user.name` });
};

CSS Typed OM

var doc = div {
  div(
    // Uses CSS's TypedOM, 
    style: {
      width: "100%",
      position: "absolute"
    }) {
    "hello world"
  }
};

custom elements

// Doc-expressions can also be represented as classes that implement an Element interface.
// For example:

var doc = div {
  head {
   // CSS in JS!!!
   await fetch(["main.css-in-js", "hello.css-in-js"]).map(style => new Style(style));
  },
  body {
    bind ["goto", "bnutter"].map(user => new User(user)),

    // web-components like syntax, creating new node types!
    User {
      name: "Sam"
    }
  },
}

class Style implements Element {
  doc() {
    return div { "hello world" };
  }
}

class User implements Element {
  doc() {
    return div { "hello world" };
  }
}

Related Work

  • JSX
  • Kotlin typed builders
  • Elm
  • Hyperscript
  • json-ish
  • Om
  • Flutter
  • Anko layouts
  • Curl
  • JFX Script
  • JXON
  • E4X
@samuelgoto
Copy link
Author

samuelgoto commented Aug 2, 2017

more thoughts

Simple

function a() {
  return <div></div>;
}

function a() {
  return div {
    // hello world
  };
}

function a() {
  return __generic__("div", [], () => { 
    // hello world
  });
}

class Element {

}

function __generic__(name: string, args: Array<?>, init: Element => Array<Element>): Element {
  var el = new Element(arguments);
  var proxy = new Proxy(el, {
    get(target, key) {
      console.log(`accessing ${key} on ${target}`);

      // console.log(target.parent);
      if (target[key]) {
        return target[key];
      }
      
      var child = children.add(__generic__(key, ...));

      return child;
    }
  });
  init.call(proxy);
  return React.createElement("div", el.props, el.children);
}

function a() {
  return div {
    div {
    }
  }
}

function a() {
  return __generic__("div", [], () => {
    __generic__("div", [], () => {
    });
  });
}


function a() {
  return React.createElement("div", null);
}

Nested

function a() {
  return <div>
    <span></span>
    <span></span>
  </div>;
}

function a() {
  return div { 
    span {} 
    span {}
  }
}



ffunction a() {
  return React.createElement(
    "div",
    null,
    React.createElement("span", null),
    React.createElement("span", null)
  );
}

@samuelgoto
Copy link
Author

React implementation

class Element {
  constructor(name, args) {
    this.name = name;
    this.args = args;
    this.children = [];
  }

  addChild(el) {
    this.children.push(el);
  }
}

function __generic__(name, args, body) {
  console.log(`__generic__ ${name} ${args} ${body}`);
  let el = new Element(name, args);
  body.call(el);
  if (this instanceof Element) {
    console.log(`I have a parent!!`);
    this.addChild(el);
  } else {
    console.log(`I don't have a parent :(`);
    return el;
  }
}

let result = __generic__.call(this, "div", {foo: 1}, function() {
  console.log("am i an element?");
  console.log(this);
  __generic__.call(this, "span", {bar: 2}, function() {
    // hello world
  });
});

console.log(result);

@samuelgoto
Copy link
Author

// react

function buttonBar(x1,x2,x3){ 
  return
   <div>
     <button>{x1}</button>
     <button>{x2}</button>
     <button>{x3}</button>
   </div> 
}


// hyperscript
var h = require('hyperscript')
var obj = {
  a: 'Apple',
  b: 'Banana',
  c: 'Cherry',
  d: 'Durian',
  e: 'Elder Berry'
}
h('table',
  h('tr', h('th', 'letter'), h('th', 'fruit')),
  Object.keys(obj).map(function (k) {
    return h('tr',
      h('th', k),
      h('td', obj[k])
    )
  })
)

// JFX
 import javafx.stage.Stage;
 import javafx.scene.Scene;
 import javafx.scene.text.Text;
 import javafx.scene.text.Font;
 
 Stage {
     title: "Hello World"
     width: 250
     height: 80
     scene: Scene {
         content: Text {
             font : Font {
                 size : 24
             }
             x: 10, y: 30
             content: "Hello World"
         }
     } 
 }


// 

kotlin

// kotlin

fun main(args: Array<String>) {
    return
            html {
                head {
                    title { +"XML encoding with Kotlin" }
                }
                body {
                    h1 { +"XML encoding with Kotlin" }
                    p { +"this format can be used as an alternative markup to XML" }

                    // an element with attributes and text content
                    a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }

                    // mixed content
                    p {
                        +"This is some"
                        b { +"mixed" }
                        +"text. For more see the"
                        a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }
                        +"project"
                    }
                    p { +"some text" }

                    // content generated from command-line arguments
                    p {
                        +"Command line arguments were:"
                        ul {
                            for (arg in args)
                                li { +arg }
            }
                    }
                }
            }
}

@samuelgoto
Copy link
Author

// Technique #1 string building
let fragment = "";
fragment += "<div>";
fragment += "  <span>hello world</span>";
fragment += "</div>";
let node = document.createElement("div").innerHTML = fragment;
document.body.appendChild(node);

// Technique #2 imperative calls
let root = document.createElement("div");
let span = document.createElement("span");
let content = document.createTextNode("hello world");
span.appendChild(content);
root.appendChild(span);
document.body.appendChild(root);

// Technique #3: template languages
let root = mycomponent();
document.body.appendChild(root);

// mycomponent.soy, gets transpiled into JS
{template name="mycomponent"}
  hello world
{/template}

// Technique #4: DSL, extendeds JS
let root = 
  <div>
    <span>hello world</span>
  </div>;
document.body.appendChild(root);

let fragment = div {
  div {
    // New syntax introduced to build documents.
    div {
      // This is a text node and a child
      "hello world"    
      // Also, comments allowed!!!
    }
  }
};

document.body.write(fragment);

@samuelgoto
Copy link
Author

samuelgoto commented Aug 7, 2017

Some options for syntax for configuring a builder:

// Cast-like expression
let a = (HtmlElement) div {
  span {
  }
}

// Cast-like expression, no parens
let a = html div {
  span {
  }
}

"pragma"-like statement
let a = "html" div {
  span {
  }
}

// Root-level element
let a = html {
   div {
    span {
    }
  }
}

// decorator
let a = @html div {
  span {
  }
}

// #hash
let a = #html div {
  span {
  }
}

let a = div(@doc = html) {
  span {
  }
}

// Extra syntax
let a = doc(html) {
   div {
    span {
    }
  }
}

let a = html#div() {
  span {
  }
}

let a = div#html() {
  span {
  }
}

let a = div@html() {
  span {
  }
}

let a = div as html {
  span {
  }
}

let a = div[html] {
  span {
  }
}

let a = /** HtmlElement */ div {
  span {
  }
}

let a = new HtmlElement() {
  div {
    span {
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment