Browse Source

First case of bidirectional binding, only for checkboxes.

Herbert Vojčík 12 years ago
parent
commit
8d96a3cb3e
6 changed files with 465 additions and 17 deletions
  1. 17 5
      js/Trapped-Demo.deploy.js
  2. 20 8
      js/Trapped-Demo.js
  3. 147 0
      js/Trapped-Frontend.deploy.js
  4. 197 0
      js/Trapped-Frontend.js
  5. 13 4
      st/Trapped-Demo.st
  6. 71 0
      st/Trapped-Frontend.st

+ 17 - 5
js/Trapped-Demo.deploy.js

@@ -12,7 +12,7 @@ smalltalk.send(self,"_dispatcher_",[smalltalk.send((smalltalk.TrappedDumbDispatc
 obj=smalltalk.HashedCollection._fromPairs_([smalltalk.send("title","__minus_gt",["To-Do List"])]);
 smalltalk.send(self,"_model_",[obj]);
 smalltalk.send((function(){
-smalltalk.send(obj,"_at_put_",["items",["hello", "world"]]);
+smalltalk.send(obj,"_at_put_",["items",[[true, "hello"], [false, "world"]]]);
 return smalltalk.send(smalltalk.send(self,"_dispatcher",[]),"_changed_",[[]]);
 }),"_valueWithTimeout_",[(2000)]);
 return self}
@@ -28,20 +28,32 @@ smalltalk.method({
 selector: "renderOn:",
 fn: function (html){
 var self=this;
-var $1,$2;
+var $1,$2,$3,$4;
 smalltalk.send(smalltalk.send(html,"_h2",[]),"_trapShow_",[["title"]]);
 smalltalk.send(smalltalk.send(html,"_div",[]),"_trap_toggle_ifNotPresent_",[["items"],(function(){
 smalltalk.send(smalltalk.send(html,"_p",[]),"_with_",[(function(){
 smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[smalltalk.symbolFor("size")]]);
 return smalltalk.send(html,"_with_",[" item(s)."]);
 })]);
-return smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
+smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
 return smalltalk.send(smalltalk.send(html,"_ul",[]),"_trapIter_tag_do_",[[],smalltalk.symbolFor("li"),(function(each){
+smalltalk.send(smalltalk.send(html,"_root",[]),"_empty",[]);
 $1=smalltalk.send(html,"_input",[]);
 smalltalk.send($1,"_type_",["checkbox"]);
-$2=smalltalk.send($1,"_at_put_",["checked",true]);
+$2=smalltalk.send($1,"_trapBind_",[[(1)]]);
 $2;
-return smalltalk.send(html,"_with_",[each]);
+return smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[(2)]]);
+})]);
+})]);
+smalltalk.send(smalltalk.send(html,"_p",[]),"_with_",["... and again, to see the bidirectional binding:"]);
+return smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
+return smalltalk.send(smalltalk.send(html,"_ul",[]),"_trapIter_tag_do_",[[],smalltalk.symbolFor("li"),(function(each){
+smalltalk.send(smalltalk.send(html,"_root",[]),"_empty",[]);
+$3=smalltalk.send(html,"_input",[]);
+smalltalk.send($3,"_type_",["checkbox"]);
+$4=smalltalk.send($3,"_trapBind_",[[(1)]]);
+$4;
+return smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[(2)]]);
 })]);
 })]);
 }),(function(){

+ 20 - 8
js/Trapped-Demo.js

@@ -13,12 +13,12 @@ smalltalk.send(self,"_dispatcher_",[smalltalk.send((smalltalk.TrappedDumbDispatc
 obj=smalltalk.HashedCollection._fromPairs_([smalltalk.send("title","__minus_gt",["To-Do List"])]);
 smalltalk.send(self,"_model_",[obj]);
 smalltalk.send((function(){
-smalltalk.send(obj,"_at_put_",["items",["hello", "world"]]);
+smalltalk.send(obj,"_at_put_",["items",[[true, "hello"], [false, "world"]]]);
 return smalltalk.send(smalltalk.send(self,"_dispatcher",[]),"_changed_",[[]]);
 }),"_valueWithTimeout_",[(2000)]);
 return self},
 args: [],
-source: "initialize\x0a\x09| obj |\x0a\x09super initialize.\x0a    self dispatcher: TrappedDumbDispatcher new.\x0a    obj := #{'title' -> 'To-Do List'}.\x0a    self model: obj.\x0a    [ obj at: 'items' put: #('hello' 'world'). self dispatcher changed: #() ] valueWithTimeout: 2000\x0a",
+source: "initialize\x0a\x09| obj |\x0a\x09super initialize.\x0a    self dispatcher: TrappedDumbDispatcher new.\x0a    obj := #{'title' -> 'To-Do List'}.\x0a    self model: obj.\x0a    [ obj at: 'items' put: #(#(true 'hello') #(false 'world')). self dispatcher changed: #() ] valueWithTimeout: 2000\x0a",
 messageSends: ["initialize", "dispatcher:", "new", "->", "model:", "valueWithTimeout:", "at:put:", "changed:", "dispatcher"],
 referencedClasses: ["TrappedDumbDispatcher"]
 }),
@@ -34,20 +34,32 @@ selector: "renderOn:",
 category: 'rendering',
 fn: function (html){
 var self=this;
-var $1,$2;
+var $1,$2,$3,$4;
 smalltalk.send(smalltalk.send(html,"_h2",[]),"_trapShow_",[["title"]]);
 smalltalk.send(smalltalk.send(html,"_div",[]),"_trap_toggle_ifNotPresent_",[["items"],(function(){
 smalltalk.send(smalltalk.send(html,"_p",[]),"_with_",[(function(){
 smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[smalltalk.symbolFor("size")]]);
 return smalltalk.send(html,"_with_",[" item(s)."]);
 })]);
-return smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
+smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
 return smalltalk.send(smalltalk.send(html,"_ul",[]),"_trapIter_tag_do_",[[],smalltalk.symbolFor("li"),(function(each){
+smalltalk.send(smalltalk.send(html,"_root",[]),"_empty",[]);
 $1=smalltalk.send(html,"_input",[]);
 smalltalk.send($1,"_type_",["checkbox"]);
-$2=smalltalk.send($1,"_at_put_",["checked",true]);
+$2=smalltalk.send($1,"_trapBind_",[[(1)]]);
 $2;
-return smalltalk.send(html,"_with_",[each]);
+return smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[(2)]]);
+})]);
+})]);
+smalltalk.send(smalltalk.send(html,"_p",[]),"_with_",["... and again, to see the bidirectional binding:"]);
+return smalltalk.send(smalltalk.send(html,"_form",[]),"_with_",[(function(){
+return smalltalk.send(smalltalk.send(html,"_ul",[]),"_trapIter_tag_do_",[[],smalltalk.symbolFor("li"),(function(each){
+smalltalk.send(smalltalk.send(html,"_root",[]),"_empty",[]);
+$3=smalltalk.send(html,"_input",[]);
+smalltalk.send($3,"_type_",["checkbox"]);
+$4=smalltalk.send($3,"_trapBind_",[[(1)]]);
+$4;
+return smalltalk.send(smalltalk.send(html,"_span",[]),"_trapShow_",[[(2)]]);
 })]);
 })]);
 }),(function(){
@@ -55,8 +67,8 @@ return smalltalk.send(html,"_with_",["Loading ..."]);
 })]);
 return self},
 args: ["html"],
