Files
EWF/draft/library/server/wsf_js_widget/examples/custom

EWF JS WIDGET Custom PROJECT

Getting Started

  • Copy and rename the custom-template folder

How to create a custom JS-WIDGET

#Introduction The main goal of this module is to provide easy to use javascript/html widget (controls), which allow the developer to build dynamic interfaces without the need to code javascript code. The wsf_js_widget module creates the illusion of a shared state between client and server, this illusion is created by restoring the clients state in the beginning of each request.

The wsf_js_widget module provides so called "statefull" controls which can be modified on callback (when a callback event is triggered e.g the client presses a button). The callbacks can be fully defined in Eiffel using agents.

#Goal The goal of this tutorial is to build a custom js widget control. We will build a simple bar chart which can be modified by the server. The javascript implementation is based on the javascript library d3. http://bl.ocks.org/mbostock/3885304

Preview

Step 1

First we define the state which is shared between the client and the server. The bar chart data can be represented as a key value hashmap, but since the json standard does guarantee the key order, we will use a JSON list containing objects with the key and the value.

[{"key":"Eiffel", "value":12}, {"key":"C#", "value":12}, {"key":"Java", "value":12}]

#Step 2 Based on the shared state we now can start the Eiffel implementation of the control.

Basic WSF Control structure

class
	WSF_BARCHART_CONTROL

inherit

	WSF_CONTROL
		rename
			make as make_control
		end

create
	make

feature {NONE} -- Initialization

	make
		do
			make_control ("div")
		end

feature -- State handling

	set_state (new_state: JSON_OBJECT)
		do		
		end

	state: WSF_JSON_OBJECT
		do
			create Result.make
		end

feature -- Callback

	handle_callback (cname: LIST [STRING_32]; event: STRING_32; event_parameter: detachable ANY)
		do
				-- Do nothing here
		end

feature -- Rendering

	render: STRING_32
		local
			temp: STRING_32
		do
			Result := render_tag ("Loading ...", "")
		end

end

Now we can create the set data functions. This step is straight forward:

  • We add the data structure to the class.

  • Create a setter function which registers the change in the state_changes json object. This object is used to pass the state changes which happen in a call back to the browser.

  • Since the list of tuples is not a primitiv data type we need a function which translates the data as a json object.

Code changes

feature -- Data

	set_data (a_data: like data)
		do
			data := a_data
			state_changes.replace (data_as_json, "data")
		end

	data_as_json : JSON_ARRAY
	local
		item: WSF_JSON_OBJECT
	do
		create Result.make_array
		across
			data as el
		loop
			create item.make
			if attached {STRING_8} el.item.at(1) as key and attached {DOUBLE}el.item.at(2) as value then
			item.put_string (key, "key")
			item.put_real (value, "value")
			Result.add(item)
			end
		end
	end
	data: ARRAY [TUPLE [STRING_8, DOUBLE]]

The state handling is still missing. We need to restore the state, because the objects are recreated on each callback.

	set_state (new_state: JSON_OBJECT)
			-- Restore data from json
		do
			if attached {JSON_ARRAY} new_state.item ("data") as new_data then
				create data.make_filled (["",0.0], 1, new_data.array_representation.count)
				across
					new_data.array_representation as d
				loop
					if attached {JSON_OBJECT} d.item as citem
						and then attached {JSON_STRING} citem.item ("key") as key
						and then attached {JSON_NUMBER} citem.item ("value") as value then
						data.put ([key.item,value.item.to_real_64], d.cursor_index)
					end
				end
			end
		end

	state: WSF_JSON_OBJECT
			-- Return state with data
		do
			create Result.make
			Result.put (data_as_json, "data")
		end

