Commit a7949c8c authored by Colin Eberhardt's avatar Colin Eberhardt Committed by Sindre Sorhus

Close GH-103: Added GWT implementation.

parent 47c8ab0a
## TodoMVC - GWT Version
This is a Google Web Toolkit (GWT) implementation of the TodoMVC application. The GWT version
is rather different to all the other TodoMVC versions (Backbone, Knockout etc ...) in that it is
written in Java which is compiled to JavaScript. The files within the `gwttodo` folder are the result
of running the GWT compilation process on the Java files found within the src folder. The UI
pattern used by this application is Model-View-Presenter.
Whilst this application is very different to the other TodoMVC implementations, it still makes for
an interesting comparison. Large-scale JavaScript applications are often written with GWT or Closure,
with the resulting JavaScript code delivered to the client being compiled.
You can read more about the implementation on my blog:
http://www.scottlogic.co.uk/blog/colin/2012/03/developing-a-gwt-todomvc-application/
### Folder structure
- `css` - includes GWT specific `app.css`, most styling is taken from the base CSS file `../../assets/base.css`
- `gwttodo` - the GWT compiled output, this includes various HTML files, which contain the JavaScript
code for each <a href "http://code.google.com/webtoolkit/doc/latest/tutorial/compile.html">GWT permutation</a>. This
folder also includes some redundant files, see the issue <a href="https://github.com/ColinEberhardt/todomvc/issues/9">
Remove redundant compiler output</a>.
- `src` - the Java source for this application
### Building this application
The GWT TodoMVC application was built with Java 1.6 and GWT 2.4.0. The easiest way to build this application
is to download the GWT SDK:
http://code.google.com/webtoolkit/gettingstarted.html
Together with the Eclipse plugin:
http://code.google.com/webtoolkit/usingeclipse.html
#todo-count span.word {
font-weight: normal;
}
/* The GWT TodoMVC uses a CellList - a framework widget for rendering a list of cells - to
render the list of todo items. Unfortunately the CellList uses a div as its root container
and wraps each cell in a separate div. There are no extension points that allow you to change this.
As a result, this application deviates from the TodoMVC standard of ul / li for the to do list. The
styles applied to the li elements are duplicated here, matching a listItem class instead. */
#todo-list .listItem {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list div :last-child .listItem {
border-bottom: none;
}
#todo-list .listItem.editing {
border-bottom: none;
padding: 0;
}
#todo-list .listItem.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list .listItem.editing .view {
display: none;
}
#todo-list .listItem .toggle {
text-align: center;
width: 35px;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list .listItem .toggle:after {
font-size: 18px;
content: '✔';
line-height: 40px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list .listItem .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list .listItem label {
word-break: break-word;
margin: 20px 15px;
display: inline-block;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list .listItem.completed label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list .listItem .destroy {
display: none;
position: absolute;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list .listItem .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list .listItem .destroy:after {
content: '✖';
}
#todo-list .listItem:hover .destroy {
display: block;
}
#todo-list .listItem .edit {
display: none;
}
#todo-list div :last-child .listItem.editing {
margin-bottom: -1px;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
function gwttodo(){var L='',ec='\n-',sb='" for "gwt:onLoadErrorFn"',qb='" for "gwt:onPropertyErrorFn"',Tb='"<script src=\\"',eb='"><\/script>',V='#',dc=');',Xb='-\n',fc='-><\/scr',Ub='.cache.js\\"><\/scr" + "ipt>"',X='/',Kb='171663EC66CBE8FE7F0996BF7956E52B',Lb='1CAC86B44F6B27D12EAF64AAF8436B20',Mb='2BE730BB9138544318275E910F2291A9',Nb='899ECB4A54E551C44C16980D19C0A649',Qb=':',kb='::',Vb='<scr',db='<script id="',nb='=',W='?',Ob='A332DBFB2D1C19D956ABF64D582D9BBB',Pb='ADBEA39003F2D629A6F17A9FEE0ADA53',xb='ActiveXObject',pb='Bad handler "',yb='ChromeTab.ChromeFrame',Hb='Cross-site hosted mode not yet implemented. See issue ',Rb='DOMContentLoaded',fb='SCRIPT',cb='__gwt_marker_gwttodo',gb='base',$='baseUrl',P='begin',O='bootstrap',wb='chromeframe',Z='clear.cache.gif',mb='content',cc='document.write(',U='end',$b='evtGroup: "loadExternalRefs", millis:(new Date()).getTime(),',ac='evtGroup: "moduleStartup", millis:(new Date()).getTime(),',Eb='gecko',Fb='gecko1_8',Q='gwt.codesvr=',R='gwt.hosted=',S='gwt.hybrid',rb='gwt:onLoadErrorFn',ob='gwt:onPropertyErrorFn',lb='gwt:property',M='gwttodo',ab='gwttodo.nocache.js',jb='gwttodo::',Ib='http://code.google.com/p/google-web-toolkit/issues/detail?id=2079',Db='ie6',Cb='ie8',Bb='ie9',Y='img',gc='ipt>',Wb='ipt><!-',Sb='loadExternalRefs',hb='meta',Zb='moduleName:"gwttodo", sessionId:window.__gwtStatsSessionId, subSystem:"startup",',T='moduleStartup',Ab='msie',ib='name',ub='opera',zb='safari',_='script',Jb='selectingPermutation',N='startup',_b='type: "end"});',bc='type: "moduleRequested"});',bb='undefined',Gb='unknown',tb='user.agent',vb='webkit',Yb='window.__gwtStatsEvent && window.__gwtStatsEvent({';var l=window,m=document,n=l.__gwtStatsEvent?function(a){return l.__gwtStatsEvent(a)}:null,o=l.__gwtStatsSessionId?l.__gwtStatsSessionId:null,p,q,r=L,s={},t=[],u=[],v=[],w=0,x,y;n&&n({moduleName:M,sessionId:o,subSystem:N,evtGroup:O,millis:(new Date).getTime(),type:P});if(!l.__gwt_stylesLoaded){l.__gwt_stylesLoaded={}}if(!l.__gwt_scriptsLoaded){l.__gwt_scriptsLoaded={}}function z(){var b=false;try{var c=l.location.search;return (c.indexOf(Q)!=-1||(c.indexOf(R)!=-1||l.external&&l.external.gwtOnLoad))&&c.indexOf(S)==-1}catch(a){}z=function(){return b};return b}
function A(){if(p&&q){p(x,M,r,w);n&&n({moduleName:M,sessionId:o,subSystem:N,evtGroup:T,millis:(new Date).getTime(),type:U})}}
function B(){function e(a){var b=a.lastIndexOf(V);if(b==-1){b=a.length}var c=a.indexOf(W);if(c==-1){c=a.length}var d=a.lastIndexOf(X,Math.min(c,b));return d>=0?a.substring(0,d+1):L}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=m.createElement(Y);b.src=a+Z;a=e(b.src)}return a}
function g(){var a=D($);if(a!=null){return a}return L}
function h(){var a=m.getElementsByTagName(_);for(var b=0;b<a.length;++b){if(a[b].src.indexOf(ab)!=-1){return e(a[b].src)}}return L}
function i(){var a;if(typeof isBodyLoaded==bb||!isBodyLoaded()){var b=cb;var c;m.write(db+b+eb);c=m.getElementById(b);a=c&&c.previousSibling;while(a&&a.tagName!=fb){a=a.previousSibling}if(c){c.parentNode.removeChild(c)}if(a&&a.src){return e(a.src)}}return L}
function j(){var a=m.getElementsByTagName(gb);if(a.length>0){return a[a.length-1].href}return L}
var k=g();if(k==L){k=h()}if(k==L){k=i()}if(k==L){k=j()}if(k==L){k=e(m.location.href)}k=f(k);r=k;return k}
function C(){var b=document.getElementsByTagName(hb);for(var c=0,d=b.length;c<d;++c){var e=b[c],f=e.getAttribute(ib),g;if(f){f=f.replace(jb,L);if(f.indexOf(kb)>=0){continue}if(f==lb){g=e.getAttribute(mb);if(g){var h,i=g.indexOf(nb);if(i>=0){f=g.substring(0,i);h=g.substring(i+1)}else{f=g;h=L}s[f]=h}}else if(f==ob){g=e.getAttribute(mb);if(g){try{y=eval(g)}catch(a){alert(pb+g+qb)}}}else if(f==rb){g=e.getAttribute(mb);if(g){try{x=eval(g)}catch(a){alert(pb+g+sb)}}}}}}
function D(a){var b=s[a];return b==null?null:b}
function E(a,b){var c=v;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function F(a){var b=u[a](),c=t[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(y){y(a,d,b)}throw null}
u[tb]=function(){var c=navigator.userAgent.toLowerCase();var d=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return c.indexOf(ub)!=-1}())return ub;if(function(){return c.indexOf(vb)!=-1||function(){if(c.indexOf(wb)!=-1){return true}if(typeof window[xb]!=bb){try{var b=new ActiveXObject(yb);if(b){b.registerBhoIfNeeded();return true}}catch(a){}}return false}()}())return zb;if(function(){return c.indexOf(Ab)!=-1&&m.documentMode>=9}())return Bb;if(function(){return c.indexOf(Ab)!=-1&&m.documentMode>=8}())return Cb;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(c);if(a&&a.length==3)return d(a)>=6000}())return Db;if(function(){return c.indexOf(Eb)!=-1}())return Fb;return Gb};t[tb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};gwttodo.onScriptLoad=function(a){gwttodo.onScriptLoad=null;p=a;A()};if(z()){alert(Hb+Ib);return}C();B();n&&n({moduleName:M,sessionId:o,subSystem:N,evtGroup:O,millis:(new Date).getTime(),type:Jb});var G;try{E([Bb],Kb);E([ub],Lb);E([Fb],Mb);E([Cb],Nb);E([zb],Ob);E([Db],Pb);G=v[F(tb)];var H=G.indexOf(Qb);if(H!=-1){w=Number(G.substring(H+1));G=G.substring(0,H)}}catch(a){return}var I;function J(){if(!q){q=true;A();if(m.removeEventListener){m.removeEventListener(Rb,J,false)}if(I){clearInterval(I)}}}
if(m.addEventListener){m.addEventListener(Rb,function(){J()},false)}var I=setInterval(function(){if(/loaded|complete/.test(m.readyState)){J()}},50);n&&n({moduleName:M,sessionId:o,subSystem:N,evtGroup:O,millis:(new Date).getTime(),type:U});n&&n({moduleName:M,sessionId:o,subSystem:N,evtGroup:Sb,millis:(new Date).getTime(),type:P});var K=Tb+r+G+Ub;m.write(Vb+Wb+Xb+Yb+Zb+$b+_b+Yb+Zb+ac+bc+cc+K+dc+ec+fc+gc)}
gwttodo();
\ No newline at end of file
<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<link type="text/css" rel="stylesheet" href="../../assets/base.css">
<link type="text/css" rel="stylesheet" href="css/app.css">
<title>GWT • TodoMVC</title>
<script type="text/javascript" language="javascript" src="gwttodo/gwttodo.nocache.js"></script>
</head>
<body>
<noscript>
<div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
Your web browser must have JavaScript enabled in order for this application to display correctly.
</div>
</noscript>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='gwttodo'>
<!-- Inherit the core Web Toolkit stuff. -->
<inherits name='com.google.gwt.user.User'/>
<inherits name='com.google.gwt.json.JSON'/>
<!-- Don't inherit any GWT styles - they're ugly! -->
<!-- <inherits name='com.google.gwt.user.theme.clean.Clean'/>-->
<!-- Other module inherits -->
<!-- Specify the app entry point class. -->
<entry-point class='com.todo.client.GwtToDo'/>
<!-- Specify the paths for translatable code -->
<source path='client'/>
<add-linker name="xs" />
</module>
package com.todo.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.RootPanel;
/**
* Entry point class
*/
public class GwtToDo implements EntryPoint {
@Override
public void onModuleLoad() {
ToDoView toDoView = new ToDoView();
new ToDoPresenter(toDoView);
RootPanel.get().add(toDoView);
}
}
package com.todo.client;
import com.google.gwt.user.client.ui.TextBox;
public class TextBoxWithPlaceholder extends TextBox {
/**
* Sets the placeholder for this textbox
*
* @param value the placeholder value
*/
public void setPlaceholder(String value) {
getElement().setAttribute("placeholder", value);
}
/**
* Gets the placeholder for this textbox
*
* @return the placeholder
*/
public String getPlaceholder() {
return getElement().getAttribute("placeholder");
}
}
package com.todo.client;
import java.util.Date;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
/**
* A cell that renders {@link ToDoItem} instances. This cell is rendered in both view and edit modes
* based on user interaction. In edit mode, browser events are handled in order to update the model
* item state.
*
* @author ceberhardt
*
*/
public class ToDoCell extends AbstractCell<ToDoItem> {
/**
* The HTML templates used to render the cell.
*/
interface Templates extends SafeHtmlTemplates {
/**
* The view-mode template
*/
@SafeHtmlTemplates.Template("<div class='{2}' data-timestamp='{3}'>" + "{0} "
+ "<label>{1}</label>" + "<button class='destroy'></a>" + "</div>")
SafeHtml view(SafeHtml checked, SafeHtml task, String done, String timestamp);
/**
* A template the renders a checked input
*/
@SafeHtmlTemplates.Template("<input class='toggle' type='checkbox' checked>")
SafeHtml inputChecked();
/**
* A template the renders an un-checked input
*/
@SafeHtmlTemplates.Template("<input class='toggle' type='checkbox'>")
SafeHtml inputClear();
/**
* The edit-mode template
*/
@SafeHtmlTemplates.Template("<div class='listItem editing'><input class='edit' value='{0}' type='text'></div>")
SafeHtml edit(String task);
}
private static Templates templates = GWT.create(Templates.class);
/**
* The item that is currently being edited
*/
private ToDoItem editingItem = null;
/**
* A flag that indicates that we are starting to edit the cell
*/
private boolean beginningEdit = false;
public ToDoCell() {
super("click", "keyup", "blur", "dblclick");
}
@Override
public void render(Context context, ToDoItem value, SafeHtmlBuilder sb) {
// render the cell in edit or view mode
if (isEditing(value)) {
SafeHtml rendered = templates.edit(value.getTitle());
sb.append(rendered);
} else {
SafeHtml rendered =
templates.view(value.isDone() ? templates.inputChecked() : templates.inputClear(),
SafeHtmlUtils.fromString(value.getTitle()), value.isDone() ? "listItem view done"
: "listItem view",
// NOTE: The addition of a timestamp here is a bit of a HACK! The problem
// is that the CellList uses a HasDataPresenter for rendering. This class
// caches the more recent rendered contents for each cell, skipping a render
// if it looks like the cell hasn't changed. However, this fails for editable cells
// that are able to change the DOM representation directly. This hack simply
// ensures that the presenter always renders the cell.
Long.toString(new Date().getTime()));
sb.append(rendered);
}
}
@Override
public boolean isEditing(Context context, Element parent, ToDoItem value) {
return isEditing(value);
}
@Override
public void onBrowserEvent(Context context, Element parent, ToDoItem value, NativeEvent event,
ValueUpdater<ToDoItem> valueUpdater) {
String type = event.getType();
if (isEditing(value)) {
// handle keyup events
if ("keyup".equals(type)) {
int keyCode = event.getKeyCode();
// handle enter key to commit the edit
if (keyCode == KeyCodes.KEY_ENTER) {
commitEdit(parent, value);
endEdit(context, parent, value);
}
// handle escape key to cancel the edit
if (keyCode == KeyCodes.KEY_ESCAPE) {
endEdit(context, parent, value);
}
}
// handle blur event
if ("blur".equals(type) && !beginningEdit) {
commitEdit(parent, value);
endEdit(context, parent, value);
}
} else {
// handle double clicks to enter edit more
if ("dblclick".equals(type)) {
beginEdit(context, parent, value);
beginningEdit = true;
InputElement input = getInputElement(parent);
input.focus();
input.select();
beginningEdit = false;
}
// when not in edit mode - handle click events on the cell
if ("click".equals(type)) {
EventTarget eventTarget = event.getEventTarget();
Element clickedElement = Element.as(eventTarget);
String tagName = clickedElement.getTagName();
// check whether the checkbox was clicked
if (tagName.equals("INPUT")) {
// if so, synchronise the model state
InputElement input = clickedElement.cast();
value.setDone(input.isChecked());
// update the 'row' style
if (input.isChecked()) {
getViewRootElement(parent).addClassName("done");
} else {
getViewRootElement(parent).removeClassName("done");
}
} else if (tagName.equals("BUTTON")) {
// if the delete anchor was clicked - delete the item
value.delete();
}
}
}
}
/**
* Commits the changes in text value to the ToDoItem
*/
private void commitEdit(Element parent, ToDoItem value) {
InputElement input = getInputElement(parent);
value.setTitle(input.getValue());
}
/**
* Begins editing the given item, rendering the cell in edit mode
*/
private void beginEdit(Context context, Element parent, ToDoItem value) {
editingItem = value;
renderCell(context, parent, value);
}
/**
* Ends editing the given item, rendering the cell in view mode
*/
private void endEdit(Context context, Element parent, ToDoItem value) {
editingItem = null;
renderCell(context, parent, value);
}
/**
* Renders the cell, replacing the contents of the parent with the newly rendered content.
*/
private void renderCell(Context context, Element parent, ToDoItem value) {
SafeHtmlBuilder sb = new SafeHtmlBuilder();
render(context, value, sb);
parent.setInnerHTML(sb.toSafeHtml().asString());
}
/**
* Gets whether the given item is being edited.
*/
private boolean isEditing(ToDoItem item) {
return editingItem == item;
}
/**
* Get the input element in edit mode.
*/
private InputElement getInputElement(Element parent) {
return parent.getFirstChild().getFirstChild().<InputElement> cast();
}
/**
* Gets the root DIV element of the view mode template.
*/
private DivElement getViewRootElement(Element parent) {
return parent.getFirstChild().<DivElement> cast();
}
}
package com.todo.client;
/**
* An individual ToDo item.
*
* @author ceberhardt
*
*/
public class ToDoItem {
private final ToDoPresenter presenter;
private String title;
private boolean done;
public ToDoItem(String text, ToDoPresenter presenter) {
this.title = text;
this.done = false;
this.presenter = presenter;
}
public ToDoItem(String title, boolean done, ToDoPresenter presenter) {
this.title = title;
this.done = done;
this.presenter = presenter;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
presenter.itemStateChanged(this);
}
public void delete() {
presenter.deleteTask(this);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
presenter.itemStateChanged(this);
}
}
package com.todo.client;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.storage.client.Storage;
import com.google.gwt.view.client.AbstractDataProvider;
import com.google.gwt.view.client.ListDataProvider;
/**
* The presenter for the ToDo application. This class is responsible for the lifecycle of the
* {@link ToDoItem} instances.
*
* @author ceberhardt
*
*/
public class ToDoPresenter {
private static final String STORAGE_KEY = "todo-gwt";
/**
* The interface that a view for this presenter must implement.
*/
public interface View {
/**
* Gets the text that the user has input for the creation of new tasks.
*/
String getTaskText();
/**
* Clears the user input field where new tasks are added.
*/
void clearTaskText();
/**
* Sets the current task statistics.
*/
void setTaskStatistics(int totalTasks, int completedTasks);
/**
* Sets the data provider that acts as a source of {@link ToDoItem} instances.
*/
void setDataProvider(AbstractDataProvider<ToDoItem> data);
/**
* Adds the handler to the events raised by the view.
*/
void addhandler(ViewEventHandler handler);
}
/**
* The interface that handles interactions from the view.
*
*/
public interface ViewEventHandler {
/**
* Invoked when a user adds a new task.
*/
void addTask();
/**
* Invoked when a user wishes to clear completed tasks.
*/
void clearCompletedTasks();
/**
* Sets the completed state of all tasks to the given state
*/
void markAllCompleted(boolean completed);
}
/**
* Handler for view events, defers to private presenter methods.
*/
private final ViewEventHandler viewHandler = new ViewEventHandler() {
@Override
public void addTask() {
ToDoPresenter.this.addTask();
}
@Override
public void clearCompletedTasks() {
ToDoPresenter.this.clearCompletedTasks();
}
@Override
public void markAllCompleted(boolean completed) {
ToDoPresenter.this.markAllCompleted(completed);
}
};
private final ListDataProvider<ToDoItem> todos = new ListDataProvider<ToDoItem>();
private final View view;
private boolean suppressStateChanged = false;
public ToDoPresenter(View view) {
this.view = view;
loadState();
view.addhandler(viewHandler);
view.setDataProvider(todos);
updateTaskStatistics();
}
/**
* Computes the tasks statistics and updates the view.
*/
private void updateTaskStatistics() {
int totalTasks = todos.getList().size();
int completeTask = 0;
for (ToDoItem task : todos.getList()) {
if (task.isDone()) {
completeTask++;
}
}
view.setTaskStatistics(totalTasks, completeTask);
}
/**
* Deletes the given task and updates statistics.
*/
protected void deleteTask(ToDoItem toDoItem) {
todos.getList().remove(toDoItem);
updateTaskStatistics();
saveState();
}
/**
* Invoked by a task when its state changes so that we can update the view statistics and persist.
*/
protected void itemStateChanged(ToDoItem toDoItem) {
if (suppressStateChanged) {
return;
}
// if the item has become empty, remove it
if (toDoItem.getTitle().trim().equals("")) {
todos.getList().remove(toDoItem);
}
updateTaskStatistics();
saveState();
}
/**
* Sets the completed state of all tasks
*/
private void markAllCompleted(boolean completed) {
// update the completed state of each item
suppressStateChanged = true;
for (ToDoItem task : todos.getList()) {
task.setDone(completed);
}
suppressStateChanged = false;
// cause the view to refresh the whole list - yes, this is a bit ugly!
List<ToDoItem> items = new ArrayList<ToDoItem>(todos.getList());
todos.getList().clear();
todos.getList().addAll(items);
updateTaskStatistics();
saveState();
}
/**
* Adds a new task based on the user input field
*/
private void addTask() {
String taskTitle = view.getTaskText().trim();
// if white-space only, do not add a todo
if (taskTitle.equals(""))
return;
ToDoItem toDoItem = new ToDoItem(taskTitle, this);
view.clearTaskText();
todos.getList().add(toDoItem);
updateTaskStatistics();
saveState();
}
/**
* Clears completed tasks and updates the view.
*/
private void clearCompletedTasks() {
Iterator<ToDoItem> iterator = todos.getList().iterator();
while (iterator.hasNext()) {
ToDoItem item = iterator.next();
if (item.isDone()) {
iterator.remove();
}
}
updateTaskStatistics();
saveState();
}
/**
* Saves the current to-do items to local storage
*/
private void saveState() {
Storage storage = Storage.getLocalStorageIfSupported();
if (storage != null) {
// JSON encode the items
JSONArray todoItems = new JSONArray();
for (int i = 0; i < todos.getList().size(); i++) {
ToDoItem toDoItem = todos.getList().get(i);
JSONObject jsonObject = new JSONObject();
jsonObject.put("task", new JSONString(toDoItem.getTitle()));
jsonObject.put("complete", JSONBoolean.getInstance(toDoItem.isDone()));
todoItems.set(i, jsonObject);
}
// save to local storage
storage.setItem(STORAGE_KEY, todoItems.toString());
}
}
private void loadState() {
Storage storage = Storage.getLocalStorageIfSupported();
if (storage != null) {
try {
// get state
String state = storage.getItem(STORAGE_KEY);
// parse the JSON array
JSONArray todoItems = JSONParser.parseStrict(state).isArray();
for (int i = 0; i < todoItems.size(); i++) {
// extract the to-do item values
JSONObject jsonObject = todoItems.get(i).isObject();
String task = jsonObject.get("task").isString().stringValue();
boolean completed = jsonObject.get("complete").isBoolean().booleanValue();
// add a new item to our list
todos.getList().add(new ToDoItem(task, completed, this));
}
} catch (Exception e) {
}
}
}
}
package com.todo.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.AbstractDataProvider;
import com.todo.client.ToDoPresenter.ViewEventHandler;
/**
* A view for the {@link ToDoPresenter}
*
*/
public class ToDoView extends Composite implements ToDoPresenter.View {
private static ToDoViewUiBinder uiBinder = GWT.create(ToDoViewUiBinder.class);
interface ToDoViewUiBinder extends UiBinder<Widget, ToDoView> {
}
@UiField
TextBoxWithPlaceholder taskText;
@UiField
Element remainingTasksCount;
@UiField
SpanElement remainingTasksLabel;
@UiField
Element mainSection;
@UiField
Element todoStatsContainer;
@UiField
SpanElement clearTasksCount;
@UiField
Button clearCompleted;
@UiField
InputElement toggleAll;
@UiField(provided = true)
CellList<ToDoItem> todoTable = new CellList<ToDoItem>(new ToDoCell());
public ToDoView() {
initWidget(uiBinder.createAndBindUi(this));
// removes the yellow highlight
todoTable.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
// add IDs to the elements that have ui:field attributes. This is required because the UiBinder
// does not permit the addition of ID attributes to elements marked with ui:field.
// *SIGH*
mainSection.setId("main");
clearCompleted.getElement().setId("clear-completed");
taskText.getElement().setId("new-todo");
todoStatsContainer.setId("footer");
toggleAll.setId("toggle-all");
}
@Override
public String getTaskText() {
return taskText.getText();
}
@Override
public void addhandler(final ViewEventHandler handler) {
// wire-up the events from the UI to the presenter.
// The TodoMVC project template has a markup / style that is not compatible with the markup
// generated by the GWT CheckBox control. For this reason, here we are using an InputElement
// directly. As a result, we handle low-level DOM events rather than the GWT higher level
// abstractions, e.g. ClickHandlers. A typical GWT application would not do this, however,
// this nicely illustrates how you can develop GWT applications
// that program directly against the DOM.
final com.google.gwt.user.client.Element clientToggleElement = toggleAll.cast();
DOM.sinkEvents(clientToggleElement, Event.ONCLICK);
DOM.setEventListener(clientToggleElement, new EventListener() {
@Override
public void onBrowserEvent(Event event) {
handler.markAllCompleted(toggleAll.isChecked());
}
});
taskText.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
handler.addTask();
}
}
});
clearCompleted.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
handler.clearCompletedTasks();
}
});
}
@Override
public void setDataProvider(AbstractDataProvider<ToDoItem> data) {
data.addDataDisplay(todoTable);
}
@Override
public void clearTaskText() {
taskText.setText("");
}
@Override
public void setTaskStatistics(int totalTasks, int completedTasks) {
int remainingTasks = totalTasks - completedTasks;
hideElement(mainSection, totalTasks == 0);
hideElement(todoStatsContainer, totalTasks == 0);
hideElement(clearCompleted.getElement(), completedTasks == 0);
remainingTasksCount.setInnerText(Integer.toString(remainingTasks));
remainingTasksLabel.setInnerText(remainingTasks > 1 || remainingTasks == 0 ? "items" : "item");
clearTasksCount.setInnerHTML(Integer.toString(completedTasks));
toggleAll.setChecked(totalTasks == completedTasks);
}
private void hideElement(Element element, boolean hide) {
if (hide) {
element.setAttribute("style", "display:none;");
} else {
element.setAttribute("style", "display:block;");
}
}
}
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:cv="urn:import:com.google.gwt.user.cellview.client"
xmlns:todo="urn:import:com.todo.client">
<g:HTMLPanel>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo:TextBoxWithPlaceholder
placeholder="What needs to be done?" ui:field="taskText">
</todo:TextBoxWithPlaceholder>
</header>
<section ui:field="mainSection">
<input ui:field="toggleAll" type="checkbox"></input>
<label for="toggle-all">Mark all as complete</label>
<div id="todo-list">
<cv:CellList ui:field="todoTable"></cv:CellList>
</div>
</section>
<footer ui:field="todoStatsContainer">
<span id="todo-count">
<strong class="number" ui:field="remainingTasksCount"></strong>
<span class="word" ui:field="remainingTasksLabel"></span>
left.
</span>
<g:Button ui:field="clearCompleted">
Clear completed (<span class="number-done" ui:field="clearTasksCount"></span>)
</g:Button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<p>Created by <a href="http://www.scottlogic.co.uk/blog/colin/">Colin Eberhardt</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</g:HTMLPanel>
</ui:UiBinder>
\ No newline at end of file
......@@ -48,4 +48,4 @@
$('#contributor-list').show().children('span').html( ret.join(', ') );
});
})();
\ No newline at end of file
})();
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment