Key Elements Of Seven5

Seven5 is a toolkit for constructing modern web applications. Applications written against Seven5 are written in Go—entirely in Go. Seven5 provides facilities for developing both the server and client portion of a web application as well as for exchanging Go data structures between them.

This article is intended to provide a light overview of the key ideas of Seven5. More detail, including a fairly complex real application, can be found in the tutorial. The current release of Seven5 is named cure. If you have questions about this article or the toolkit in general, you can ask for help in the google group. Seven5 is so named because the author began work on it while living in Paris, France and all the postal codes for Paris begin with 75.

Both sides of the wire

In this article, we will focus most of our attention on the client side of an application. In the past, one would have been forced to build this part in Javascript. The primary reason for this focus on the client side is that Go on the server side is far more common, and far more commonly understood. The server portion is what most people expect a “Go-based web toolkit” to be.

To build Go programs that run in a browser, they must be compiled to Javascript. For this, Seven5 expects you to use the wonderful Gopherjs compiler. “It just works” is perhaps the highest compliment you can give to a compiler, and that certainly applies here. It is a testament to its quality that this article won’t even mention Gopherjs further, in the same way that it will not mention that the Go compilation tools on the server (6g and the like)–because they just work.

Modern

We have referred to modern web applications previously. To be more precise, we mean applications which do not generate web content on the server but rather supply an API for clients–including a web browser–to obtain content from. The returned content is then formatted for display. The “server side” of a Seven5 app has two basic tasks: respond to requests for static, unchanging files and respond to an API of the developer’s choice. Seven5 expects that API to be RESTful, although this is not because REST is the best possible choice, but rather because REST is simple, common, and well-known.

Ajax

If the server’s job is to expose a REST API, the client’s job is to consume that API and present results to the user in a browser. In Seven5, the client application can use the function AjaxGet to fetch some data. In this example we are retrieving a slice of PaymentMethod structures from the server:

func (p myPage) getPaymentMethods() {
	var methods *[]*wire.PaymentMethod
	content, errChan := s5.AjaxGet(methods, "/rest/paymentmethods")
	go func() {
		select {
		case raw := <-content:
			methods := raw.(*[]*wire.PaymentMethod)
			processSomeMethods(*methods)
		case err := <-errChan:
			print("unable to get information about payments: ", err.StatusCode, err.Message)
		}
	}()
}

Two conventions about Seven5 can be seen in this example. First, Seven5 is imported into a Go program as s5. For the server side of an application this corresponds to github.com/seven5/seven5 but for a client program, as above, this corresponds to the package github.com/seven5/seven5/client. Second, the PaymentMethod type being exchanged is part of a package called “wire”. Wire types are those that are exchanged between the client and server. A package that includes wire types is compiled by both the client and server since it refers to the structures being exchanged between them. There is no way for the client and server to get “out of sync”.

The call to s5.AjaxGet returns two channels, one for content in the success case and one for the error case. Only one of these channels will receive data. So it is appropriate to call select on these two channels to wait until the content has been received from the server. Because this can take an unknown amount of time–browsers will typically try for 60 seconds to reach a server that is down–we wrap the select in a goroutine so as not to “lock up” the user’s browser while we wait for the server to provide this data. It is natural in go to think of asynchronous calls to the server as something that will produce a value on a channel when it completes.

HTML Generation

Once data is received by the client, such as in our example above, it has to be converted into HTML that can be rendered by the browser. Typically, the “framing” of a web page is static content—a simple HTML file—and the dynamic portion is added to the page based on results returned from an Ajax call. Seven5 provides a tree-building library for generating DOM subtrees that can be attached to a page.

This is a trivial example of a code snippet that builds a small DOM tree. This tree has a div HTML element with one child that is also a div element. The child has in turn two span child elements, and the text to display in these span elements is a fixed string.

s5.DIV(
	s5.DIV(
		s5.SPAN(
			s5.Text("foo"),
		),
		s5.SPAN(
			s5.Text("bar"),
		),
	),
)

The snippets in this section use formatting to make the tree structure easier to see. This formatting is allowed by gofmt.

You can easily add some CSS classes to make your HTML tree look nicer when it appears on screen. CSS classes, and many other HTML-related entities, are modelled as types in Seven5.

var (
	row = s5.NewCssClass("row")
	offset1 = s5.NewCssClass("col-sm-offset-1")
	col10 = s5.NewCssClass("col-sm-10")
)

...

s5.DIV(
	s5.Class(row)
	s5.DIV(
		s5.Class(offset1)
		s5.Class(col10)
		s5.SPAN(
			s5.Text("foo"),
		),
		s5.SPAN(
			s5.Text("bar"),
		),
	),
)

You can even add event handlers directly in the tree building code, and we will show this is in the next example. This is used far less commonly in Seven5 than most web toolkits, for reasons that will be explained in the next section, but this example prints out a message when the “foo” on screen is clicked.

Although Seven5’s client package is built on top of JQuery internally, this is not exposed frequently, as Seven5 provides its own abstractions that are less error-prone that the JQuery mechanisms. One place this is exposed, however, is the jquery.Event object that is passed to the handler of a click event.

var (
	row = s5.NewCssClass("row")
	offset1 = s5.NewCssClass("col-sm-offset-1")
	col10 = s5.NewCssClass("col-sm-10")
)

...

s5.DIV(
	s5.Class(row)
	s5.DIV(
		s5.Class(offset1)
		s5.Class(col10)
		s5.SPAN(
			s5.Text("foo"),
			s5.Event(s5.CLICK, func(evt jquery.Event) {
				print("clicked foo")
			}),
		),
		s5.SPAN(
			s5.Text("bar"),
		),
	),
)

Constraints and Attributes

Seven5 uses a technique called “constraints” to make building the user interface of a web application easier. A constraint is simply a function that computes a result. The values that a constraint operates on–its parameters–and produces are called attributes. These correspond directly to Go’s functions and variables.

Let’s define a structure that has a few attributes so we can see how this will work:

type lightSwitches struct {
	s1     s5.BooleanAttribute
	s2     s5.BooleanAttribute
	output s5.StringAttribute
}

This structure definition uses s5.BooleanAttribute and s5.StringAttribute instead of Go’s builtin bool and string because Seven5 does some extra bookkeeping around each attribute. Let’s define a constraint, which is just a function:

func eitherSwitchIsOn(raw []s5.Equaler) s5.Equaler {
	s1Status := raw[0].(s5.BoolEqualer).B)
	s2Status := raw[1].(s5.BoolEqualer).B)
	if s1Status || s2Status {
		return s5.StringEqualer{S:"on"}
	}
	return s5.StringEqualer{S: "off"} 
}

The types here are not static as one would like, but it should be clear that the parameters passed to eitherSwitchIsOn are two booleans and it returns the string “on” if either one of these booleans is true, otherwise it returns “off”. “equalers” in the above example such as s5.BoolEqualer and s5.StringEqualer represent the value of an attribute.

Let’s attach the constraint now:

	// ls is an instance of the type lightSwitches

	ls.output.Attach(
		s5.NewSimpleConstraint(
			eitherSwitchIsOn,
			ls.s1,
			ls.s2))

This snippet bears scrutiny. It attaches the eitherSwitchIsOn constraint function we’ve written above to the output string field. The inputs to the function are the two other boolean fields, s1 and s2. Once attached, Seven5 guarantees that this constraint is always met.

For the curious, the algorithm used to insure constraint evaluation is both correct and close to minimal is eval_vite from 1993.

In and of itself, this ability to have functions of variables, even ones that are maintained automatically, would be of little value. The big win comes from the ability to connect attributes and constraints to the DOM that is generated by a web application. Let’s connect our switches and the output to the DOM:

	// onClass is a css class that changes the display for "on"
	// ls is an instance of the type lightSwitches

s5.DIV(
	s5.Class(row)
	s5.DIV(
		s5.Class(offset1)
		s5.Class(col10)
		s5.SPAN(
			s5.CssExistence(onClass, ls.s1),
			s5.Text("switch1"),
			s5.Event(s5.CLICK, func(evt jquery.Event) {
				ls.s1.Set(!ls.s1.Get())
			}),
		),
		s5.SPAN(
			s5.CssExistence(onClass, ls.s2),
			s5.Text("switch2"),
			s5.Event(s5.CLICK, func(evt jquery.Event) {
				ls.s2.Set(!ls.s2.Get())
			}),
		),
		s5.SPAN(
			s5.TextEqual(ls.output)
		),
	),
)

In this example, we have added three additional constraints to the attributes in the lightSwitches struct. Two of these are to constrain the presence or absence of the CSS class onClass on the DOM elements to the appropriate span elements for switches s1 and s2. Seven5 will guarantee that if ls.s1 is true, the CSS class will be attached to the first span and it will not be present if the attribute is false. This can provide feedback to the user about the state–such as perhaps changing the color, background, or any other attribute that can be manipulated through style sheets.

The other constraint is that the text of the third span will always be the same as the value of the field ls.output. Since this is computed by the constraint we wrote above, the displayed value is always the logical OR of the two switches.

Finally, we have added two small event handlers, one for each “switch”. When the appropriate “switch” text is clicked, the value of the boolean s1 or s2 will get inverted. This, naturally, cascades through the function eitherSwitchIsOn that then updates the attribute output, that causes the constraint on the last span’s text content to be updated appropriately. Also, the constraint that chooses to add or remove the CSS class onClass will update the display for the particular switch span that is clicked on.

This type of event handler is common in Seven5: once you have expressed your display as a set of constraints, the event handler’s job is simply to update the state, Seven5 takes care of any necessary screen updates, and is careful to not update things that are not affected by the change in the data. Constraints are not a free lunch: they require work from you in structuring your application. They require you to think carefully about the inputs and outputs of your user interface, and require clear articulation of the processing to be done. This articulation must be done so that the processing of inputs to outputs can be encoded in constraint functions. Our experience has shown that the effort required to structure a UI with constraints is easily outweighed by the benefits gained in cleaner UI code.

Summary

In this short article, we’ve touched on three things that make building a modern web application easier when you use Seven5. First, the ability to use the familiar Go abstraction of channels to handle asynchronous connections from a web browser to the server. Second, Seven5’s tree-building utilities that make it convenient to programmatically construct trees of DOM elements that reflect the semantics of your app. Finally, we discussed Seven5’s use constraints to make the connections between application data structures and the user interface shown in the browser.