-source: "renderOn: html\x0a\x09html h2 trapShow: #('title').\x0a    html div trap: #('items') toggle: [\x0a        html p with: [ html span trapShow: #(#size). html with: ' item(s).' ].\x0a\x09\x09html form with: [ html ul trapIter: #() tag: #li do: [ :each |\x0a            html input\x0a                type: 'checkbox';\x0a                at: 'checked' put: true.\x0a            html with: each\x0a        ]]\x0a    ] ifNotPresent: [ html with: 'Loading ...' ]",
-messageSends: ["trapShow:", "h2", "trap:toggle:ifNotPresent:", "with:", "span", "p", "trapIter:tag:do:", "type:", "input", "at:put:", "ul", "form", "div"],
+source: "renderOn: html\x0a\x09html h2 trapShow: #('title').\x0a    html div trap: #('items') toggle: [\x0a        html p with: [ html span trapShow: #(#size). html with: ' item(s).' ].\x0a\x09\x09html form with: [ html ul trapIter: #() tag: #li do: [ :each |\x0a            html root empty.\x0a            html input\x0a                type: 'checkbox';\x0a                trapBind: #(1).\x0a            html span trapShow: #(2).\x0a        ]].\x0a        html p with: '... and again, to see the bidirectional binding:'.\x0a\x09\x09html form with: [ html ul trapIter: #() tag: #li do: [ :each |\x0a            html root empty.\x0a            html input\x0a                type: 'checkbox';\x0a                trapBind: #(1).\x0a            html span trapShow: #(2).\x0a        ]].\x0a    ] ifNotPresent: [ html with: 'Loading ...' ]",
+messageSends: ["trapShow:", "h2", "trap:toggle:ifNotPresent:", "with:", "span", "p", "trapIter:tag:do:", "empty", "root", "type:", "input", "trapBind:", "ul", "form", "div"],
 referencedClasses: []
 }),
 smalltalk.AppView);

+ 147 - 0
js/Trapped-Frontend.deploy.js

@@ -1,4 +1,104 @@
 smalltalk.addPackage('Trapped-Frontend', {});
+smalltalk.addClass('TrappedBinder', smalltalk.Object, ['brush'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_brush_",
+smalltalk.method({
+selector: "brush:",
+fn: function (aTagBrush){
+var self=this;
+self["@brush"]=aTagBrush;
+return self}
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_installFor_",
+smalltalk.method({
+selector: "installFor:",
+fn: function (path){
+var self=this;
+smalltalk.send(self["@brush"],"_trap_read_",[path,smalltalk.send(self,"_showBlock",[])]);
+return self}
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_prim_",
+smalltalk.method({
+selector: "prim:",
+fn: function (anObject){
+var self=this;
+return anObject.valueOf();
+;
+return self}
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_showBlock",
+smalltalk.method({
+selector: "showBlock",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_subclassResponsibility",[]);
+return self}
+}),
+smalltalk.TrappedBinder);
+
+
+
+smalltalk.addClass('TrappedAttrBinder', smalltalk.TrappedBinder, ['attr'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_attr_",
+smalltalk.method({
+selector: "attr:",
+fn: function (aString){
+var self=this;
+self["@attr"]=aString;
+return self}
+}),
+smalltalk.TrappedAttrBinder);
+
+smalltalk.addMethod(
+"_installFor_",
+smalltalk.method({
+selector: "installFor:",
+fn: function (path){
+var self=this;
+smalltalk.send(self,"_installFor_",[path],smalltalk.TrappedBinder);
+smalltalk.send(path,"_trapDescend_",[(function(){
+var actual;
+actual=smalltalk.send((smalltalk.Trapped || Trapped),"_path",[]);
+actual;
+return smalltalk.send(self["@brush"],"_onChange_",[(function(){
+return smalltalk.send(actual,"_trapDescend_",[(function(){
+return smalltalk.send(self["@brush"],"_trap_modify_",[[],(function(){
+return smalltalk.send(smalltalk.send(smalltalk.send(self["@brush"],"_asJQuery",[]),"_attr_",["checked"]),"_notNil",[]);
+})]);
+})]);
+})]);
+})]);
+return self}
+}),
+smalltalk.TrappedAttrBinder);
+
+smalltalk.addMethod(
+"_showBlock",
+smalltalk.method({
+selector: "showBlock",
+fn: function (){
+var self=this;
+var $1;
+$1=(function(model){
+return smalltalk.send(smalltalk.send(self["@brush"],"_asJQuery",[]),"_attr_put_",[self["@attr"],smalltalk.send(self,"_prim_",[model])]);
+});
+return $1;
+}
+}),
+smalltalk.TrappedAttrBinder);
+
+
+
 smalltalk.addClass('TrappedDispatcher', smalltalk.KeyedPubSubBase, [], 'Trapped-Frontend');
 smalltalk.addMethod(
 "_subscriptionKey_block_",
@@ -248,6 +348,23 @@ smalltalk.TrappedSingleton.klass);
 
 
 smalltalk.addClass('Trapped', smalltalk.TrappedSingleton, ['registry'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_binder_",
+smalltalk.method({
+selector: "binder:",
+fn: function (aTagBrush){
+var self=this;
+var $2,$3,$1;
+$2=smalltalk.send((smalltalk.TrappedAttrBinder || TrappedAttrBinder),"_new",[]);
+smalltalk.send($2,"_attr_",["checked"]);
+smalltalk.send($2,"_brush_",[aTagBrush]);
+$3=smalltalk.send($2,"_yourself",[]);
+$1=$3;
+return $1;
+}
+}),
+smalltalk.Trapped);
+
 smalltalk.addMethod(
 "_byName_",
 smalltalk.method({
@@ -466,6 +583,25 @@ return self}
 }),
 smalltalk.Array);
 
+smalltalk.addMethod(
+"_trap_modify_",
+smalltalk.method({
+selector: "trap:modify:",
+fn: function (path,aBlock){
+var self=this;
+smalltalk.send(path,"_trapDescend_",[(function(){
+var actual;
+var model;
+actual=smalltalk.send((smalltalk.Trapped || Trapped),"_path",[]);
+actual;
+model=smalltalk.send(smalltalk.send((smalltalk.Trapped || Trapped),"_current",[]),"_byName_",[smalltalk.send(actual,"_first",[])]);
+model;
+return smalltalk.send(model,"_modify_do_",[smalltalk.send(actual,"_allButFirst",[]),aBlock]);
+})]);
+return self}
+}),
+smalltalk.TagBrush);
+
 smalltalk.addMethod(
 "_trap_read_",
 smalltalk.method({
@@ -539,6 +675,17 @@ return self}
 }),
 smalltalk.TagBrush);
 
+smalltalk.addMethod(
+"_trapBind_",
+smalltalk.method({
+selector: "trapBind:",
+fn: function (path){
+var self=this;
+smalltalk.send(smalltalk.send(smalltalk.send((smalltalk.Trapped || Trapped),"_current",[]),"_binder_",[self]),"_installFor_",[path]);
+return self}
+}),
+smalltalk.TagBrush);
+
 smalltalk.addMethod(
 "_trapIter_tag_do_",
 smalltalk.method({

+ 197 - 0
js/Trapped-Frontend.js

@@ -1,4 +1,139 @@
 smalltalk.addPackage('Trapped-Frontend', {});
+smalltalk.addClass('TrappedBinder', smalltalk.Object, ['brush'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_brush_",
+smalltalk.method({
+selector: "brush:",
+category: 'accessing',
+fn: function (aTagBrush){
+var self=this;
+self["@brush"]=aTagBrush;
+return self},
+args: ["aTagBrush"],
+source: "brush: aTagBrush\x0a\x09brush := aTagBrush",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_installFor_",
+smalltalk.method({
+selector: "installFor:",
+category: 'action',
+fn: function (path){
+var self=this;
+smalltalk.send(self["@brush"],"_trap_read_",[path,smalltalk.send(self,"_showBlock",[])]);
+return self},
+args: ["path"],
+source: "installFor: path\x0a\x09brush trap: path read: self showBlock",
+messageSends: ["trap:read:", "showBlock"],
+referencedClasses: []
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_prim_",
+smalltalk.method({
+selector: "prim:",
+category: 'converting',
+fn: function (anObject){
+var self=this;
+return anObject.valueOf();
+;
+return self},
+args: ["anObject"],
+source: "prim: anObject\x0a\x09<return anObject.valueOf()>",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.TrappedBinder);
+
+smalltalk.addMethod(
+"_showBlock",
+smalltalk.method({
+selector: "showBlock",
+category: 'action',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_subclassResponsibility",[]);
+return self},
+args: [],
+source: "showBlock\x0a\x09self subclassResponsibility",
+messageSends: ["subclassResponsibility"],
+referencedClasses: []
+}),
+smalltalk.TrappedBinder);
+
+
+
+smalltalk.addClass('TrappedAttrBinder', smalltalk.TrappedBinder, ['attr'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_attr_",
+smalltalk.method({
+selector: "attr:",
+category: 'accessing',
+fn: function (aString){
+var self=this;
+self["@attr"]=aString;
+return self},
+args: ["aString"],
+source: "attr: aString\x0a\x09attr := aString",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.TrappedAttrBinder);
+
+smalltalk.addMethod(
+"_installFor_",
+smalltalk.method({
+selector: "installFor:",
+category: 'action',
+fn: function (path){
+var self=this;
+smalltalk.send(self,"_installFor_",[path],smalltalk.TrappedBinder);
+smalltalk.send(path,"_trapDescend_",[(function(){
+var actual;
+actual=smalltalk.send((smalltalk.Trapped || Trapped),"_path",[]);
+actual;
+return smalltalk.send(self["@brush"],"_onChange_",[(function(){
+return smalltalk.send(actual,"_trapDescend_",[(function(){
+return smalltalk.send(self["@brush"],"_trap_modify_",[[],(function(){
+return smalltalk.send(smalltalk.send(smalltalk.send(self["@brush"],"_asJQuery",[]),"_attr_",["checked"]),"_notNil",[]);
+})]);
+})]);
+})]);
+})]);
+return self},
+args: ["path"],
+source: "installFor: path\x0a\x09super installFor: path.\x0a    path trapDescend: [ | actual |\x0a        actual := Trapped path.\x0a\x09    brush onChange: [ actual trapDescend: [ brush trap: #() modify: [\x0a            (brush asJQuery attr: 'checked') notNil\x0a        ]]]\x0a    ]",
+messageSends: ["installFor:", "trapDescend:", "path", "onChange:", "trap:modify:", "notNil", "attr:", "asJQuery"],
+referencedClasses: ["Trapped"]
+}),
+smalltalk.TrappedAttrBinder);
+
+smalltalk.addMethod(
+"_showBlock",
+smalltalk.method({
+selector: "showBlock",
+category: 'action',
+fn: function (){
+var self=this;
+var $1;
+$1=(function(model){
+return smalltalk.send(smalltalk.send(self["@brush"],"_asJQuery",[]),"_attr_put_",[self["@attr"],smalltalk.send(self,"_prim_",[model])]);
+});
+return $1;
+},
+args: [],
+source: "showBlock\x0a\x09^[ :model | brush asJQuery attr: attr put: (self prim: model)  ]",
+messageSends: ["attr:put:", "prim:", "asJQuery"],
+referencedClasses: []
+}),
+smalltalk.TrappedAttrBinder);
+
+
+
 smalltalk.addClass('TrappedDispatcher', smalltalk.KeyedPubSubBase, [], 'Trapped-Frontend');
 smalltalk.TrappedDispatcher.comment="I am base class for change event dispatchers.\x0aI manage changed path - action block subscriptions.\x0aThese subscription are instances of TrappedSubscription\x0a\x0aMy subclasses need to provide implementation for:\x0a\x09add:\x0a    do:\x0a    clean\x0a    (optionally) run\x0a"
 smalltalk.addMethod(
@@ -338,6 +473,28 @@ smalltalk.TrappedSingleton.klass);
 
 
 smalltalk.addClass('Trapped', smalltalk.TrappedSingleton, ['registry'], 'Trapped-Frontend');
+smalltalk.addMethod(
+"_binder_",
+smalltalk.method({
+selector: "binder:",
+category: 'binders',
+fn: function (aTagBrush){
+var self=this;
+var $2,$3,$1;
+$2=smalltalk.send((smalltalk.TrappedAttrBinder || TrappedAttrBinder),"_new",[]);
+smalltalk.send($2,"_attr_",["checked"]);
+smalltalk.send($2,"_brush_",[aTagBrush]);
+$3=smalltalk.send($2,"_yourself",[]);
+$1=$3;
+return $1;
+},
+args: ["aTagBrush"],
+source: "binder: aTagBrush\x0a    \x22Prototype; will select based on tag etc.\x22\x0a    ^TrappedAttrBinder new attr: 'checked'; brush: aTagBrush; yourself",
+messageSends: ["attr:", "new", "brush:", "yourself"],
+referencedClasses: ["TrappedAttrBinder"]
+}),
+smalltalk.Trapped);
+
 smalltalk.addMethod(
 "_byName_",
 smalltalk.method({
@@ -621,6 +778,30 @@ referencedClasses: ["TrappedPathStack"]
 }),
 smalltalk.Array);
 
+smalltalk.addMethod(
+"_trap_modify_",
+smalltalk.method({
+selector: "trap:modify:",
+category: '*Trapped-Frontend',
+fn: function (path,aBlock){
+var self=this;
+smalltalk.send(path,"_trapDescend_",[(function(){
+var actual;
+var model;
+actual=smalltalk.send((smalltalk.Trapped || Trapped),"_path",[]);
+actual;
+model=smalltalk.send(smalltalk.send((smalltalk.Trapped || Trapped),"_current",[]),"_byName_",[smalltalk.send(actual,"_first",[])]);
+model;
+return smalltalk.send(model,"_modify_do_",[smalltalk.send(actual,"_allButFirst",[]),aBlock]);
+})]);
+return self},
+args: ["path", "aBlock"],
+source: "trap: path modify: aBlock\x0a\x09path trapDescend: [ | actual model |\x0a    \x09actual := Trapped path.\x0a        model := Trapped current byName: actual first.\x0a        model modify: actual allButFirst do: aBlock\x0a    ]",
+messageSends: ["trapDescend:", "path", "byName:", "first", "current", "modify:do:", "allButFirst"],
+referencedClasses: ["Trapped"]
+}),
+smalltalk.TagBrush);
+
 smalltalk.addMethod(
 "_trap_read_",
 smalltalk.method({
@@ -709,6 +890,22 @@ referencedClasses: []
 }),
 smalltalk.TagBrush);
 
+smalltalk.addMethod(
+"_trapBind_",
+smalltalk.method({
+selector: "trapBind:",
+category: '*Trapped-Frontend',
+fn: function (path){
+var self=this;
+smalltalk.send(smalltalk.send(smalltalk.send((smalltalk.Trapped || Trapped),"_current",[]),"_binder_",[self]),"_installFor_",[path]);
+return self},
+args: ["path"],
+source: "trapBind: path\x0a\x09(Trapped current binder: self) installFor: path",
+messageSends: ["installFor:", "binder:", "current"],
+referencedClasses: ["Trapped"]
+}),
+smalltalk.TagBrush);
+
 smalltalk.addMethod(
 "_trapIter_tag_do_",
 smalltalk.method({

+ 13 - 4
st/Trapped-Demo.st

@@ -11,7 +11,7 @@ initialize
     self dispatcher: TrappedDumbDispatcher new.
     obj := #{'title' -> 'To-Do List'}.
     self model: obj.
-    [ obj at: 'items' put: #('hello' 'world'). self dispatcher changed: #() ] valueWithTimeout: 2000
+    [ obj at: 'items' put: #(#(true 'hello') #(false 'world')). self dispatcher changed: #() ] valueWithTimeout: 2000
 ! !
 
 Widget subclass: #AppView
@@ -25,11 +25,20 @@ renderOn: html
     html div trap: #('items') toggle: [
         html p with: [ html span trapShow: #(#size). html with: ' item(s).' ].
 		html form with: [ html ul trapIter: #() tag: #li do: [ :each |
+            html root empty.
             html input
                 type: 'checkbox';
-                at: 'checked' put: true.
-            html with: each
-        ]]
+                trapBind: #(1).
+            html span trapShow: #(2).
+        ]].
+        html p with: '... and again, to see the bidirectional binding:'.
+		html form with: [ html ul trapIter: #() tag: #li do: [ :each |
+            html root empty.
+            html input
+                type: 'checkbox';
+                trapBind: #(1).
+            html span trapShow: #(2).
+        ]].
     ] ifNotPresent: [ html with: 'Loading ...' ]
 ! !
 

+ 71 - 0
st/Trapped-Frontend.st

@@ -1,4 +1,56 @@
 Smalltalk current createPackage: 'Trapped-Frontend' properties: #{}!
+Object subclass: #TrappedBinder
+	instanceVariableNames: 'brush'
+	package: 'Trapped-Frontend'!
+
+!TrappedBinder methodsFor: 'accessing'!
+
+brush: aTagBrush
+	brush := aTagBrush
+! !
+
+!TrappedBinder methodsFor: 'action'!
+
+installFor: path
+	brush trap: path read: self showBlock
+!
+
+showBlock
+	self subclassResponsibility
+! !
+
+!TrappedBinder methodsFor: 'converting'!
+
+prim: anObject
+	<return anObject.valueOf()>
+! !
+
+TrappedBinder subclass: #TrappedAttrBinder
+	instanceVariableNames: 'attr'
+	package: 'Trapped-Frontend'!
+
+!TrappedAttrBinder methodsFor: 'accessing'!
+
+attr: aString
+	attr := aString
+! !
+
+!TrappedAttrBinder methodsFor: 'action'!
+
+installFor: path
+	super installFor: path.
+    path trapDescend: [ | actual |
+        actual := Trapped path.
+	    brush onChange: [ actual trapDescend: [ brush trap: #() modify: [
+            (brush asJQuery attr: 'checked') notNil
+        ]]]
+    ]
+!
+
+showBlock
+	^[ :model | brush asJQuery attr: attr put: (self prim: model)  ]
+! !
+
 KeyedPubSubBase subclass: #TrappedDispatcher
 	instanceVariableNames: ''
 	package: 'Trapped-Frontend'!
@@ -186,6 +238,13 @@ start
     ]
 ! !
 
+!Trapped methodsFor: 'binders'!
+
+binder: aTagBrush
+    "Prototype; will select based on tag etc."
+    ^TrappedAttrBinder new attr: 'checked'; brush: aTagBrush; yourself
+! !
+
 !Trapped methodsFor: 'initialization'!
 
 initialize
@@ -261,6 +320,14 @@ trapDescend: aBlock
 
 !TagBrush methodsFor: '*Trapped-Frontend'!
 
+trap: path modify: aBlock
+	path trapDescend: [ | actual model |
+    	actual := Trapped path.
+        model := Trapped current byName: actual first.
+        model modify: actual allButFirst do: aBlock
+    ]
+!
+
 trap: path read: aBlock
 	path trapDescend: [ | actual model |
     	actual := Trapped path.
@@ -288,6 +355,10 @@ trap: path toggle: aBlock ifNotPresent: anotherBlock
     ]
 !
 
+trapBind: path
+	(Trapped current binder: self) installFor: path
+!
+
 trapIter: path tag: aSymbol do: aBlock
     self trap: path read: [ :model :html |
         html root empty.