TodoList Example

This example is built in three layers: the model, the user interface, and the data.

file: example/todo/model/TodoList.sc
// The classic TodoList example.  Unlike most examples, there's no 'view model' or coreui layer.
class TodoList {
   @Component
   class TodoItem {
      String text;
      boolean complete;

      complete =: updateRemaining();

      // Use ManualGetSet so we do not send unnecessary change events from the constructor
      @sc.obj.ManualGetSet
      TodoItem(String t, boolean c) {
          text = t;
          complete = c;
      }
   }
   ArrayList<TodoItem> todos; /* = {
      new TodoItem("Run todo sample", true),
      new TodoItem("Check me and see it stay in sync", false),
      new TodoItem("Add a new entry and press 'remove completed'", false),
   } */

   String todoText = "";

   // No need to synchronize these two because we sync todos from which they are computed
   @sc.obj.Sync(syncMode=sc.obj.SyncMode.Disabled)
   int remaining, numTodos;

   todos =: updateRemaining();

   void addTodoEntry() {
      todos.add(new TodoItem(todoText, false));
      todoText = "";
   }

   void updateRemaining() {
      int count = 0;
      if (todos != null) {
         for (TodoItem todo: todos) {
            if (!todo.complete)
               count++;
         }
      }
      remaining = count;
      numTodos = todos.size();
   }

   int getSize(List<TodoItem> list) {
      return list == null ? 0 : list.size();
   }

   void removeComplete() {
      for (int i = 0; i < todos.size(); i++) {
         TodoItem todo = todos.get(i);
         if (todo.complete) {
            todos.remove(i);

            DynUtil.dispose(todo);
            i--;
         }
      }
   }
}
Here is the user interface, in the example.todo.jsui layer. That layer extends the model layer so this file modifies the Todo type above. That happens because these files have the same name and the layers use the same package. You have access to the properties in the model layer, just like the "code behind" pattern.
file: example/todo/jsui/TodoList.schtml
<!-- This file, TodoList.schtml modifies TodoList.sc in the previous model layer -->
<html extends="EditablePage">
   <head>
      <link rel="stylesheet" type="text/css" href="todoStyle.css" />
   </head>
  <body>
     <div class="appFrame" id="appFrame">
        <h2>Todo List</h2>
        <div id="todoControl">
           <span><%= remaining %> of <%= numTodos %> to do</span>

           <!-- When the tag is clicked, clickEvent fires and calls removeComplete() -->
           [ <a href="#" clickEvent="=: removeComplete()">remove completed</a> ]
           <ul>
              <!-- Repeat li once for each element in todos, setting the var todo -->
              <li repeat=":= todos" repeatVarName="todo">
                 <!-- Set checked to the value of todo.complete and vice-versa -->
                 <input type="checkbox" checked=":=: todo.complete"/>
                 <!-- set the class to be complete-true or complete-false -->
                 <span class=':= "complete-" + todo.complete'><%= todo.text %></span>
              </li>
           </ul>
           <form submitEvent="=: addTodoEntry()">
              <input type="text" value=":=: todoText" size="45" placeholder="enter todo entry here"/>
              <input type="submit" value="Add" disabled=':= todoText.length() == 0'/>
           </form>
        </div>
     </div>
   </body>
</html>
and a separate layer to store the data:
file: example/todo/data/TodoList.sc
// Specifies the data to initialize the todo list.  
TodoList {
   todos = {
      new TodoItem("Run StrataCode todo sample", true),
      new TodoItem("Check me and see it stay in sync", false),
      new TodoItem("Add a new entry and press 'remove completed'", false)
   };
}
This example runs in several different modes, depending on the framework layers and options. With just the js.schtml layer, it runs in the browser only (see above). With both the js.schtml and jetty.schtml layers, it runs in client/server mode. With just jetty.schtml, it runs in server-only mode.

As each model entry is changed, its value is sent to the server and saved in the session using the page's scope. By default, it uses appSession scope so that two tabs in the same browser session will be synchronized (if js.sync is included).

Change the default scope using the servlet.options.windowScope layer for each tab to have its own state. Or set the scope attribute to request for all info to not use the session

This test script uses the scr format and tests the domain model for flexibility

file: example/todo/model/testTodoList.scr
cmd.pauseTime = 250;

