Loading...

Chapters

Home
Introduction and Tutorial
Reference - Top Level
Reference - JavaScript
Reference - Knockout
Reference - Types
Reference - Alerts
Reference - Arrays
Reference - Dates
Reference - Globals
Reference - Maths
Reference - Regular Expressions
Reference - Strings

Home

Motivation

If you answered yes to all these questions, then Spanner is for you!

ToDoMVC

Here can be found the canonical Spanner implementation of the todomvc.com demonstration. The source code can be found on the Spanner code site.

A Simple Example: a log-in/sign-up dialogue

In traditional style, we start with something to whet the appetite. Here is a simple log-in/sign-up dialogue:

The reader is invited to scan the Spanner source code, with an eye to getting the gist, before examining the generated HTML and JavaScript to see how clean and traceable it is.

Log-in Demo source code in Spanner.
using System;
using System.Collections.Generic;
using System.Linq;
using Spanner;

namespace SpannerWebSite
{
    internal partial class LoginDemo : UI
    {
        enum Mode { LogIn, SignUp };

        internal static Html Content()
        {
            var mode = ObsVar("mode", Mode.LogIn);

            var name = ObsVar("name", "");

            var password = ObsVar("password", "");

            var email = ObsVar("email", "");

            var errMsg = ObsVar("errMsg", "");

            var busy = ObsVar("busy", false);

            var validEmailRE = Var("validEmailRE", NewRegExp(@"\\w\\w\\w@\\w\\w\\w"));

            var fakeRPC = Procedure("fakeRPC", Do(
                Set(busy, false),
                Alert("Welcome!") // Everyone's a winner.
            ));

            var go = Procedure("go",
                IfCase(
                    When(name == "",
                        Set(errMsg, "Please provide a log-in name.")
                    ),
                    When(password == "",
                        Set(errMsg, "Please provide a valid password.")
                    ),
                    When(mode == Mode.SignUp & !Test(validEmailRE, email),
                        Set(errMsg, "Please provide a valid e-mail address.")
                    ),
                    Otherwise(Do(
                        Set(busy, true),
                        Do(SetTimeout(fakeRPC, 1000)) // Pretend we're talking to a remote service.
                    ))
                )
            );

            var chgMode = Procedure<Mode>("chgMode", x => Do(
                Set(mode, x),
                Set(errMsg, "")
            ));

            var content = Div().WithClass("login-demo").WithChildren(
                Div().WithAttr("style",
                        "border: 4px solid black; margin: 1em; padding: 1em; width: 16em").WithChildren(
                    B("Log-in/Sign-up Demo"),
                    Div().KoIf(mode == Mode.LogIn).WithChildren(
                        H3("Log in or ", Button("sign up").KoClick(Call(chgMode, Mode.SignUp)))
                    ),
                    Div().KoIf(mode == Mode.SignUp).WithChildren(
                        H3("Sign up or ", Button("log in").KoClick(Call(chgMode, Mode.LogIn)))
                    ),
                    Table(
                        TR(TD("Name"),      TD(Input().KoValue(name))),
                        TR(TD("Password"),  TD(Input().KoValue(password).WithAttr("type", "password"))),
                        TR(TD("E-mail"),    TD(Input().KoValue(email))).KoVisible(mode == Mode.SignUp),
                        TR(TD(""),          TD(Show(errMsg).WithClass("login-demo-error").KoVisible(errMsg != "")))
                    ),
                    Button("Go!").KoClick(go).KoEnabled(!busy),
                    Span("processing...").KoIf(busy)
                )
            );

            return content;
        }

        internal static void WriteWebPage()
        {
            const string path = Const.DemoWebSitePath;
            var content = Content();
            var html = StandardSpannerHtmlPage("Login demo", content);
            var webPage = WebPage("LoginDemo", html, null);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}          
Log-in Demo web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Login demo    </title>
  </head>
  <body>
    <div class='login-demo'>
      <div style='border: 4px solid black; margin: 1em; padding: 1em; width: 16em'>
        <b>
Log-in/Sign-up Demo        </b>
        <div data-bind='if: (mode() === "LogIn")'>
          <h3>
Log in or             <button data-bind='click: (function () {
chgMode("SignUp");            })'>
sign up            </button>
          </h3>
        </div>
        <div data-bind='if: (mode() === "SignUp")'>
          <h3>
Sign up or             <button data-bind='click: (function () {
chgMode("LogIn");            })'>
log in            </button>
          </h3>
        </div>
        <table>
          <tr>
<td>Name</td><td><input data-bind='value: name'></td>          </tr>
          <tr>
<td>Password</td><td><input type='password' data-bind='value: password'></td>          </tr>
          <tr data-bind='visible: (mode() === "SignUp")'>
<td>E-mail</td><td><input data-bind='value: email'></td>          </tr>
          <tr>
<td></td><td><span data-bind='visible: (errMsg() !== ""), text: errMsg()' class='login-demo-error'></span></td>          </tr>
        </table>
        <button data-bind='enable: !busy(), click: go'>
Go!        </button>
<span data-bind='if: busy()'>processing...</span>      </div>
    </div>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function LoginDemo() {
        var self = this;
        self.mode = ko.observable("LogIn");
        self.errMsg = ko.observable("");
        self.chgMode = (function (x) {
          self.mode(x);
          self.errMsg("");
        });
        self.name = ko.observable("");
        self.password = ko.observable("");
        self.email = ko.observable("");
        self.busy = ko.observable(false);
        self.validEmailRE = new RegExp("\\w\\w\\w@\\w\\w\\w");
        self.fakeRPC = (function () {
          self.busy(false);
          alert("Welcome!");
        });
        self.go = (function () {
          if ((self.name() === "")) {
            self.errMsg("Please provide a log-in name.");
          } else if ((self.password() === "")) {
            self.errMsg("Please provide a valid password.");
          } else if (((self.mode() === "SignUp") && !self.validEmailRE.test(self.email()))) {
            self.errMsg("Please provide a valid e-mail address.");
          } else if (true) {
            self.busy(true);
            setTimeout(self.fakeRPC, 1000);
          }
        });
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new LoginDemo()); });
    </script>
  </body>
</html>          

A Non-trivial Example: the Memory Game

Here is something slightly more exciting...

click

Observe again the close correspondence between the Spanner implementation and the generated HTML and JavaScript.

Memory Game source code in Spanner.
using System;
using System.Collections.Generic;
using System.Linq;
using Spanner;

namespace SpannerWebSite
{
    [SpannerPoco]
    public class Tile
    {
        public int Index;
        public ObsVar<string> Letter;
        public string Colour;
        public ObsVar<bool> Revealed;
        public bool Paired;
    }

    internal partial class MemoryGame : UI
    {
        internal class BodyHtmlAndInitAct {
            internal Html BodyHtml;
            internal Act InitAct;
        }

        internal static BodyHtmlAndInitAct Content()
        {
            const string word = "*SPANNER";

            const string wordword = word + word;

            var numTiles = wordword.Length;

            var sideLength = (int)Math.Sqrt(numTiles);

            var letters = Var<string[]>("letters", wordword.Select(x => x.ToString()).ToArray());

            var colours = Var<string[]>("colours", new string[] { "red", "green", "blue" });

            var numColours = Length(colours);

            var newTile = Function<string, int, Tile>("newTile", (letter, i) =>
                Let<Tile, Tile>(new Tile(), tile =>
                    ActExpr(
                        Do(
                            Set(tile.Index(), i),
                            Set(tile.Letter(), NewObsVar(letter)),
                            Set(tile.Colour(), Ith(colours, i % numColours)),
                            Set(tile.Revealed(), NewObsVar<bool>(true)),
                            Set(tile.Paired(), false)
                        ),
                        tile
                    )
                )
            );

            var tiles = Var<Tile[]>("tiles", Map(letters, newTile));

            var numClicks = ObsVar<int>("numClicks", 0);

            var newGame = Procedure("newGame", Do(
                ForEach(tiles, Proc<Tile, int>((tile, i) =>
                    Local(Floor(Random() * ToDouble(i + 1)), j =>
                    Local(Ith(tiles, i).Letter(), tmp =>
                        Do(
                            Set(Ith(tiles, i).Revealed(), false),
                            Set(Ith(tiles, i).Paired(), false),
                            Set(Ith(tiles, i).Letter(), Ith(tiles, j).Letter()),
                            Set(Ith(tiles, j).Letter(), tmp)
                        )
                    ))
                )),
                Set(numClicks, 0)
            ));

            var fst = Var<Tile>("fst", null);

            var snd = Var<Tile>("snd", null);

            var tileClicked = Procedure<Tile>("tileClicked", tile =>
                IfCase(
                    When(tile.Revealed() | tile.Paired(),
                        Noop()
                    ),
                    When(fst == null, Do(
                        Set(fst, tile),
                        Set(tile.Revealed(), true),
                        Set(numClicks, numClicks + 1)
                    )),
                    When(snd == null & tile.Letter() == fst.Letter(), Do(
                        Set(tile.Paired(), true),
                        Set(fst.Paired(), true),
                        Set(fst, null),
                        Set(tile.Revealed(), true),
                        Set(numClicks, numClicks + 1)
                    )),
                    When(snd == null, Do(
                        Set(snd, tile),
                        Set(tile.Revealed(), true),
                        Set(numClicks, numClicks + 1)
                    )),
                    When(snd != null, Do(
                        Set(fst.Revealed(), false),
                        Set(snd.Revealed(), false),
                        Set(fst, tile),
                        Set(snd, null),
                        Set(tile.Revealed(), true),
                        Set(numClicks, numClicks + 1)
                    ))
                )
            );

            var content = Group(
                Style(@"
                    div.tile {
                        width: 2em;
                        height: 1.2em;
                        float: left;
                        border: 2px solid green;
                        border-radius: 6px;
                        margin: 2px;
                        text-align: center;
                        color: white;
                        font: bold 32px/1.2 times;
                    }"),
                KoForEach(tiles, tile =>
                    Div(Show(tile.Letter()).KoVisible(tile.Revealed()))
                        .WithClass("tile")
                        .KoStyle(
                            CssStyleValue("background", tile.Colour()),
                            CssStyleValue("clear", Cond<string>(tile.Index() % sideLength == 0, "both", "none"))
                         )
                        .KoClick(Call(tileClicked, tile))
                ),
                Div(Show(numClicks), " click", Show(Cond<string>(numClicks == 1, "", "s")))
                    .WithAttr("style", "clear: both"),
                Button("New game").KoClick(newGame)
            );

            return new BodyHtmlAndInitAct { BodyHtml = content, InitAct = Call(newGame) };
        }

        internal static void WriteWebPage()
        {
            const string path = Const.DemoWebSitePath;
            var content = Content();
            var html = StandardSpannerHtmlPage("Memory game", content.BodyHtml);
            var webPage = WebPage("MemoryGame", html, content.InitAct);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}          
Memory Game web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Memory game    </title>
  </head>
  <body>
    <style>

                    div.tile {
                        width: 2em;
                        height: 1.2em;
                        float: left;
                        border: 2px solid green;
                        border-radius: 6px;
                        margin: 2px;
                        text-align: center;
                        color: white;
                        font: bold 32px/1.2 times;
                    }    </style>
    <!-- ko foreach: tiles -->
      <div data-bind='click: (function () {
$root.tileClicked($data);      }), style: {background: $data.Colour, clear: ((($data.Index % 4) === 0) ? "both" : "none")}' class='tile'>
<span data-bind='visible: $data.Revealed(), text: $data.Letter()'></span>      </div>
    <!-- /ko -->
    <div style='clear: both'>
<span data-bind='text: numClicks()'></span> click<span data-bind='text: ((numClicks() === 1) ? "" : "s")'></span>    </div>
    <button data-bind='click: newGame'>
New game    </button>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function MemoryGame() {
        var self = this;
        self.letters = ["*", "S", "P", "A", "N", "N", "E", "R", "*", "S", "P", "A", "N", "N", "E", "R"];
        self.colours = ["red", "green", "blue"];
        self.newTile = (function (letter, i) {
          return (function() {
            {
              var tile = {Index:0, Letter:null, Colour:null, Revealed:null, Paired:false};
              tile.Index = i;
              tile.Letter = ko.observable(letter);
              tile.Colour = self.colours[(i % self.colours.length)];
              tile.Revealed = ko.observable(true);
              tile.Paired = false;
            }
            return tile;
          })();
        });
        self.tiles = self.letters.map(self.newTile);
        self.fst = null;
        self.numClicks = spanner.obsInt(0);
        self.snd = null;
        self.tileClicked = (function (tile) {
          if ((tile.Revealed() || tile.Paired)) {
            {} /* No op. */;
          } else if ((self.fst === null)) {
            self.fst = tile;
            tile.Revealed(true);
            self.numClicks((self.numClicks() + 1));
          } else if (((self.snd === null) && (tile.Letter() === self.fst.Letter()))) {
            tile.Paired = true;
            self.fst.Paired = true;
            self.fst = null;
            tile.Revealed(true);
            self.numClicks((self.numClicks() + 1));
          } else if ((self.snd === null)) {
            self.snd = tile;
            tile.Revealed(true);
            self.numClicks((self.numClicks() + 1));
          } else if ((self.snd !== null)) {
            self.fst.Revealed(false);
            self.snd.Revealed(false);
            self.fst = tile;
            self.snd = null;
            tile.Revealed(true);
            self.numClicks((self.numClicks() + 1));
          }
        });
        self.newGame = (function () {
          self.tiles.forEach((function (tile, i) {
            var j = Math.floor((Math.random() * (i + 1)));
            var tmp = self.tiles[i].Letter();
            self.tiles[i].Revealed(false);
            self.tiles[i].Paired = false;
            self.tiles[i].Letter(self.tiles[j].Letter());
            self.tiles[j].Letter(tmp);
          }));
          self.numClicks(0);
        });
        {
          self.newGame();
        }
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new MemoryGame()); });
    </script>
  </body>