Before starting the javascript development part we add the control to the page.

	initialize_controls
		do
			Precursor
			-- Bind custom.js
			add_javascript ("assets/custom.js")
			navbar.set_active (1)
			-- Define colum 12/12
			main_control.add_column (6)
			main_control.add_column (6)
			create chart.make
			chart.set_data (<<["Eiffel",100.0],["Java",20.0],["C++",40.0]>>)
			main_control.add_control (1,chart)

			-- Create button
			create button.make ("Load new dataset")
			button.set_click_event (agent do
				chart.set_data (<<["Eiffel",120.0],["Java",60.0],["C#",30.0],["Python",230.0]>>)
			end)
			--Add button control
			main_control.add_control (2,button)
		end

Javascript Part

The part of the application an be written in javascript or any language which compiles to javascript. We decided to use coffescript beacuse it is simpler to define classes.

  • Create a file assets/graph.css
.bar {
  fill: steelblue;
}

.bar:hover {
  fill: brown;
}

.axis {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}
  • Create a file assets/custom.coffee
  • Start cofffescript compiler coffee -cbw widget.coffee

First we create a class with the same name like the Eiffel class. If the control has any external dependencies we can add these dependencies in a JSON array with the name requirements. All the javascript and css files referenced in this array will be loaded before the attach_events function is executed. Already loaded dependencies will not be reloaded.

	class WSF_BARCHART_CONTROL extends WSF_CONTROL
		requirements: ['/assets/d3.min.js', '/assets/graph.css']

After loading the dependencies the attach_events function is executed. In this function we need to attach_events the d3 graph. @$el is a jquery object which refrences the DOM element of the current control.

	attach_events: ()->
		super
		margin =
			top: 20
			right: 20
			bottom: 30
			left: 40

		#Clear
		@$el.html("")
		#Calculate width
		width = @$el.width() - margin.left - margin.right
		height = 500 - margin.top - margin.bottom
		#Create axis
		x = d3.scale.ordinal().rangeRoundBands([
			0
			width
		], .1)
		y = d3.scale.linear().range([
			height
			0
		])
		@xAxis = d3.svg.axis().scale(x).orient("bottom")
		@yAxis = d3.svg.axis().scale(y).orient("left").ticks(10)
		svg = d3.select(@$el[0]).append("svg")
			.attr("width", width + margin.left + margin.right)
			.attr("height", height + margin.top + margin.bottom)
			.append("g")
			.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
		
		#Store svg, x and y scale in class so we can update the domain if a callback occures
		@x = x
		@y = y
		@svg = svg
		@height = height
		@xAxis_container = svg.append("g")
			.attr("class", "x axis")
			.attr("transform", "translate(0," + height + ")")
		@yAxis_container = svg.append("g")
			.attr("class", "y axis")
		#Call build graph to load the graph data
		@updatechart()

	updatechart: ()->
		height = @height
		data = @state.data
		x = @x
		y = @y
		#Set domain
		x.domain data.map((d) ->
			d.key
		)
		y.domain [
			0
			d3.max(data, (d) ->
				d.value
			)
		]
		#Update axis
		@xAxis_container.transition().duration(1000).call @xAxis
		@yAxis_container.transition().duration(1000).call @yAxis
		#Map data
		rect = @svg.selectAll(".bar")
			.data(data, (d)-> d.key)
		#Add new bars
		rect.enter()
				.insert("rect")
					.attr("class", "bar")
					.attr("x", (d) ->
						x d.key
					)
					.attr("width", x.rangeBand())
					.attr("y", (d) ->
						height
					)
					.attr "height", (d) ->
						0
		#Adjust bar positions and sizes
		rect.transition().duration(1000).attr("x", (d) ->
				x d.key
			)
			.attr("width", x.rangeBand())
			.attr("y", (d) ->
				y d.value
			)
			.attr "height", (d) ->
				height - y(d.value)
		#Animate exiting bars
		rect.exit().transition().duration(1000)
			.style('opacity', 0)
			.attr("height", (d) ->
				0
			).attr("y", (d) ->
				height
			).remove()


The last part which is still missing. Is the update state event handling. If the state is updated the update function is called with the new state as parameter. All we must do now is update the state object and call the updatechart function.

	update: (state) ->
		if state.data != undefined
			@state['data'] = state.data
			data = state.data
			@updatechart()