// Run these commands in the java process - they will be sync'd to the client
cmd.targetRuntime = "java";
// Optionally - send the commands to the client - after converting to JS,
// they are eval'd using the sync framework.
//cmd.targetRuntime = "js";

TodoList {
   todos.size(); 
   assert todos.size() == 3 : "No todos";
   todos.get(1).complete = true;

   todoText = "A new entry";
   addTodoEntry();
   removeComplete();

   while (todos.size() > 0) {
      todos.get(0).complete = true;
      removeComplete();
   }

   todoText = "done";
   addTodoEntry();
   cmd.sleep(1000);
   todos.get(0).complete = true;
}
The web framework runs testX.scr automatically: scc example/todo/clientServer -tw For swing run: scc example/todo/swingmain -ts testTodoList.scr Here's the swing UI layer:
file: example/todo/swingui/TodoList.sc
TodoList extends AppFrame {
   location = new Point(300, 300);
   size = new Dimension(430, 430);

   int spad = 10;

   object mainLabel extends JLabel {
      // The getRemaining method depends on the 'complete' state of the TodoItem so this binding is refreshed
      // when completeCheckBox.selected changes by calling Bind.refreshBindings
      text := "Todo - " + remaining + " of: " + numTodos + " remaining";
      location := SwingUtil.point(xpad, ypad + baseline);
      size := preferredSize;
   }

   object nameButton extends JButton {
      location := SwingUtil.point(mainLabel.location.x + mainLabel.size.width + xpad, ypad);
      size := preferredSize;
      text = "Remove Completed";
      clickCount =: removeComplete();
   }

   double startListY := nameButton.location.y + nameButton.size.height + ypad + spad;

   class TodoComponent extends ComponentGroup {
      TodoItem todo;
      int ix;

      TodoComponent(TodoItem todo, int ix) {
         this.todo = todo;
         this.ix = ix;
      }

      double startTodoY;

      public void setIx(int ix) {
         this.ix = ix;
         startTodoY = ix == 0 ? startListY : todoList.repeatComponents.get(ix-1).startTodoY + todoTextLabel.size.height + ypad;
      }

      object completeCheckBox extends JCheckBox {
         selected :=: todo.complete;

         // To refresh the mainLabel.text property due to the getRemaining function, validate all bindings on that label
         selected =: Bind.refreshBindings(mainLabel);

         location := SwingUtil.point(xpad, startTodoY);
         size := preferredSize;
      }

      object todoTextLabel extends JLabel {
         text :=: todo.text;
         location := SwingUtil.point(completeCheckBox.location.x + completeCheckBox.size.width + xpad, startTodoY + baseline);
         size := preferredSize;
         enabled := !todo.complete;
      }
   }

   object todoList extends RepeatComponent<TodoComponent> {
      repeat := todos;

      double height = ypad;

      parentComponent = TodoList.this;

      // This will be called once for each todoItem when the RepeatComponent refreshes itself
      public TodoComponent createRepeatElement(Object todo, int ix, Object oldComp) {
         return new TodoComponent((TodoItem)todo, ix);
      }

      // Called to find the original repeat value given the component created above
      public Object getRepeatVar(TodoComponent todoComp) {
         return todoComp.todo;
      }

      public void setRepeatIndex(TodoComponent todoComp, int ix) {
         todoComp.ix = ix;
      }

      boolean refreshList() {
         boolean anyChanges = super.refreshList();

         int newHeight = ypad;
         for (int i = 0; i < repeatComponents.size(); i++) {
            java.awt.Rectangle bounds = SwingUtil.getBoundingRectangle(repeatComponents.get(i));
            newHeight += bounds.size.height + ypad;
         }
         height = newHeight;
         Bind.refreshBindings(mainLabel);

         TodoList.this.revalidate();
         TodoList.this.repaint();

         return anyChanges;
      }
   }

   double startAddY := startListY + todoList.height + ypad + spad;

   object newTodoText extends JTextField {
      location := SwingUtil.point(xpad, startAddY);
      size := SwingUtil.dimension(TodoList.this.size.width - 4*xpad - addTodo.size.width, preferredSize.height);
      text :=: todoText;

      userEnteredCount =: addTodoEntry();
   }

   object addTodo extends JButton {
      location := SwingUtil.point(newTodoText.location.x + newTodoText.size.width, startAddY);
      size := preferredSize;
      text = "Add";
      clickCount =: addTodoEntry();
      enabled := newTodoText.text.length() > 0;
   }
}