</html>          

Overview

Spanner is a C# solution to the tedious problems faced by single-page web application developers. The design goal was to provide programmers with a highly protective, single, expressive, lightweight, statically typed coding environment allowing them to exploit their existing knowledge of HTML, JavaScript, and Knockout.

What is hard about conventional web app development?

Standard web technologies, namely HTML, CSS, and JavaScript, have evolved in a piecemeal fashion largely untouched by any knowledge of computer science. The result is an unfortunate polyglot which places demands on the programmer which much of mainstream software development left behind a long time ago:

  • 'Stringly typed' programming. A web application integrates code written in HTML, CSS, and JavaScript. Identifiers are shared across the three languages purely as embedded strings. Current tools provide at best rudimentary protection against typos. Protection against inappropriate use, such as type errors, and support for basic refactoring is essentially non-existent.
  • JavaScript is effectively untyped. Yes, JavaScript has a type system of sorts and, yes, TypeScript goes some way to remedying that problem, but in almost all circumstances the programmer only finds out about type errors when the application misbehaves or crashes (with TypeScript these problems still arise due to the any type and the unchecked embedding of names as strings in HTML and CSS). In modern languages, such as C#, such errors are flagged as you type by the development environment.
  • Poor abstraction support. In a mainstream language we just define functions to abstract over common operations. There is no single, simple, equivalent abstraction mechanism for HTML or CSS without resorting to mutually ignorant tools such as razor and less and no mechanisms which integrate across all three web languages.
  • Debugging is tedious. Trivial errors go undetected before runtime. A great deal of a web developer's time is spent tracking down trivial bugs which would have been caught automatically in any other modern programming domain.

Why Spanner?

Spanner addresses these problems by providing a lightweight, embedded domain specific language in C#. Every HTML element, Knockout binding, and JavaScript type and control-flow statement has a corresponding Spanner function with the same name and meaning. Spanner understands ordinary C# POCOs which allows for seamless integration with server-side APIs. Spanner also integrates well with third party JavaScript libraries. Because Spanner does not force a policy on the developer (it is not a 'framework' in that sense), you are free to develop web applications in whatever style suits your needs. Because a Spanner solution is a C# program, the developer has the full benefit of working in a single, modern, integrated, statically typed language. Finally, HTML and JavaScript generated by Spanner is both easy to read and debug and maps directly on to the original C# code, right down to the variable names.

Download

You can download Spanner from spanner.codeplex.com . It is released under a variant of the Microsoft Public Licence, which means you are free to use this code without restriction, in commercial software or otherwise, provided only you retain the original licence for Spanner.

Introduction and Tutorial

Opening remarks

Spanner is designed to help build single-page web applications based on HTML, JavaScript, and the Knockout MVVM library, without all the usual web development pain. I have tried to avoid getting into fine detail too quickly here, so I encourage the reader to gloss over any code fragments which are not immediately explained. I hope I have achieved the key design goal that Spanner be largely self-explanatory. Having said that, please note that this tutorial assumes a working knowledge of HTML, CSS, and JavaScript, and at least some idea of Knockout's observable variables.

The Spanner 'Workflow'

Web development using Spanner proceeds as follows: the developer writes a C# program using the Spanner library. When this program is compiled and run, it generates a complete web site - HTML, JavaScript, CSS - which can be served as static web pages. All interaction is mediated on the client via JavaScript and Knockout.

Why use Spanner?

  • Because your web site is described as a C# program, you have the advantage of using a single, statically type checked language for every aspect of web application development.
  • Using a single, standard, language means you can use standard abstraction mechanisms for building components of your architecture.
  • In particular, most errors are flagged by Visual Studio as you type, exactly as with C# development for other kinds of application.
  • Spanner uses HTML, CSS, and JavaScript concepts in a one-to-one fashion, right down to element, function, and type names.
  • You can have increased confidence in the resulting web application because it, too, has been statically type checked.
  • Spanner integrates painlessly with any third party JavaScript which can be given a well typed API.
  • Spanner imposes minimal policy on your design: you are largely free to arrange your architecture as you see fit.
  • The generated JavaScript is highly regular and is easy to debug on the client.

Hello, World!

In traditional style, we begin with Hello, World! Here is the complete C# listing:

Hello, World! in Spanner in C#.
using Spanner;

namespace SpannerWebSite
{
    internal partial class HelloWorld: UI
    {
        internal static Html Content()
        {
            return P("Hello, World!");
        }
        internal static void WriteWebPage() {
            const string path = Const.DemoWebSitePath;
            var html = StandardSpannerHtmlPage("Hello, World!", Content());
            var initAct = (Act) null;
            var webPage = WebPage("HelloWorld", html, initAct);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}          

After compiling and running this program, the directory E:\Tmp\Spanner\Demo will have been created, if it didn't already exist, with subdirectories html, js, css, and media. The html subdirectory will contain HelloWorld.html, the start page of our web 'application'.

Examining the .html file, we find this:

Hello, World! web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Hello, World!    </title>
  </head>
  <body>
    <p>
Hello, World!    </p>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function HelloWorld() {
        var self = this;
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new HelloWorld()); });
    </script>
  </body>
</html>          

The HTML should be immediately familiar and the reader should be able to get the gist of the JavaScript component; Spanner always generates some JavaScript; this is just what you get for a web 'application' with no logic.

The end result, of course, is just:

Hello, World!

Hello, World! - the long way

Above we used the function StandardSpannerHtmlPage to provide the usual HTML boilerplate. Here we see how we can achieve the same end without taking short-cuts:

Hello, World! - the long way.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spanner;

namespace SpannerWebSite
{
    internal partial class HelloWorldLong : UI
    {
        internal static Html Content()
        {
            return P("Hello, World! (The long way.)");
        }
        internal static void WriteWebPage()
        {
            const string path = Const.DemoWebSitePath;
            var html = Doctype(
                Html(
                    Head(
                        Title("Hello, World! (The long way.)")
                    ),
                    Body(
                        Content(),
                        ViewModelJavaScript()
                    )
                )
            );
            var initAct = (Act)null;
            var webPage = WebPage("HelloWorldLong", html, initAct);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
        // END
    }
}          

and here we see that the HTML generated from this Spanner program is identical to that produced previously (the ViewModelJavaScript() expression above is the Spanner magic for 'include all the relevant JavaScript here'):

Hello, World! (the long way) - web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Hello, World! (The long way.)    </title>
  </head>
  <body>
    <p>
Hello, World! (The long way.)    </p>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function HelloWorldLong() {
        var self = this;
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new HelloWorldLong()); });
    </script>
  </body>
</html>          

Once again, the resulting web page is just this:

Hello, World! (The long way.)

Now, having done that the long way, in what follows we'll just use StandardSpannerHtmlPage.

HTML in Spanner

The long version of Hello, World! is a good illustration of how Spanner represents and generates HTML. Let's look at the source code in more detail.

First, observe that the class containing our code inherits from UI the root Spanner class: class Intro: UI. This is a useful idiom which means we don't have to explicitly prefix every Spanner function use with UI(e.g., UI.P(...), UI.Html(...) etc.).

Second, observe how the HTML for our web page is described in Spanner:

internal static Html Content()
{
    return P("Hello, World! (The long way.)");
}
internal static void WriteWebPage()
{
    const string path = Const.DemoWebSitePath;
    var html = Doctype(
        Html(
            Head(
                Title("Hello, World! (The long way.)")
            ),
            Body(
                Content(),
                ViewModelJavaScript()
            )
        )
    );
    var initAct = (Act)null;
    var webPage = WebPage("HelloWorldLong", html, initAct);
    var ctxt = new Context(path);
    ctxt.WriteWebPage(webPage);
}          

and compare that with the generated HTML. We immediately see some obvious correspondences:

Indeed, every HTML5 element is represented directly in Spanner in this fashion: A; Abbr; Address; Area; Article; Aside; Audio; B; Base; Bdi; Bdo; BlockQuote; Body; BR; Button; Canvas; Caption; CDATA; Cite; Code; Col; ColGroup; Command; Data; DataGrid; DataList; DD; Del; Details; Doctype; Dfn; Div; DL; DT; Em; Embed; EventSource; FieldSet; FigCaption; Figure; Footer; Form; H1; H2; H3; H4; H5; H6; Head; Header; HGroup; HR; Html; I; IFrame; Img; Input; Ins; Kbd; KeyGen; Label; Legend; LI; Link; Mark; ImageMap; Menu; Meta; Meter; Nav; Noscript; Object; OL; OptGroup; Option; Output; P; Param; Pre; Progress; Q; Ruby; RP; RT; S; Samp; Script; Section; Select; Small; Source; Span; Strong; Style; Sub; Summary; Sup; Table; Tbody; TD; TextArea; TFoot; TH; THead; Time; Title; TR; Track; U; UL; VAR; Video; and Wbr.

Each of these functions returns an immutable value of type Html. Each can also take any number of Html values as children, with the following exceptions: Area, Base, BR, Col, Command, Embed, HR, Img, KeyGen, Meta, Param, Source, Track, and Wbr, which accept no arguments; CDATA, which accepts any number of verbatim strings; and Comment, which accepts a single string and generates an HTML <!-- comment -->. Note that ordinary strings are implicitly cast to Html where required.

Knockout, JavaScript, and Spanner

Your Name in Lights

Next, let's look at a simple interactive web application written using Spanner. In this example, the user is asked to enter their name, whereupon they will see it writ large, in capitals, surrounded by 'lights':

Please tell me your name:

Your name in lights:

Here is the significant code for YourNameInLights.cs:

Your Name in Lights in Spanner.
internal static Html Content()
{
    var name = ObsVar<string>("name", "");
    var bodyHtml = Group(
        P("Please tell me your name: ", Input().KoValue(name)),
        Div().KoVisible(name != "").WithChildren(
            P("Your name in lights:").WithAttr("style", "margin-bottom: 40px"),
            Show(ToUpperCase(name))
                .WithAttr("style", "border: 10px dotted blue")
                .WithAttr("style", "font-size: 40px")
                .WithAttr("style", "margin-left: 40px")
        )
    );
    return bodyHtml;
}            

Examining this piece by piece:

  • var name = ObsVar<string>("name", "") defines a new string valued observable variable (a Knockout observable) initialised to the empty string. The first argument to ObsVar is optional and specifies the name this variable should have in the generated JavaScript. (Spanner will add unique suffixes to names where required to ensure uniqueness.) Observables are the core idea of the MVVM pattern, where any change to an observable automatically triggers updates on any dependent computed observables and HTML properties - no bookkeeping is required on the part of the developer.
  • Group(E1, E2, E3, ...) is a useful pseudo-element representing the 'unenclosed' sequence of elements E1, E2, E3, ... In the generated HTML, these elements will appear in that sequence, with no enclosing parent element. (We could use a plain Div, but that is redundant since these elements will already have a parent in the Body element.)
  • P("Please tell me your name: ", ...) denotes an HTML <p>...</p> element with two children, the first being a plain text string. (Spanner knows which elements have closing tags and which don't; you have no need to remember which.)
  • Input().KoName(name) denotes the following HTML: <input data-bind='value: name'>. The Knockout data-bind attribute applied in this case via the call KoName(name).
  • Div().KoVisible(name != "").WithChildren(...) denotes the following HTML: <div data-bind='visible: name() != ""'>...</div>. Observe that Spanner knows that the observable name should not be dereferenced when used in the value binding above, but should be dereferenced - name() - when used in an expression, as in the visible binding, here. The idiom Div().[attrs].WithChildren(...) is equivalent to writing Div(...).[attrs], but it often reads better when attributes are specified prior to the children of an element.
  • Show(ToUpperCase(name)).WithAttr("style", ...).WithAttr("style", ...).WithAttr("style", ...) denotes the HTML <span data-bind='text: name().toUpperCase()' style='{..., ..., ...}'></span>. The function Show produces a <span> element with a Knockout text binding showing the value of its string or number valued argument. (Building libraries of utility combinators like Show in Spanner is trivial and natural.) The other point illustrated here is that Spanner understands that style attributes should be grouped together in { braces }.

(Observe that Spanner uses an applicative(functional) style, ToUpperCase(name), rather than an object.method style, name.toUpperCase() - this is a consequence following from implementing a domain specific language in C#.)

Here is the HTML generated by the above Spanner program:

Your Name in Lights web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Your name in lights    </title>
  </head>
  <body>
    <p>
Please tell me your name: <input data-bind='value: name'>    </p>
    <div data-bind='visible: (name() !== "")'>
      <p style='margin-bottom: 40px'>
Your name in lights:      </p>
<span style='margin-left: 40px; font-size: 40px; border: 10px dotted blue' data-bind='text: name().toUpperCase()'></span>    </div>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function YourNameInLights() {
        var self = this;
        self.name = ko.observable("");
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new YourNameInLights()); });
    </script>
  </body>
</html>            

Some final points of explanation, here:

  • Spanner automatically imports Knockout. The particular version and URL can be overridden by the programmer.
  • The Spanner runtime is always included. This is a handful of straightforward definitions on which the translation into JavaScript depends.
  • The view model, YourNameInLights, is defined via a function encapsulating all variables and functions reachable from the HTML and the initialisation action (which is absent in this case). Spanner automatically orders variable definitions to satisfy dependencies.
  • spanner.windowOnLoad, from the Spanner runtime, appends a function to a list of functions to be run on the onload event, namely when all the resources on which the page depends have been loaded by the client. In this case the function calls Knockout's ko.applyBindings method, which is how the application is started.

Your Name in Flashing Lights

Now we'll improve the Your Name in Lights application with a little animation:

Please tell me your name:

Your name in flashing lights:

Here is the significant code for YourNameInFlashingLights.cs:

Your Name in Flashing Lights in Spanner.
using Spanner;

namespace SpannerWebSite
{
    internal partial class YourNameInFlashingLights : UI
    {
        internal class BodyHtmlAndInitAct {
            internal Html BodyHtml;
            internal Act InitAct;
        }
        internal static BodyHtmlAndInitAct Content()
        {
            var name = ObsVar<string>("name", "");
            var counter = ObsVar<int>("counter", 0);
            var initAct = Do(SetInterval(Proc(Set(counter, counter + 1)), 750));
            var bodyHtml = Group(
                Style(@"
                    .yellow-lights  { border: 10px dotted yellow; }
                    .red-lights     { border: 10px dotted red; }
                    .blue-lights    { border: 10px dotted blue; }
                    .green-lights   { border: 10px dotted green; }
                "),
                P("Please tell me your name: ", Input().KoValue(name)),
                Div().KoVisible(name != "").WithChildren(
                    P("Your name in flashing lights:").WithAttr("style", "margin-bottom: 40px"),
                    Show(ToUpperCase(name))
                        .WithAttr("style", "font-size: 80px")
                        .WithAttr("style", "margin-left: 40px")
                        .KoCss(CssClassCondition("yellow-lights", counter % 4 == 0),
                               CssClassCondition("red-lights",    counter % 4 == 1),
                               CssClassCondition("green-lights",  counter % 4 == 2),
                               CssClassCondition("blue-lights",   counter % 4 == 3))
                )
            );
            return new BodyHtmlAndInitAct { BodyHtml = bodyHtml, InitAct = initAct };
        }
        internal static void WriteWebPage()
        {
            const string path = Const.DemoWebSitePath;
            var content = Content();
            var html = StandardSpannerHtmlPage("Your name in flashing lights", content.BodyHtml);
            var webPage = WebPage("YourNameInFlashingLights", html, content.InitAct);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}            

Let's examine the new features:

  • class BodyHtmlAndInitAct is just a convenience type pairing some HTML with a corresponding initialisation act. Spanner uses the term act (or action) to denote operations carried out for their side-effects (as opposed to functions which are used to compute values).
  • var counter = ObsVar<int>("counter", 0) defines an integer counter observable variable used to control the flashing lights effect.
  • var initAct = Do(SetInterval(Proc(Set(counter, counter + 1)), 750)) defines the initialisation act for this view model. In this case the action is equivalent to the JavaScript setInterval(function () { counter = counter + 1; }, 750). The following points expand on this line of code.
  • Set(counter, counter + 1) is obviously the action of incrementing the variable counter. Set is used for all assignment in Spanner. Observe that no special syntax is required merely because counter is a Knockout observable (in Knockout this must be written counter(counter() + 1); Spanner syntax is marginally more intuitive in this regard).
  • Proc(Set(counter, counter + 1)) defines an inline procedure, that is a function whose only purpose is its side effect, equivalent to the JavaScript function () { counter(counter() + 1); } in this case.
  • Do(SetInterval(..., ...)) is equivalent to the JavaScript setInterval(..., ...). The Do act is used to evaluate a function purely for its side-effect - in this case SetInterval returns an ID, which we don't need here, which can be used to cancel the deferred computation. Now, returning to the main body of the code...
  • KoCss(CssClassCondition("yellow-lights", counter % 4 == 0), ...) denotes the Knockout css binding taking a list of static CSS classes, each accompanied by a condition dictating when the class should be applied to the HTML element.
  • Finally, because this example requires some initialisation, we must pass initAct (the initialisation action) to the WebPage function. This ensures the initialisation action will be carried out after all the variables have been created, but before the Knockout applyBindings call is made.

And here is the HTML generated by the Spanner program:

Your Name in Flashing Lights web page generated bySpanner.
<!DOCTYPE html>
<html>
  <head>
    <title>
Your name in flashing lights    </title>
  </head>
  <body>
    <style>

                    .yellow-lights  { border: 10px dotted yellow; }
                    .red-lights     { border: 10px dotted red; }
                    .blue-lights    { border: 10px dotted blue; }
                    .green-lights   { border: 10px dotted green; }
                    </style>
    <p>
Please tell me your name: <input data-bind='value: name'>    </p>
    <div data-bind='visible: (name() !== "")'>
      <p style='margin-bottom: 40px'>
Your name in flashing lights:      </p>
<span data-bind='css: {"yellow-lights": ((counter() % 4) === 0), "red-lights": ((counter() % 4) === 1), "green-lights": ((counter() % 4) === 2), "blue-lights": ((counter() % 4) === 3)}, text: name().toUpperCase()' style='margin-left: 40px; font-size: 80px'></span>    </div>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      function YourNameInFlashingLights() {
        var self = this;
        self.name = ko.observable("");
        self.counter = spanner.obsInt(0);
        {
          setInterval((function () {
            self.counter((self.counter() + 1));
          }), 750);
        }
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new YourNameInFlashingLights()); });
    </script>
  </body>
</html>            

And there's more...

The above provides the flavour of web application development using Spanner. The reference section explains things in detail, including how to use arrays, computed observables, ordinary JavaScript variables and functions, templates, libraries, interfacing with POCOs, interfacing with third-party JavaScript code, and so forth.

Reference - Top Level

Overview

Hello, World! in Spanner in C#.
using Spanner;

namespace SpannerWebSite
{
    internal partial class HelloWorld: UI
    {
        internal static Html Content()
        {
            return P("Hello, World!");
        }
        internal static void WriteWebPage() {
            const string path = Const.DemoWebSitePath;
            var html = StandardSpannerHtmlPage("Hello, World!", Content());
            var initAct = (Act) null;
            var webPage = WebPage("HelloWorld", html, initAct);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}          

Running this program will create (if they don't already exist) sub-directories html, css, js, and media in E:\Tmp\Spanner\Demo and populate them. By default, WriteWebPage will generate a single-file solution, in this case putting all HTML, JavaScript, and so forth in html/HelloWorld.html.

The UI Class

The UI class is where Spanner exposes the bulk of its API. By making your own top-level classes sub-classes of UI you can avoid all the tedious explicit qualification of Spanner API methods.

The Html Class

The Html class represents HTML elements. Every HTML element has a correspondingly named Spanner constructor function. For example, Div(HR(), P("Hi!"), HR()) denotes the HTML <div><hr><p>Hi!</p><hr></div>.

The Attrs Class

The Attrs class represents HTML attributes. Attributes are added to HTML elements thus: Span("Hi!").WithAttr("id", "xyz").WithAttr("title", "Greetings.") denotes the HTML <span id='xyz' title='Greetings.'>Hi!</span>. Various convenience functions are provided such as WithID and WithClass. The Attrs support is also aware of structured attributes such as class and style - for example, Span("Hi!").WithAttr("class", "foo").WithAttr("class", "bar") represents the HTML <span class="foo bar">Hi!</span>.

The Expr Class

The Expr<T> class represents JavaScript values of type T, including JavaScript functions. You can use most of the standard C# operators where appropriate, with a few exceptions (notably &&, ||, and ? : for which Spanner provides alternative syntax). Note that Spanner expressions largely hide the distinction between ordinary JavaScript variables and Knockout observables.
Handwritten C#
var x = Var<int>("x", 1);
var y = ObsVar<int>("y", 2);
var z = ComputedVar<int>("z", x + y);          
Generated JavaScript
function ExprDemo() {
  var self = this;
  self.x = 1;
  self.y = spanner.obsInt(2);
  self.z = ko.computed(function () {
    return (self.x + self.y());
  });
};          

Variables: Var, ObsVar, ComputedVar, and Param

Variables can be ordinary JavaScript variables, Knockout observables, Knockout 'computeds', or function parameters. Variables all share the same syntax and, except where a Knockout variable is required, can be used interchangeably. The one syntactic fly in the ointment is the need to use Set(x, y) to assign variable x rather than something more perspicuous, a problem for which we can blame C#'s quirky operator overloading rules.

The Act Class

The Act class represents JavaScript statements. Virtually all JavaScript statements are represented, although typically with slightly different syntax.
Handwritten C#
var x = Var<int>("x", 1);
var y = ObsVar<int>("y", 2);
InitAct =
    IfThen(x < 3, Do(
        Set(x, 3),
        Set(y, y + 1)
    ));          
Generated JavaScript
function ActDemo() {
  var self = this;
  self.x = 1;
  self.y = spanner.obsInt(2);
  {
    if ((self.x < 3)) {
      self.x = 3;
      self.y((self.y() + 1));
    }
  }
};          

Functions and Procedures

Being strongly typed, Spanner draws a distinction between functions, which are evaluated to obtain a value, and procedures, which are evaluated purely for a side effect. Impure functions (i.e., with side effects) are supported via ActExpr. Good functional programmers will eschew such a practice!
Handwritten C#
var x = Var<int>("x", 123);
var isEven = Function<int, bool>("isEven", a => a % 2 == 0);
var incX = Procedure<int>("incX", y => Set(x, x + y));
var xs = Var<int[]>("xs", Array(1, 2, 3));
var ys = Var<int[]>("ys", Array<int>());
InitAct = Do(
    Call(incX, 1),
    Set(ys, Map(Filter(xs, isEven), Fn<int, int>(b => b + 1)))
);          
Generated JavaScript
function FnProcDemo() {
  var self = this;
  self.x = 123;
  self.incX = (function (y) {
    self.x = (self.x + y);
  });
  self.ys = [];
  self.xs = [1, 2, 3];
  self.isEven = (function (a) {
    return ((a % 2) === 0);
  });
  {
    self.incX(1);
    self.ys = self.xs.filter(self.isEven).map((function (b) {
      return (b + 1);
    }));
  }
};          

TemplateModels and LibraryModels

Re-usable views and view models are supported via TemplateModel while shared code libraries are supported via LibraryModel.

The following example demonstrates the top-level document using a list of items template which, in turn, uses a single item template:

Handwritten C#
// Templates would typically be defined in separate C# classes;
// here we define them in-line for the sake of brevity.

// The template and view model for an "item":
var item = Var<string>("item", "");
var itemTemplate = TemplateModel<string>(
    "itemTemplate",
    // The HTML for the template.
    P().WithAttr("color", "green").KoText(item),
    // The initialisation procedure for the template.
    x => Set(item, x)
    // No dependencies.
);

// The template and view model for a list of "items"
// using the "item" template:
var itemInsts = Var<TemplateModel[]>("itemInsts", null);
var itemsTemplate = TemplateModel<string[]>(
    "itemsTemplate",
    // The HTML for the template.
    Div().KoForEach(itemInsts, x => Div().KoTemplate(x)),
    // The initialisation procedure for the template.
    xs => Set(itemInsts, 
        Map(xs, Fn<string, TemplateModel>(x => NewTemplateModelInstance(itemTemplate, x)))
    ),
    itemTemplate // Dependency (could be many such).
);

// The top-level view using the "items" template.
var names = Array<string>("Alice", "Bob", "Cissy");
var itemsInst = Var<TemplateModel>("itemsInst",
    NewTemplateModelInstance(itemsTemplate, names));
Content = Div().KoTemplate(itemsInst);
Dependencies = new Model[] { itemsTemplate }; // Top-level dependencies.          
Generated HTML
<div data-bind='template: spanner.templateArgs(itemsInst)'>
</div>
<script id='itemTemplate__template__' type='text/html'>
  <p data-bind='text: item' color='green'>
  </p>
</script>
<script id='itemsTemplate__template__' type='text/html'>
  <div data-bind='foreach: itemInsts'>
      <div data-bind='template: spanner.templateArgs($data)'>
      </div>
  </div>
</script>          
Generated JavaScript
function itemTemplate($xyz) {
  var self = this;
  self.item = "";
  self.__templateID = "itemTemplate__template__";
  {
    self.item = $xyz;
  }
};
function itemsTemplate($xyz) {
  var self = this;
  self.itemInsts = null;
  self.__templateID = "itemsTemplate__template__";
  {
    self.itemInsts = $xyz.map((function (x) {
      return new itemTemplate(x);
    }));
  }
};
function TemplateDemo() {
  var self = this;
  self.itemsInst = new itemsTemplate(["Alice", "Bob", "Cissy"]);
};          

[Try it.]

[Note that each TemplateModel definition, as well as the top-level page, has to include the list of template models on which it depends. Spanner cannot infer these dependencies because template model instances might be passed in from anywhere.]

The next example shows how library models are defined and used in Spanner

Handwritten C#
using Spanner;

namespace SpannerWebSite
{
    internal partial class LibraryModelDemo : UI
    {
        // A simple global counter.
        public class GlobalCounterModel : LibraryModel {
            // The API for the Counter global model.
            public Var<int> N;
            public Var<Fn<Act>> Inc;
            // The constructor provides the implementation.
            public GlobalCounterModel(): base("GlobalCounter", null)
            {
                N = Var<int>("N", 0);
                Inc = Procedure("Inc", Set(N, N + 1));
            }
        }

        internal static GlobalCounterModel GlobalCounter = new GlobalCounterModel();

        internal static void WriteWebPage()
        {
            var xs = Var<int[]>("xs", Array<int>());

            // Use the Counter global model in our initialisation action.
            var InitAct = Do(
                Call(GlobalCounter.Inc),
                Push(xs, GlobalCounter.N),
                Call(GlobalCounter.Inc),
                Push(xs, GlobalCounter.N),
                Call(GlobalCounter.Inc),
                Push(xs, GlobalCounter.N)
            );

            var Content = Div().KoForEach(xs, x => P().KoText(x));

            var Dependencies = new Model[] { GlobalCounter };

            const string path = Const.DemoWebSitePath;
            var html = StandardSpannerHtmlPage("Library Model Demo", Content);
            var webPage = WebPage("LibraryModelDemo", html, InitAct, Dependencies);
            var ctxt = new Context(path);
            ctxt.WriteWebPage(webPage);
        }
    }
}          
Generated web page
<!DOCTYPE html>
<html>
  <head>
    <title>
Library Model Demo    </title>
  </head>
  <body>
    <div data-bind='foreach: xs'>
        <p data-bind='text: $data'>
        </p>
    </div>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      var GlobalCounter = new (function GlobalCounter() {
        var self = this;
        self.N = 0;
        self.Inc = (function () {
          self.N = (self.N + 1);
        });
      })();
      function LibraryModelDemo() {
        var self = this;
        self.xs = [];
        {
          GlobalCounter.Inc();
          self.xs.push(GlobalCounter.N);
          GlobalCounter.Inc();
          self.xs.push(GlobalCounter.N);
          GlobalCounter.Inc();
          self.xs.push(GlobalCounter.N);
        }
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new LibraryModelDemo()); });
    </script>
  </body>
</html>          

[Try it.]

Note that Spanner automatically orders dependencies so they are included in the correct order.

Third party code

Spanner integrates nicely with third-party JavaScript libraries. Here is an example showing how one might use jQuery (a real jQuery binding would obviously be much larger, but this serves to get the idea across).

Handwritten C#
using System;
using Spanner;

namespace SpannerWebSite
{
    internal class ThirdPartyDemo : MiniWebApp
    {
        // A very simple wrapper around some jQuery functionality.
        // A real wrapper would be much bigger!
        public static ExternalNonSpannerLibraryModel JQCore =
            new ExternalNonSpannerLibraryModel("JQCore", @"http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.min.js");

        public class JQModel : LibraryModel
        {
            // The API for the Counter global model.
            public class Elts { }
            public Expr<Elts> Find(Expr<string> query) { return JSExpr<Elts>("$(#)", query); }
            public Expr<Elts> FadeIn(Expr<Elts> elts) { return JSExpr<Elts>("#.fadeIn()", elts); }
            public Expr<Elts> FadeOut(Expr<Elts> elts) { return JSExpr<Elts>("#.fadeOut()", elts); }

            // The constructor provides the implementation.
            public JQModel() : base("JQ", null /* No init. act. */, JQCore) { }
        }

        internal static JQModel JQ = new JQModel();

        internal ThirdPartyDemo()
        {
            Name = "ThirdPartyDemo";

            PageTitle = "Third Party Demo";

            var helloID = HtmlID("Hello");

            var fade = Procedure("fade", Do(JQ.FadeIn(JQ.FadeOut(JQ.Find("#" + helloID.ToString())))));

            Content = Div(
                P("Hello!").WithID(helloID),
                Button("Fade").KoClick(fade)
            );

            Dependencies = new Model[] { JQ };
        }
    }
}          

Observe the use of ExternalNonSpannerLibraryModel to include jQuery by reference to its URL. (There is a similar NonSpannerLibraryModel method for directly specifying the JavaScript code for a third-party library.)

Obserbe also the use of JSExpr to generate in-line JavaScript which integrates neatly with Spanner values. (There is a similar JSAct method for specifying in-line actions -- that is, for code which does not return a value.)

Generated web page
<!DOCTYPE html>
<html>
  <head>
    <title>
Third Party Demo    </title>
  </head>
  <body>
    <div>
      <p id='Hello'>
Hello!      </p>
      <button data-bind='click: fade'>
Fade      </button>
    </div>
    <script src='http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js'>
    </script>
    <script src='http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.min.js'>
    </script>
    <script>
      [Spanner runtime code omitted for brevity.]
      var JQ = new (function JQ() {
        var self = this;
      })();
      function ThirdPartyDemo() {
        var self = this;
        self.fade = (function () {
          $("#Hello").fadeOut().fadeIn();
        });
      };
      spanner.windowOnLoad(function () { ko.applyBindings(new ThirdPartyDemo()); });
    </script>
  </body>
</html>          

[Try it.]

As mentioned, Spanner automatically orders dependencies so they are included in the correct order.

POCOs

Spanner makes it easy to integrate with standard POCOs. By including the T4 template file, SpannerPocos.tt in your Visual Studio solution, extension methods will be generated for all public, top-level classes in your solution with the annotation [SpannerPoco]. For example, the Memory Game from the home page features the following POCO:

POCO from the Memory Game demo.
[SpannerPoco]
public class Tile
{
    public int Index;
    public ObsVar<string> Letter;
    public string Colour;
    public ObsVar<bool> Revealed;
    public bool Paired;
}
          

Normally, for a variable x of type Expr<Tile> we would be forced to access its fields via the cumbersome construction x.Field(o => o.Index), et cetera. Again, this awkwardness is because C# is not well suited to embedded domain specific languages. However, with SpannerPocos.tt in the solution, the extension methods below will be generated, allowing us to access the fields of x via the more agreeable syntax of x.Index(), et cetera. This syntax even works for assignments, such as Set(x.Index(), 42). Not perfect, but much better than the alternative.

Extension methods generated by SpannerPocos.tt
namespace SpannerWebSite
{
public static class TileSpannerPocoExtensions
{
     public static FieldExpr<int> Index(this Expr<Tile> x)
     {
         return x.Field(o => o.Index);
     }
     public static ObsVarField<string> Letter(this Expr<Tile> x)
     {
         return x.ObsVarField(o => o.Letter);
     }
     public static FieldExpr<string> Colour(this Expr<Tile> x)
     {
         return x.Field(o => o.Colour);
     }
     public static ObsVarField<bool> Revealed(this Expr<Tile> x)
     {
         return x.ObsVarField(o => o.Revealed);
     }
     public static FieldExpr<bool> Paired(this Expr<Tile> x)
     {
         return x.Field(o => o.Paired);
     }
}
}          

Observe that you can include ObsVar fields in your POCOs, which will be treated slightly differently so as to give the same semantics as ordinary ObsVars in the rest of your code. Note that, currently, nested classes, properties, methods, and inheritance are not supported by SpannerPocos.tt.

Reference - JavaScript

JSAct act

JSAct(fmt, arg1, arg2, ...) is the raw JavaScript code derived from the format string fmt with argN substituted for the Nth '#' appearing in the format string. For example, JSAct("alert(# + ' ' + #)", "Hello", "World") corresponds to the JavaScript alert("Hello" + ' ' + "World")

Noop

Noop() is the JavaScript 'no operation' action, {}.

Set

Set(x, e) is the JavaScript assigning e to x (that is: x = e if x is not an observable; x(e) if x is an observable).

While

While(cond, act) denotes the JavaScript while (cond) { act }.

DoWhile

DoWhile(act, cond) denotes the JavaScript do { act } while(cond);.

For

For(init, cond, inc, act) denotes the JavaScript for(init; cond; inc) { act }.

Continue

Continue() denotes the JavaScript continue action.

Break

Break() denotes the JavaScript break action.

Return

Return() denotes the JavaScript return action (note that actions in Spanner do not return values -- for that you need a func).

IfThen

IfThen(cond, thenAct) denotes the JavaScript if (cond) { thenAct } action.

IfThenElse

IfThenElse(cond, thenAct, elseAct) denotes the JavaScript if (cond) { thenAct } else { elseAct } action.

IfCase

IfCase(When(cond1, act1), When(cond2, act2), ..., When(condN, actN)) denotes the JavaScript if (cond1) { act1 } else if (cond2) { act2 } ... else if (condN) { actN }.

Otherwise

Otherwise(act) is shorthand for When(true, act) and is useful for the catch-all IfCase clause.

Do (expr)

Do(x) denotes the JavaScript x and is used to evaluate an expression purely for its side effect (e.g., Do(SetTimeout(...)) will set a timeout, but discard the return value otherwisedused for ClearTimeout).

Do (acts)

Do(act1, act2, ..., actN) denotes the JavaScript { act1; act2; ... actN; } and is used to treat a sequence of actions as a single act (a variant of this exists taking any enumerable sequence of actions).

Local (initially undefined)

Local(x => act) denotes the JavaScript var x; act.

Local (with initial value)

Local(expr, x => act) denotes the JavaScript var x = expr; act.

Throw (error object)

Throw(errObj) denotes the JavaScript throw(errObj), where the argument is a (subclass of) JSError which specifies a single string-valued message field.

Throw (error message)

Throw(errMsg) denotes the JavaScript throw(errMsg), where the argument is a string.

Try

Try(tryAct[, catchAct: x => catchAct][, finallyAct: finallyAct]) denotes the JavaScript try { tryAct } catch(x) { catchAct } finally { finallyAct }. At least one of the catch or finally parameters must be supplied.

VerbatimJavaScript

VerbatimJavaScript(...) denotes the JavaScript ... exactly as provided.

Reference - Knockout

ObsVar

ObsVar<Type>(name, value) constructs an observable variable of the given name and type, initialised to the given value. The variable is made a property of the enclosing view or library model. The type argument is optional if the type can be unambiguously inferred from the given value. The name argument is optional, but omitting it will lead to the JavaScript variable having a generated name. If the name is not unique, a distinguishing suffix will be appended.

Handwritten C#
var u = ObsVar<int[]>("u", new int[] { 1, 1, 2, 3, 5 });
var v = ObsVar<string>("v", null);
var w = ObsVar("w", true);
var x = ObsVar("x", 123);
var y = ObsVar("y", 2.718);
var z = ObsVar("z", "abc");          
Generated JavaScript
function ObsVarExample() {
  var self = this;
  self.u = ko.observableArray([1, 1, 2, 3, 5]);
  self.v = ko.observable(null);
  self.w = ko.observable(true);
  self.x = spanner.obsInt(123);
  self.y = spanner.obsDouble(2.718);
  self.z = ko.observable("abc");
};          

Note that array types are treated specially to take advantage of Knockout's array observables.

Note that observables of type int and double are treated specially, to ensure that these values are always reported as numbers. This is useful when, for example, the value is obtained from an input element, which always returns a string value.

NewObsVar

NewObsVar<Type>(value) dynamically creates a (reference to a) new observable of the given type and value.

Handwritten C#
var xs = ObsVar<ObsVarRef<int>[]>("xs", Array<ObsVarRef<int>>());
InitAct = Do(
    Push(xs, NewObsVar<int>(1)),
    Push(xs, NewObsVar<int>(2)),
    Push(xs, NewObsVar<int>(3))
);          
Generated JavaScript
function NewObsVarExample() {
  var self = this;
  self.xs = ko.observableArray([]);
  {
    self.xs.push(spanner.obsInt(1));
    self.xs.push(spanner.obsInt(2));
    self.xs.push(spanner.obsInt(3));
  }
};          

XXX Need to ensure Spanner uses Knockout observable array push.

ComputedVar

ComputedVar<Type>(name, value) dynamically creates a new computed observable of the given type and value with the given name. Any change to an observable in the value will cause the computed observable to be updated accordingly.The variable is made a property of the enclosing view or library model. The type argument is optional if the type can be unambiguously inferred from the given value. The name argument is optional, but omitting it will lead to the JavaScript variable having a generated name. If the name is not unique, a distinguishing suffix will be appended.

Handwritten C#
var x = ObsVar("x", 1);
var y = ComputedVar("y", 10 * x + 3);          
Generated JavaScript
function ComputedVarExample() {
  var self = this;
  self.x = spanner.obsInt(1);
  self.y = ko.computed(function () {
    return ((10 * self.x()) + 3);
  });
};          

NewComputedVar

NewComputedVar<Type>(value) dynamically creates a (reference to a) new computed observable of the given type and value.

Handwritten C#
var x = ObsVar("x", 0);
var xs = ObsVar<ComputedVarRef<int>[]>("xs", Array<ComputedVarRef<int>>());
InitAct = Do(
    Push(xs, NewComputedVar<int>(x + 1)),
    Push(xs, NewComputedVar<int>(x + 2)),
    Push(xs, NewComputedVar<int>(x + 3))
);          
Generated JavaScript
function NewComputedVarExample() {
  var self = this;
  self.xs = ko.observableArray([]);
  self.x = spanner.obsInt(0);
  {
    self.xs.push(ko.computed(function () {
      return (self.x() + 1);
    }));
    self.xs.push(ko.computed(function () {
      return (self.x() + 2);
    }));
    self.xs.push(ko.computed(function () {
      return (self.x() + 3);
    }));
  }
};          

Subscribe

Subscribe(observableRef, proc) creates a subscription on the observable, triggering the given procedure whenever the observable changes (the new value of the observable is passed to the procedure as its argument).

Handwritten C#
var x = ObsVar("x", 0);
var s = Var("s", Subscribe(x, Proc<int>(newValue => JSAct("/* Do something... */"))));
InitAct = Set(x, x + 1); // This will trigger the subscription on x.          
Generated JavaScript
function SubscribeExample() {
  var self = this;
  self.x = spanner.obsInt(0);
  self.s = self.x.subscribe((function (newValue) {
    /* Do something... */;
  }));
  {
    self.x((self.x() + 1));
  }
};          

The handle s returned from the observable can be used to cancel the subscription by invoking Dispose(s). If the subscription handle is not required, one can simply create it via Do(Subscribe(x, ...)) using the unary Do form to evaluate the Subscribe expression purely for its side effect (viz, placing the subscription).

Peek

Peek(observableRef) returns the current value of the observable without placing any dependency on that observable. This is useful, for example, when creating computed observables which do not depend on changes to certain observables used to compute them.

Handwritten C#
var x = ObsVar("x", 42);
var y = ObsVar("y", 69);
var z = ComputedVar(x + Peek(y)); // Changes to x (but not y) will update z.          
Generated JavaScript
function PeekExample() {
  var self = this;
  self.x = spanner.obsInt(42);
  self.y = spanner.obsInt(69);
  self._x1 = ko.computed(function () {
    return (self.x() + self.y.peek());
  });
};          

KoForEach (Virtual Binding)

KoForEach(xs, x => htmlForX) corresponds to the Knockout <!-- ko foreach: xs --> virtual binding.

Handwritten C#
var xs = ObsVar("xs", Array<int>(1, 2, 3));
Content = KoForEach(xs, x => P(Show(x)));          
Generated HTML
    <!-- ko foreach: xs() -->
      <p>
<span data-bind='text: $data'></span>      </p>
    <!-- /ko -->          
Generated JavaScript
function KoForEachVirtualExample() {
  var self = this;
  self.xs = ko.observableArray([1, 2, 3]);
};          

[Try it.]

KoForEach (Element Binding)

htmlElt.KoForEach(xs, x => htmlForX) corresponds to the Knockout foreach binding.

Handwritten C#
var xs = ObsVar("xs", Array<int>(1, 2, 3));
Content = UL().KoForEach(xs, x => LI(Show(x)));          
Generated HTML
    <ul data-bind='foreach: xs()'>
        <li>
<span data-bind='text: $data'></span>        </li>
    </ul>          
Generated JavaScript
function KoForEachElementExample() {
  var self = this;
  self.xs = ko.observableArray([1, 2, 3]);
};          

[Try it.]

Note that you can nest for-each bindings in Spanner much more conveniently than in vanilla Knockout. For example:

Handwritten C#
var xs = ObsVar("xs", Array("a", "b", "c"));
var ys = ObsVar("ys", Array(1, 2, 3));
Content =
    UL().KoForEach(xs, x =>
        KoForEach(ys, y =>
            LI(Show(x), Show(y))
        )
    );          
Generated HTML
    <ul data-bind='foreach: xs()'>
        <!-- ko foreach: $root.ys() -->
          <li>
<span data-bind='text: $parents[0]'></span><span data-bind='text: $data'></span>          </li>
        <!-- /ko -->
    </ul>          
Generated JavaScript
function NestedKoForEachExample() {
  var self = this;
  self.xs = ko.observableArray(["a", "b", "c"]);
  self.ys = ko.observableArray([1, 2, 3]);
};          

[Try it.]

KoVisible

htmlElt.Visible(cond) corresponds to the Knockout visible binding.

Handwritten C#
var x = ObsVar("x", true);
Content = P("Here I am.").KoVisible(x);          
Generated HTML
    <p data-bind='visible: x()'>
Here I am.    </p>          
Generated JavaScript
function KoVisibleExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

KoText

htmlElt.KoText(x) corresponds to the Knockout visible binding. It works for string, int, and double valued expressions.

Handwritten C#
var x = ObsVar("x", 3.14159);
Content = P().KoText(x);          
Generated HTML
<p data-bind='text: x()'>
</p>          
Generated JavaScript
function KoTextExample() {
  var self = this;
  self.x = spanner.obsDouble(3.14159);
};          

[Try it.]

Show

Show(x) is shorthand for Span().KoText(x)

Handwritten C#
var x = ObsVar("x", "Hello, World!");
Content = P(Show(x));          
Generated HTML
    <p>
<span data-bind='text: x()'></span>    </p>          
Generated JavaScript
function KoShowExample() {
  var self = this;
  self.x = ko.observable("Hello, World!");
};          

[Try it.]

KoHtml

htmlElt.KoHtml(x) corresponds to the Knockout html binding. It takes a string valued expression interpreted as literal HTML.

Handwritten C#
var x = ObsVar("x", "<span>Lawks!</span>");
Content = P().KoHtml(x);          
Generated HTML
<p data-bind='html: x()'>
</p>          
Generated JavaScript
function KoHtmlExample() {
  var self = this;
  self.x = ko.observable("<span>Lawks!</span>");
};          

[Try it.]

KoCss

htmlElt.KoCss(x) corresponds to the Knockout css binding where x evaluates to a space-separated list of CSS classes.

Handwritten C#
var x = ObsVar("x", "short shiny black");
Content = P("Nice dress").KoCss(x);          
Generated HTML
    <p data-bind='css: x()'>
Nice dress    </p>          
Generated JavaScript
function KoCssListExample() {
  var self = this;
  self.x = ko.observable("short shiny black");
};          

[Try it.]

Alternatively, htmlElt.KoCss(CssClassCondition(className, cond), ...) corresponds to the Knockout css binding taking a list of CSS class/condition pairs.

Handwritten C#
var score = ObsVar("score", 88);
Content = P(Show(score).KoCss(
    CssClassCondition("dismal", score < 30), 
    CssClassCondition("terrific", 90 < score)
));          
Generated HTML
    <p>
<span data-bind='css: {dismal: (score() < 30), terrific: (90 < score())}, text: score()'></span>    </p>          
Generated JavaScript
function KoCssCondExample() {
  var self = this;
  self.score = spanner.obsInt(88);
};          

[Try it.]

KoStyle

htmlElt.KoStyle(CssStyleValue(styleName, value) corresponds to the Knockout style binding.

Handwritten C#
var color = ObsVar("color", "red");
var weight = ObsVar("weight", "bold");
Content = P("Anger").KoStyle(
    CssStyleValue("color", color),
    CssStyleValue("font-weight", weight)
);          
Generated HTML
    <p data-bind='style: {color: color(), "font-weight": weight()}'>
Anger    </p>          
Generated JavaScript
function KoStyleExample() {
  var self = this;
  self.color = ko.observable("red");
  self.weight = ko.observable("bold");
};          

[Try it.]

(Observe that Spanner knows when to quote CSS class and style names.)

KoAttr

htmlElt.KoAttr(AttrValue(styleName, value) corresponds to the Knockout attr binding.

Handwritten C#
var min = ObsVar("min", 1);
var max = ObsVar("max", 10);
var x = ObsVar("x", min); // XXX This SHOULD generate a spanner.ObsInt!
Content = P(Input().KoValue(x).KoAttr(
    AttrValue<string>("type", "number"),
    AttrValue("min", min),
    AttrValue("max", max)
));          
Generated HTML
    <p>
<input data-bind='attr: {type: "number", min: min(), max: max()}, value: x'>    </p>          
Generated JavaScript
function KoAttrExample() {
  var self = this;
  self.min = spanner.obsInt(1);
  self.max = spanner.obsInt(10);
  self.x = ko.observable(self.min());
};          

[Try it.]

KoIf

htmlElt.If(cond) corresponds to the Knockout if binding.

Handwritten C#
var x = ObsVar("x", true);
Content = P("I think, therefore I am.").KoIf(x);          
Generated HTML
    <p data-bind='if: x()'>
I think, therefore I am.    </p>          
Generated JavaScript
function KoIfExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

This is also supported as a Knockout 'virtual' binding.

Handwritten C#
var x = ObsVar("x", true);
Content = KoIf(x, P("I think, therefore I am."));          
Generated HTML
    <!-- ko if: x() -->
      <p>
I think, therefore I am.      </p>
    <!-- /ko -->          
Generated JavaScript
function KoIfVirtualExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

KoIfNot

htmlElt.IfNot(cond) corresponds to the Knockout ifnot binding.

Handwritten C#
var x = ObsVar("x", true);
Content = P("I think not, therefore I am not.").KoIfNot(x);          
Generated HTML
    <p data-bind='ifnot: x()'>
I think not, therefore I am not.    </p>          
Generated JavaScript
function KoIfNotExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

This is also supported as a Knockout 'virtual' binding.

Handwritten C#
var x = ObsVar("x", true);
Content = KoIfNot(x, P("I think not, therefore I am not."));          
Generated HTML
    <!-- ko ifnot: x() -->
      <p>
I think not, therefore I am not.      </p>
    <!-- /ko -->          
Generated JavaScript
function KoIfNotVirtualExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

KoIfTruthy

htmlElt.IfTruthy(expr) corresponds to the Knockout if binding, but follows JavaScripts exciting rule of truthiness.

Handwritten C#
var x = ObsVar("x", "truthy value");
Content = P("I think vaguely, therefore I am vague.").KoIfTruthy(x);          
Generated HTML
    <p data-bind='if: x()'>
I think vaguely, therefore I am vague.    </p>          
Generated JavaScript
function KoIfTruthyExample() {
  var self = this;
  self.x = ko.observable("truthy value");
};          

[Try it.]

This is also supported as a Knockout 'virtual' binding.

Handwritten C#
var x = ObsVar("x", "truthy value");
Content = KoIfTruthy(x, P("I think vaguely, therefore I am vague."));          
Generated HTML
    <!-- ko if: x() -->
      <p>
I think vaguely, therefore I am vague.      </p>
    <!-- /ko -->          
Generated JavaScript
function KoIfTruthyVirtualExample() {
  var self = this;
  self.x = ko.observable("truthy value");
};          

[Try it.]

KoIfFalsy

htmlElt.IfFalsy(expr) corresponds to the Knockout ifnot binding, but follows JavaScripts exciting rule of falsiness.

Handwritten C#
var x = ObsVar("x", 0);
Content = P("I think wrongly, therefore I am wrong.").KoIfFalsy(x);          
Generated HTML
    <p data-bind='ifnot: x()'>
I think wrongly, therefore I am wrong.    </p>          
Generated JavaScript
function KoIfFalsyExample() {
  var self = this;
  self.x = spanner.obsInt(0);
};          

[Try it.]

This is also supported as a Knockout 'virtual' binding.

Handwritten C#
var x = ObsVar("x", 0);
Content = KoIfFalsy(x, P("I think wrongly, therefore I am wrong."));          
Generated HTML
    <!-- ko ifnot: x() -->
      <p>
I think wrongly, therefore I am wrong.      </p>
    <!-- /ko -->          
Generated JavaScript
function KoIfFalsyVirtualExample() {
  var self = this;
  self.x = spanner.obsInt(0);
};          

[Try it.]

KoClick

htmlElt.KoClick(action) corresponds to the Knockout click binding. Its argument may be

Handwritten C#
var x = ObsVar("x", "unclicked");
var plainAction = Set(x, "clicked on plain action");
var inlineProc = Proc(Set(x, "clicked on proc"));
var namedFn = Function("namedFn", () => ActExpr<bool>(
    Set(x, "clicked on func"),
    false // Returning false inhibits the default click action.
));
var eventHandler = Procedure<DummyType, DummyType>("eventHandler", (data, evt) =>
    Do(
        Set(x, "clicked on handler"),
        Alert(JSExpr<string>("#.type", evt))
    )
);
Content = Group(
    Button("Plain action").KoClick(plainAction),
    Button("Proc").KoClick(inlineProc),
    A("Func").WithAttr("href", "#").KoClick(namedFn),
    Button("Handler").KoClick(eventHandler),
    P("Status: ", Show(x))
);          
Generated HTML
    <button data-bind='click: (function () {
x("clicked on plain action");    })'>
Plain action    </button>
    <button data-bind='click: (function () {
x("clicked on proc");    })'>
Proc    </button>
<a data-bind='click: namedFn' href='#'>Func</a>    <button data-bind='click: eventHandler'>
Handler    </button>
    <p>
Status: <span data-bind='text: x()'></span>    </p>          
Generated JavaScript
function KoClickExample() {
  var self = this;
  self.x = ko.observable("unclicked");
  self.namedFn = (function () {
    self.x("clicked on func");
    return false;
  }        );
  self.eventHandler = (function (data, evt) {
    self.x("clicked on handler");
    alert(evt.type);
  });
};          

[Try it.]

KoEvent

htmlElt.KoEvent(DomEventHandler(eventID, handler), ...) corresponds to the Knockout event binding. Multiple DomEventHandler arguments may be provided. Each DomEventHandler takes an event name and an event handler. An event handler is one of the following:

Handwritten C#
var x = ObsVar("x", 0);
var y = ObsVar("y", 0);
var n = ObsVar("n", 0);
var eventHandler = Procedure<DummyType, DummyType>("eventHandler", (data, evt) =>
    Do(
        Set(x, JSExpr<int>("#.x", evt)),
        Set(y, JSExpr<int>("#.y", evt)),
        IfThen(JSExpr<string>("#.type", evt) == "click", Set(n, n + 1))
    )
);
Content = Div()
    .WithAttr("style", "width: 300px")
    .WithAttr("style", "height: 300px")
    .WithAttr("style", "background: yellow")
    .WithChildren(
        P("x = ", Show(x)),
        P("y = ", Show(y)),
        P("n = ", Show(n))
     )
    .KoEvent(
        DomEventHandler(DomEventName.MouseMove, eventHandler),
        DomEventHandler(DomEventName.Click, eventHandler)
    );          
Generated HTML
    <div data-bind='event: {mousemove: eventHandler, click: eventHandler}' style='background: yellow; height: 300px; width: 300px'>
      <p>
x = <span data-bind='text: x()'></span>      </p>
      <p>
y = <span data-bind='text: y()'></span>      </p>
      <p>
n = <span data-bind='text: n()'></span>      </p>
    </div>          
Generated JavaScript
function KoEventExample() {
  var self = this;
  self.x = spanner.obsInt(0);
  self.y = spanner.obsInt(0);
  self.n = spanner.obsInt(0);
  self.eventHandler = (function (data, evt) {
    self.x(evt.x);
    self.y(evt.y);
    if ((evt.type === "click")) {
      self.n((self.n() + 1));
    }
  });
};          

[Try it.]

KoNoBubble and KoBubble

formHtmlElt.KoBubble("eventName", cond) corresponds to the Knockout eventNameBubble: cond binding (KoNoBubble("eventName") is shorthand for KoBubble("eventName", false)).

Handwritten C#
var x = ObsVar("x", false);
Content = Div()
    .WithAttr("style", "border: 20px solid green")
    .KoClick(Fn(() => ActExpr<bool>(Alert("Div: click event"), true)))
    .WithChildren(
        Button("Click me!")
            .KoClick(Alert("Button: click event"))
            .KoBubble(DomEventName.Click, x),
        Input().WithAttr("type", "checkbox").KoChecked(x), " bubble button click."
     );          
Generated HTML
    <div data-bind='click: (function () {
alert("Div: click event");return true;    })' style='border: 20px solid green'>
      <button data-bind='clickBubble: x(), click: (function () {
alert("Button: click event");      })'>
Click me!      </button>
<input data-bind='checked: x' type='checkbox'> bubble button click.    </div>          
Generated JavaScript
function KoNoBubbleExample() {
  var self = this;
  self.x = ko.observable(false);
};          

[Try it.]

KoSubmit

formHtmlElt.KoSubmit(handler) corresponds to the Knockout submit binding.

Handwritten C#
var x = ObsVar("x", "");
var submitHandler = Procedure("submitHandler", Alert(x));
Content = Form(
    Input().KoValue(x),
    Button("Submit").WithAttr("type", "submit")
).KoSubmit(submitHandler);          
Generated HTML
    <form data-bind='submit: submitHandler'>
<input data-bind='value: x'>      <button type='submit'>
Submit      </button>
    </form>          
Generated JavaScript
function KoSubmitExample() {
  var self = this;
  self.x = ko.observable("");
  self.submitHandler = (function () {
    alert(self.x());
  });
};          

[Try it.]

KoEnable and KoEnabled

htmlElt.KoEnable(handler) corresponds to the Knockout enable binding.

Handwritten C#
var x = ObsVar("x", "");
var y = ObsVar("x", true);
Content = Div(
    Input().KoValue(x).KoEnabled(y),
    Input().WithAttr("type", "checkbox").KoChecked(y), "enable input"
);          
Generated HTML
    <div>
<input data-bind='enable: x(), value: x1'><input data-bind='checked: x' type='checkbox'>enable input    </div>          
Generated JavaScript
function KoEnableExample() {
  var self = this;
  self.x = ko.observable(true);
  self.x1 = ko.observable("");
};          

[Try it.]

Note that Spanner also provides a synonym, KoEnabled to suit those of us who prefer their code to have a more declarative flavour.

KoDisable and KoDisabled

htmlElt.KoDisable(handler) corresponds to the Knockout disable binding.

Handwritten C#
var x = ObsVar("x", "");
var y = ObsVar("x", true);
Content = Div(
    Input().KoValue(x).KoDisabled(y),
    Input().WithAttr("type", "checkbox").KoChecked(y), "disable input"
);          
Generated HTML
    <div>
<input data-bind='disable: x(), value: x1'><input data-bind='checked: x' type='checkbox'>disable input    </div>          
Generated JavaScript
function KoDisableExample() {
  var self = this;
  self.x = ko.observable(true);
  self.x1 = ko.observable("");
};          

[Try it.]

Note that Spanner also provides a synonym, KoDisabled to suit those of us who prefer their code to have a more declarative flavour.

KoValue

htmlElt.KoValue(obsVarRef) corresponds to the Knockout value binding.

Handwritten C#
var x = ObsVar("x", "");
Content = Div(
    Input().KoValue(x),
    P("You input '", Show(x), "'")
);          
Generated HTML
    <div>
<input data-bind='value: x'>      <p>
You input '<span data-bind='text: x()'></span>'      </p>
    </div>          
Generated JavaScript
function KoValueExample() {
  var self = this;
  self.x = ko.observable("");
};          

[Try it.]

KoValueUpdate

htmlElt.KoValueUpdate(obsVarRef) corresponds to the Knockout value binding.

Handwritten C#
var x = ObsVar("x", "");
Content = Div(
    P(Input().KoValue(x), " (no value update binding)"),
    P(Input().KoValue(x).KoValueUpdate(KoValueUpdateOn.KeyUp),
        " (value update on key up)"),
    P("You input '", Show(x), "'")
);          
Generated HTML
    <div>
      <p>
<input data-bind='value: x'> (no value update binding)      </p>
      <p>
<input data-bind='valueUpdate: "keyup", value: x'> (value update on key up)      </p>
      <p>
You input '<span data-bind='text: x()'></span>'      </p>
    </div>          
Generated JavaScript
function KoValueUpdateExample() {
  var self = this;
  self.x = ko.observable("");
};          

[Try it.]

KoHasFocus

htmlElt.KoHasFocus(obsVarRef) corresponds to the Knockout hasFocus binding.

Handwritten C#
var x = ObsVar("x", true);
Content = Div(
    Input().KoHasFocus(x),
    Input().WithAttr("type", "checkbox").KoChecked(x), "focus input"
);          
Generated HTML
    <div>
<input data-bind='hasfocus: x()'><input data-bind='checked: x' type='checkbox'>focus input    </div>          
Generated JavaScript
function KoHasFocusExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

KoChecked

htmlElt.KoChecked(obsVarRef) corresponds to the Knockout checked binding, used for checkbox and radio button inputs.

Handwritten C#
var x = ObsVar("x", true);
Content = Div(
    Input().WithAttr("type", "checkbox").KoChecked(x),
    Show(Cond<string>(x, "checked", "unchecked"))
);          
Generated HTML
    <div>
<input data-bind='checked: x' type='checkbox'><span data-bind='text: (x() ? "checked" : "unchecked")'></span>    </div>          
Generated JavaScript
function KoCheckedExample() {
  var self = this;
  self.x = ko.observable(true);
};          

[Try it.]

KoOptions

htmlElt.KoOptions(obsVarRef) corresponds to the Knockout options binding, used for select controls.

Handwritten C#
var xs = ObsVar("xs", Array("Alice", "Bob", "Cissy"));
Content = Div(
    Select().KoOptions(xs)
);          
Generated HTML
    <div>
<select data-bind='options: xs()'></select>    </div>          
Generated JavaScript
function KoOptionsExample() {
  var self = this;
  self.xs = ko.observableArray(["Alice", "Bob", "Cissy"]);
};          

[Try it.]

KoSelectedOptions

htmlElt.KoOptions(obsVarRef) corresponds to the Knockout options binding, used for select controls.

Handwritten C#
var xs = ObsVar("xs", Array("Alice", "Bob", "Cissy"));
var ys = ObsVar("xs", Array("Alice"));
Content = Div(
    Select()
        .WithAttr("size", 3)
        .WithAttr("multiple", true)
        .KoOptions(xs)
        .KoSelectedOptions(ys),
    Div().KoForEach(ys, y => P(Show(y)))
);          
Generated HTML
    <div>
<select data-bind='selectedOptions: xs, options: xs1()' multiple='True' size='3'></select>      <div data-bind='foreach: xs()'>
          <p>
<span data-bind='text: $data'></span>          </p>
      </div>
    </div>          
Generated JavaScript
function KoSelectedOptionsExample() {
  var self = this;
  self.xs = ko.observableArray(["Alice"]);
  self.xs1 = ko.observableArray(["Alice", "Bob", "Cissy"]);
};          

[Try it.]

KoUniqueName

htmlElt.KoUniqueName(cond) corresponds to the Knockout uniqueName binding (the condition argument is optional).

Handwritten C#
Content = Div(Input().KoUniqueName());          
Generated HTML
    <div>
<input data-bind='uniqueName: true'>    </div>          
Generated JavaScript
function KoUniqueNameExample() {
  var self = this;
};          

[Try it.]

KoTemplate

htmlElt.KoTemplate(templateModel) corresponds to the Knockout template binding. The user is referred to the TemplateModels and LibraryModels section of the Reference - Top Level chapter of the documentation.

Reference - Types

Act type

This type corresponds to JavaScript actions: code that is executed for its side effects, but which has no value as an expression.

Expr<T> type

This type corresponds to JavaScript expressions (values) of the given type.

Var<T> type

This type corresponds to ordinary (unobservable) JavaScript variables. This is a subtype of Expr<T>.

ObsVar<T> type

This type corresponds to Knockout observable variables. Obsvars are transparently dereferenced in expressions and assignments to obsvars are handled correctly. This is a subtype of Expr<T>.

ObsVarRef<T> type

This type corresponds to Knockout observable variable references: obsvarrefs are not transparently dereferenced in expressions. This allows one to write code which refers to an obsvar in itself, as opposed to the value it carries. An obsvarref for obsvar x can be obtained via Ref(x).

ComputedVar<T> type

This type corresponds to Knockout computed observables. Computedvars are transparently dereferenced in expressions. This is a subtype of Expr<T>.

ComputedVarRef<T> type

This type corresponds to Knockout computed observable variable references: computedvarrefs are not transparently dereferenced in expressions. This allows one to write code which refers to an computedvar in itself, as opposed to the value it carries. A computedvarref for computedvar x can be obtained via Ref(x).

Param<T> type

This type corresponds to JavaScript function parameters.

IndexExpr<T> type

This type corresponds to an element of an array. If x is an array, then Ith(x, i) is the indexexpr denoting element i of x(that is, x[i]). Indexexprs can be Set or treated as Expr<T> values. [The awkward syntax is the best one can do given C# as the host language.]

FieldExpr<T> type

This type corresponds to a field of an object. If x is an object, then Field(x, o => o.fld) is the fieldexpr denoting field fld of x (that is, x.fld). Fieldexprs can be Set or treated as Expr<T> values. [The awkward syntax is the best one can do given C# as the host language.]

Other types

Fill this in!

Reference - Alerts

Reference - Alerts

Alert
Confirm
Prompt

Alert

Alert(msg) denotes the JavaScript action alert(msg).

Confirm

Confirm(msg) denotes the JavaScript expression confirm(msg).

Prompt

Prompt(msg, value) denotes the JavaScript expression prompt(msg, value) (the value is optional and defaults to the empty string).

Reference - Arrays

Note on observable arrays

Observable and computed arrays are handled in the same way as ordinary JavaScript arrays: Spanner automatically manages observable dereferencing and so forth.

Array (constructor)

Array(x1, x2, ..., xN) denotes the JavaScript array literal [x1, x2, ..., xN].

Array (constructor)

Array(xs) denotes the JavaScript array literal obtained from the IEnumerable xs.

Ith

Ith(xs, i) denotes the JavaScript xs[i].

Concat

Concat(xs1, xs2, ..., xsN) denotes the JavaScript [].concat[xs1, xs2, ..., xsN].

Every

Every(xs, p) denotes the JavaScript xs.every(p) (this is overloaded to cover all the JavaScript options for p).

Filter

Filter(xs, p) denotes the JavaScript xs.filter(p) (this is overloaded to cover all the JavaScript options for p).

IndexOf

IndexOf(xs, x) denotes the JavaScript xs.indexOf(x).

Join

Join(xs [, sep]) denotes the JavaScript xs.join(sep) (the separator parameter is optional).

LastIndexOf

LastIndexOf(xs, x) denotes the JavaScript xs.lastIndexOf(x).

Length

Length(xs) denotes the JavaScript xs.length.

Map

Map(xs, f) denotes the JavaScript xs.map(f) (this is overloaded to cover all the JavaScript options for f).

Pop

Pop(xs) denotes the JavaScript xs.pop().

Shift

Shift(xs) denotes the JavaScript xs.shift().

Reduce

Reduce(xs, f, a) denotes the JavaScript xs.reduce(f, a) (this is overloaded to cover all the JavaScript options for f).

ReduceRight

ReduceRight(xs, f, a) denotes the JavaScript xs.reduceRight(f, a) (this is overloaded to cover all the JavaScript options for f).

Slice

Slice(xs, begin[, end]) denotes the JavaScript xs.slice(begin, end) (the end parameter is optional).

Some

Some(xs, p) denotes the JavaScript xs.some(p) (this is overloaded to cover all the JavaScript options for p).

ForEach

ForEach(xs, actFn) denotes the JavaScript xs.ForEach(actFn) (this is overloaded to cover all the JavaScript options for actFn).

Push

Push(xs, x) denotes the JavaScript xs.push(x).

Reverse

Reverse(xs) denotes the JavaScript xs.reverse().

Sort

Sort([cmpFn]) denotes the JavaScript xs.sort(cmpFn) (the comparison function parameter is optional; this is overloaded to cover all the JavaScript options for cmpFn).

Splice

Splice(xs, begin, howMany, ys) denotes the JavaScript [].splice.apply(xs, [begin, howMany].concat(ys)).

Unshift

Unshift(xs, ys) denotes the JavaScript [].unshift.apply(xs, ys).

Reference - Dates

NewDate

NewDate() denotes the JavaScript new Date().

NewDate milliseconds

NewDate(ms) denotes the JavaScript new Date(ms).

NewDate string

NewDate(dateStr) denotes the JavaScript new Date(dateStr).

NewDate parts

NewDate(year, month, day, hour, minute, second, ms) denotes the JavaScript new Date(year, month, day, hour, minute, second, ms) (any prefix of the parameter list after the year may be supplied, for example, NewDate(2014, 6).

NewUtcDate

NewUtcDate(year, month, day, hour, minute, second, ms) denotes the JavaScript new Date(Date.UTC(year, month, day, hour, minute, second, ms)) (any prefix of the parameter list after the year may be supplied, for example, NewUtcDate(2014, 6).

MillisecondsAtUtcDate

MillsecondsAtUtcDate(year, month, day, hour, minute, second, ms) denotes the JavaScript Date.UTC(year, month, day, hour, minute, second, ms) (any prefix of the parameter list after the year may be supplied, for example, MillisecondsAtUtcDate(2014, 6).

ParseDate

ParseDate(dateStr) denotes the JavaScript Date.parse(dateStr).

GetXYZ, GetUtcXYZ, SetXYZ, SetUtcXYZ

GetXYZ(x) denotes the JavaScript x.getXYZ(); SetXYZ(x, y) denotes the JavaScript x.setXYZ(y) where XYZ is any of Date, Day, FullYear, Hours, Milliseconds, Minutes, Month, Seconds, Time.

ToDateString

ToDateString(x) denotes the JavaScript x.toDateString().

ToLocaleDateString

ToLocaleDateString(x) denotes the JavaScript x.toLocaleDateString().

ToLocaleFormat

ToLocaleFormat(x) denotes the JavaScript x.toLocaleFormat().

ToLocaleString

ToLocaleString(x) denotes the JavaScript x.toLocaleString().

ToLocaleTimeString

ToLocaleTimeString(x) denotes the JavaScript x.toLocaleTimeString().

ToTimeString

ToTimeString(x) denotes the JavaScript x.toTimeString().

ToUtcString

ToUtcString(x) denotes the JavaScript x.toUtcString().

Reference - Globals

Undefined

Undefined<T>() denotes the JavaScript undefined pertaining to the type T.

ToString

ToString(x) denotes the JavaScript x.toString().

DecodeUri

DecodeUri(s) denotes the JavaScript decodeURI(s).

DecodeUriComponent

DecodeUriComponent(s) denotes the JavaScript decodeURIComponent(s).

EncodeUri

EncodeUri(s) denotes the JavaScript encodeURI(s).

EncodeUriComponent

EncodeUriComponent(s) denotes the JavaScript encodeURIComponent(s).

Escape

Escape(s) denotes the JavaScript escape(s).

Unescape

Unescape(s) denotes the JavaScript unescape(s).

Reference - Maths

Abs

Abs(x) denotes the JavaScript Math.abs(x).

Acos

Acos(x) denotes the JavaScript Math.acos(x).

Asin

Asin(x) denotes the JavaScript Math.asin(x).

Atan

Atan(x) denotes the JavaScript Math.atan(x).

Atan2

Atan2(x, y) denotes the JavaScript Math.atan2(x, y).

Ceil

Ceil(x) denotes the JavaScript Math.ceil(x).

Cos

Cos(x) denotes the JavaScript Math.cos(x).

E

E() denotes the JavaScript Math.E.

Exp

Exp(x) denotes the JavaScript Math.exp(x).

Floor

Floor(x) denotes the JavaScript Math.floor(x).

Ln10

Ln10(x) denotes the JavaScript Math.ln10(x).

Ln2

Ln2(x) denotes the JavaScript Math.ln2(x).

Log10E

Log10E(x) denotes the JavaScript Math.LN10(x).

Log2E

Log2E(x) denotes the JavaScript Math.LN2(x).

Log

Log(x) denotes the JavaScript Math.log(x).

MaxDouble

MaxDouble(xs) denotes the JavaScript Math.max.apply(null, xs).

MaxDouble

MaxDouble(x1, x2, ..., xN) denotes the JavaScript Math.max(x1, x2, ..., xN).

MaxInt

MaxInt(xs) denotes the JavaScript Math.max.apply(null, xs).

MaxDouble

MaxDouble(x1, x2, ..., xN) denotes the JavaScript Math.max(x1, x2, ..., xN).

MinDouble

MinDouble(xs) denotes the JavaScript Math.max.apply(null, xs).

MinDouble

MinDouble(x1, x2, ..., xN) denotes the JavaScript Math.max(x1, x2, ..., xN).

MinInt

MinInt(xs) denotes the JavaScript Math.max.apply(null, xs).

MinDouble

MinDouble(x1, x2, ..., xN) denotes the JavaScript Math.max(x1, x2, ..., xN).

Pi

Pi() denotes the JavaScript Math.PI.

Pow

Pow(x, y) denotes the JavaScript Math.pow(x, y).

Random

Random() denotes the JavaScript Math.random().

Sin

Sin(x) denotes the JavaScript Math.sin(x).

Sqrt

Sqrt(x) denotes the JavaScript Math.sqrt(x).

Sqrt1_2

Sqrt1_2() denotes the JavaScript Math.SQRT1_2.

Sqrt2

Sqrt2() denotes the JavaScript Math.SQRT2.

Tan

Tan(x) denotes the JavaScript Math.tan(x).

Reference - Regular Expressions

NewRegExp

NewRegExp(pattern[, modifiers]) denotes the JavaScript new RegExp(pattern, modifiers) (the modifiers parameter is optional).

Test

Test(r, s) denotes the JavaScript r.test(s).

Exec

Exec(r, s) denotes the JavaScript r.exec(s), and returns a RegExpMatch value which can be examined by the functions below.

MatchedSubstr

MatchedSubstr(m) denotes the matched substring for m, the result of an Exec.

ParenMatches

ParenMatches(m) denotes the array of matched substrings corresponding to parenthesized sub-patterns for m, the result of an Exec.

ParenMatch

ParenMatch(m, i) denotes the ith matched substrings corresponding to parenthesized sub-patterns for m, the result of an Exec.

MatchIndex

MatchIndex(m) denotes the start index of the matched substring for m, the result of an Exec.

MatchInput

MatchInput(m) denotes the match input for m, the result of an Exec.

Reference - Strings

Ith

Ith(s, i) denotes the JavaScript s[i].

CharAt

CharAt(s, i) denotes the JavaScript s.charAt(i).

CharCodeAt

CharCodeAt(s, i) denotes the JavaScript s.charCodeAt(i).

Concat

Concat(s1, s2, ..., sN) denotes the JavaScript "".concat(s1, s2, ..., sN).

FromCharCode

FromCharCode(x) denotes the JavaScript String.fromCharCode(x).

IndexOf

IndexOf(s1, s2[, fromIndex]) denotes the JavaScript s1.indexOf(s2, fromIndex) -- the fromIndex parameter being optional.

LastIndexOf

LastIndexOf(s1, s2[, fromIndex]) denotes the JavaScript s1.lastIndexOf(s2, fromIndex) -- the fromIndex parameter being optional.

LocaleCompare

LocaleCompare(s1, s2) denotes the JavaScript s1.localeCompare(s2).

Length

Length(s) denotes the JavaScript s.length.

Match

Match(s, x) denotes the JavaScript s.match(x) where x may be either a string or a regular expression.

Replace

Replace(s, x, y) denotes the JavaScript s.Match(x) where x may be either a string or a regular expression and y may be a string or a replacement function.

Search(s, x) denotes the JavaScript s.search(x) where x may be either a string or a regular expression.

Slice

Slice(s, beginSlice[, endSlice]) denotes the JavaScript s.slice(beginSlice, endSlice) -- the endSlice parameter being optional.

Split

Split(s, sep[, maxResults]) denotes the JavaScript s.split(sep, maxResults) -- the maxResults parameter being optional.

Substr

Substr(s, start[, length]) denotes the JavaScript s.substr(start, length) -- the length parameter being optional.

Substring

Substring(s, start[, top]) denotes the JavaScript s.substring(start, top) -- the top parameter being optional.

ToDouble

ToDouble(s) denotes the JavaScript +s.

ToInt

ToInt(s) denotes the JavaScript +s.

ToLocaleLowerCase

ToLocaleLowerCase(s) denotes the JavaScript s.toLocaleLowerCase().

ToLocaleUpperCase

ToLocaleUpperCase(s) denotes the JavaScript s.toLocaleUpperCase().

ToLowerCase

ToLowerCase(s) denotes the JavaScript s.toLowerCase().

ToUpperCase

ToUpperCase(s) denotes the JavaScript s.toUpperCase().

Trim

Trim(s) denotes the JavaScript s.trim().