Support for callbacks from knockout to Java model
authorJaroslav Tulach <jaroslav.tulach@apidesign.org>
Mon, 25 Mar 2013 11:50:36 +0100
branchmodel
changeset 879af170d42b5b3
parent 878 ecbd252fd3a7
child 884 89891834511c
Support for callbacks from knockout to Java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnFunction.java
javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java
javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java
javaquery/demo-calculator/src/main/java/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calc.java
javaquery/demo-calculator/src/main/resources/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calculator.xhtml
     1.1 --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java	Fri Mar 22 17:03:32 2013 +0100
     1.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java	Mon Mar 25 11:50:36 2013 +0100
     1.3 @@ -43,7 +43,8 @@
     1.4      }
     1.5      
     1.6      @JavaScriptBody(args = { "object", "property" },
     1.7 -        body = "var p = object[property]; return p ? p : null;"
     1.8 +        body = "if (property === null) return object;\n"
     1.9 +        + "var p = object[property]; return p ? p : null;"
    1.10      )
    1.11      private static Object getProperty(Object object, String property) {
    1.12          return null;
     2.1 --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java	Fri Mar 22 17:03:32 2013 +0100
     2.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java	Mon Mar 25 11:50:36 2013 +0100
     2.3 @@ -29,12 +29,13 @@
     2.4  public class Knockout {
     2.5      /** used by tests */
     2.6      static Knockout next;
     2.7 -    
     2.8 +
     2.9      Knockout() {
    2.10      }
    2.11      
    2.12      public static <M> Knockout applyBindings(
    2.13 -        Class<M> modelClass, M model, String[] propsGettersAndSetters
    2.14 +        Class<M> modelClass, M model, String[] propsGettersAndSetters,
    2.15 +        String[] methodsAndSignatures
    2.16      ) {
    2.17          Knockout bindings = next;
    2.18          next = null;
    2.19 @@ -53,6 +54,11 @@
    2.20                  throw new IllegalStateException(ex.getMessage());
    2.21              }
    2.22          }
    2.23 +        for (int i = 0; i < methodsAndSignatures.length; i += 2) {
    2.24 +            expose(
    2.25 +                bindings, model, methodsAndSignatures[i], methodsAndSignatures[i + 1]
    2.26 +            );
    2.27 +        }
    2.28          applyBindings(bindings);
    2.29          return bindings;
    2.30      }
    2.31 @@ -87,6 +93,14 @@
    2.32          Object bindings, Object model, String prop, String getter, String setter, boolean primitive
    2.33      ) {
    2.34      }
    2.35 +
    2.36 +    @JavaScriptBody(args = { "bindings", "model", "prop", "sig" }, body = 
    2.37 +        "bindings[prop] = function(data, ev) { model[sig](data, ev); };"
    2.38 +    )
    2.39 +    private static void expose(
    2.40 +        Object bindings, Object model, String prop, String sig
    2.41 +    ) {
    2.42 +    }
    2.43      
    2.44      @JavaScriptBody(args = { "bindings" }, body = "ko.applyBindings(bindings);")
    2.45      private static void applyBindings(Object bindings) {}
     3.1 --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java	Fri Mar 22 17:03:32 2013 +0100
     3.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java	Mon Mar 25 11:50:36 2013 +0100
     3.3 @@ -55,6 +55,7 @@
     3.4  import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
     3.5  import org.apidesign.bck2brwsr.htmlpage.api.Model;
     3.6  import org.apidesign.bck2brwsr.htmlpage.api.On;
     3.7 +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
     3.8  import org.apidesign.bck2brwsr.htmlpage.api.Page;
     3.9  import org.apidesign.bck2brwsr.htmlpage.api.Property;
    3.10  import org.openide.util.lookup.ServiceProvider;
    3.11 @@ -68,6 +69,7 @@
    3.12  @SupportedAnnotationTypes({
    3.13      "org.apidesign.bck2brwsr.htmlpage.api.Model",
    3.14      "org.apidesign.bck2brwsr.htmlpage.api.Page",
    3.15 +    "org.apidesign.bck2brwsr.htmlpage.api.OnFunction",
    3.16      "org.apidesign.bck2brwsr.htmlpage.api.On"
    3.17  })
    3.18  public final class PageProcessor extends AbstractProcessor {
    3.19 @@ -164,6 +166,7 @@
    3.20          try {
    3.21              StringWriter body = new StringWriter();
    3.22              List<String> propsGetSet = new ArrayList<>();
    3.23 +            List<String> functions = new ArrayList<>();
    3.24              Map<String, Collection<String>> propsDeps = new HashMap<>();
    3.25              if (!generateComputedProperties(body, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) {
    3.26                  ok = false;
    3.27 @@ -171,6 +174,9 @@
    3.28              if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps)) {
    3.29                  ok = false;
    3.30              }
    3.31 +            if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
    3.32 +                ok = false;
    3.33 +            }
    3.34              
    3.35              FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e);
    3.36              w = new OutputStreamWriter(java.openOutputStream());
    3.37 @@ -208,6 +214,13 @@
    3.38                          }
    3.39                          sep = ",\n";
    3.40                      }
    3.41 +                    w.write("\n  }, new String[] {\n");
    3.42 +                    sep = "";
    3.43 +                    for (String n : functions) {
    3.44 +                        w.write(sep);
    3.45 +                        w.write(n);
    3.46 +                        sep = ",\n";
    3.47 +                    }
    3.48                      w.write("\n  });\n  return this;\n}\n");
    3.49  
    3.50                      w.write("public void triggerEvent(Element e, OnEvent ev) {\n");
    3.51 @@ -275,46 +288,7 @@
    3.52                              continue;
    3.53                          }
    3.54                          ExecutableElement ee = (ExecutableElement)method;
    3.55 -                        StringBuilder params = new StringBuilder();
    3.56 -                        {
    3.57 -                            boolean first = true;
    3.58 -                            for (VariableElement ve : ee.getParameters()) {
    3.59 -                                if (!first) {
    3.60 -                                    params.append(", ");
    3.61 -                                }
    3.62 -                                first = false;
    3.63 -                                if (ve.asType() == stringType) {
    3.64 -                                    if (ve.getSimpleName().contentEquals("id")) {
    3.65 -                                        params.append('"').append(id).append('"');
    3.66 -                                        continue;
    3.67 -                                    }
    3.68 -                                    params.append("org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toString(ev, \"");
    3.69 -                                    params.append(ve.getSimpleName().toString());
    3.70 -                                    params.append("\")");
    3.71 -                                    continue;
    3.72 -                                }
    3.73 -                                if (processingEnv.getTypeUtils().getPrimitiveType(TypeKind.DOUBLE) == ve.asType()) {
    3.74 -                                    params.append("org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toDouble(ev, \"");
    3.75 -                                    params.append(ve.getSimpleName().toString());
    3.76 -                                    params.append("\")");
    3.77 -                                    continue;
    3.78 -                                }
    3.79 -                                String rn = ve.asType().toString();
    3.80 -                                int last = rn.lastIndexOf('.');
    3.81 -                                if (last >= 0) {
    3.82 -                                    rn = rn.substring(last + 1);
    3.83 -                                }
    3.84 -                                if (rn.equals(className)) {
    3.85 -                                    params.append(className).append(".this");
    3.86 -                                    continue;
    3.87 -                                }
    3.88 -                                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
    3.89 -                                    "@On method can only accept String named 'id' or " + className + " arguments",
    3.90 -                                    ee
    3.91 -                                );
    3.92 -                                return false;
    3.93 -                            }
    3.94 -                        }
    3.95 +                        CharSequence params = wrapParams(ee, id, className, "ev", null);
    3.96                          if (!ee.getModifiers().contains(Modifier.STATIC)) {
    3.97                              processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee);
    3.98                              ok = false;
    3.99 @@ -659,4 +633,105 @@
   3.100              e = e.getEnclosingElement();
   3.101          }
   3.102      }
   3.103 +
   3.104 +    private boolean generateFunctions(
   3.105 +        Element clazz, StringWriter body, String className, 
   3.106 +        List<? extends Element> enclosedElements, List<String> functions
   3.107 +    ) {
   3.108 +        for (Element m : enclosedElements) {
   3.109 +            if (m.getKind() != ElementKind.METHOD) {
   3.110 +                continue;
   3.111 +            }
   3.112 +            ExecutableElement e = (ExecutableElement)m;
   3.113 +            OnFunction onF = e.getAnnotation(OnFunction.class);
   3.114 +            if (onF == null) {
   3.115 +                continue;
   3.116 +            }
   3.117 +            if (!e.getModifiers().contains(Modifier.STATIC)) {
   3.118 +                processingEnv.getMessager().printMessage(
   3.119 +                    Diagnostic.Kind.ERROR, "@OnFunction method needs to be static", e
   3.120 +                );
   3.121 +                return false;
   3.122 +            }
   3.123 +            if (e.getModifiers().contains(Modifier.PRIVATE)) {
   3.124 +                processingEnv.getMessager().printMessage(
   3.125 +                    Diagnostic.Kind.ERROR, "@OnFunction method cannot be private", e
   3.126 +                );
   3.127 +                return false;
   3.128 +            }
   3.129 +            if (e.getReturnType().getKind() != TypeKind.VOID) {
   3.130 +                processingEnv.getMessager().printMessage(
   3.131 +                    Diagnostic.Kind.ERROR, "@OnFunction method should return void", e
   3.132 +                );
   3.133 +                return false;
   3.134 +            }
   3.135 +            String n = e.getSimpleName().toString();
   3.136 +            body.append("private void ").append(n).append("(Object data, Object ev) {\n");
   3.137 +            body.append("  ").append(clazz.getSimpleName()).append(".").append(n).append("(");
   3.138 +            body.append(wrapParams(e, null, className, "ev", "data"));
   3.139 +            body.append(");\n");
   3.140 +            body.append("}\n");
   3.141 +            
   3.142 +            functions.add('\"' + n + '\"');
   3.143 +            functions.add('\"' + n + "__VLjava_lang_Object_2Ljava_lang_Object_2" + '\"');
   3.144 +        }
   3.145 +        return true;
   3.146 +    }
   3.147 +
   3.148 +    private CharSequence wrapParams(
   3.149 +        ExecutableElement ee, String id, String className, String evName, String dataName
   3.150 +    ) {
   3.151 +        TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
   3.152 +        StringBuilder params = new StringBuilder();
   3.153 +        boolean first = true;
   3.154 +        for (VariableElement ve : ee.getParameters()) {
   3.155 +            if (!first) {
   3.156 +                params.append(", ");
   3.157 +            }
   3.158 +            first = false;
   3.159 +            String toCall = null;
   3.160 +            if (ve.asType() == stringType) {
   3.161 +                if (ve.getSimpleName().contentEquals("id")) {
   3.162 +                    params.append('"').append(id).append('"');
   3.163 +                    continue;
   3.164 +                }
   3.165 +                toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toString";
   3.166 +            }
   3.167 +            if (ve.asType().getKind() == TypeKind.DOUBLE) {
   3.168 +                toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toDouble";
   3.169 +            }
   3.170 +            if (ve.asType().getKind() == TypeKind.INT) {
   3.171 +                toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toInt";
   3.172 +            }
   3.173 +
   3.174 +            if (toCall != null) {
   3.175 +                params.append(toCall).append('(');
   3.176 +                if (dataName != null && ve.getSimpleName().contentEquals("data")) {
   3.177 +                    params.append(dataName);
   3.178 +                    params.append(", null");
   3.179 +                } else {
   3.180 +                    params.append(evName);
   3.181 +                    params.append(", \"");
   3.182 +                    params.append(ve.getSimpleName().toString());
   3.183 +                    params.append("\"");
   3.184 +                }
   3.185 +                params.append(")");
   3.186 +                continue;
   3.187 +            }
   3.188 +            String rn = ve.asType().toString();
   3.189 +            int last = rn.lastIndexOf('.');
   3.190 +            if (last >= 0) {
   3.191 +                rn = rn.substring(last + 1);
   3.192 +            }
   3.193 +            if (rn.equals(className)) {
   3.194 +                params.append(className).append(".this");
   3.195 +                continue;
   3.196 +            }
   3.197 +            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   3.198 +                "@On method can only accept String named 'id' or " + className + " arguments",
   3.199 +                ee
   3.200 +            );
   3.201 +        }
   3.202 +        return params;
   3.203 +    }
   3.204  }
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnFunction.java	Mon Mar 25 11:50:36 2013 +0100
     4.3 @@ -0,0 +1,34 @@
     4.4 +/**
     4.5 + * Back 2 Browser Bytecode Translator
     4.6 + * Copyright (C) 2012 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4.7 + *
     4.8 + * This program is free software: you can redistribute it and/or modify
     4.9 + * it under the terms of the GNU General Public License as published by
    4.10 + * the Free Software Foundation, version 2 of the License.
    4.11 + *
    4.12 + * This program is distributed in the hope that it will be useful,
    4.13 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    4.14 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    4.15 + * GNU General Public License for more details.
    4.16 + *
    4.17 + * You should have received a copy of the GNU General Public License
    4.18 + * along with this program. Look for COPYING file in the top folder.
    4.19 + * If not, see http://opensource.org/licenses/GPL-2.0.
    4.20 + */
    4.21 +package org.apidesign.bck2brwsr.htmlpage.api;
    4.22 +
    4.23 +import java.lang.annotation.ElementType;
    4.24 +import java.lang.annotation.Retention;
    4.25 +import java.lang.annotation.RetentionPolicy;
    4.26 +import java.lang.annotation.Target;
    4.27 +
    4.28 +/** Methods in class annotated by {@link Model} or {@link Page} can be 
    4.29 + * annotated by this annotation to signal that they should be available
    4.30 + * as functions to users of the model classes.
    4.31 + *
    4.32 + * @author Jaroslav Tulach <jtulach@netbeans.org>
    4.33 + */
    4.34 +@Target(ElementType.METHOD)
    4.35 +@Retention(RetentionPolicy.SOURCE)
    4.36 +public @interface OnFunction {
    4.37 +}
     5.1 --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java	Fri Mar 22 17:03:32 2013 +0100
     5.2 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java	Mon Mar 25 11:50:36 2013 +0100
     5.3 @@ -21,6 +21,7 @@
     5.4  import org.apidesign.bck2brwsr.core.JavaScriptBody;
     5.5  import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
     5.6  import org.apidesign.bck2brwsr.htmlpage.api.OnEvent;
     5.7 +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
     5.8  import org.apidesign.bck2brwsr.htmlpage.api.Page;
     5.9  import org.apidesign.bck2brwsr.htmlpage.api.Property;
    5.10  import org.apidesign.bck2brwsr.vmtest.BrwsrTest;
    5.11 @@ -34,7 +35,8 @@
    5.12   */
    5.13  @Page(xhtml="Knockout.xhtml", className="KnockoutModel", properties={
    5.14      @Property(name="name", type=String.class),
    5.15 -    @Property(name="results", type=String.class, array = true)
    5.16 +    @Property(name="results", type=String.class, array = true),
    5.17 +    @Property(name="callbackCount", type=int.class)
    5.18  }) 
    5.19  public class KnockoutTest {
    5.20      
    5.21 @@ -55,7 +57,7 @@
    5.22      
    5.23      @HtmlFragment(
    5.24          "<ul id='ul' data-bind='foreach: results'>\n"
    5.25 -        + "  <li><b data-bind='text: $data'></b></li>\n"
    5.26 +        + "  <li data-bind='text: $data, click: $root.call'/>\n"
    5.27          + "</ul>\n"
    5.28      )
    5.29      @BrwsrTest public void displayContentOfArray() {
    5.30 @@ -66,10 +68,15 @@
    5.31          int cnt = countChildren("ul");
    5.32          assert cnt == 1 : "One child, but was " + cnt;
    5.33          
    5.34 -        m.getResults().add("hello");
    5.35 +        m.getResults().add("Hi");
    5.36  
    5.37          cnt = countChildren("ul");
    5.38          assert cnt == 2 : "Two children now, but was " + cnt;
    5.39 +        
    5.40 +        triggerChildClick("ul", 1);
    5.41 +        
    5.42 +        assert 1 == m.getCallbackCount() : "One callback " + m.getCallbackCount();
    5.43 +        assert "Hi".equals(m.getName()) : "We got callback from 2nd child " + m.getName();
    5.44      }
    5.45      
    5.46      @HtmlFragment(
    5.47 @@ -91,6 +98,12 @@
    5.48          assert cnt == 2 : "Two children now, but was " + cnt;
    5.49      }
    5.50      
    5.51 +    @OnFunction
    5.52 +    static void call(KnockoutModel m, String data) {
    5.53 +        m.setName(data);
    5.54 +        m.setCallbackCount(m.getCallbackCount() + 1);
    5.55 +    }
    5.56 +    
    5.57      @ComputedProperty
    5.58      static String helloMessage(String name) {
    5.59          return "Hello " + name + "!";
    5.60 @@ -112,4 +125,12 @@
    5.61          + "return e.children.length;\n "
    5.62      )
    5.63      private static native int countChildren(String id);
    5.64 +
    5.65 +    @JavaScriptBody(args = { "id", "pos" }, body = 
    5.66 +          "var e = window.document.getElementById(id);\n "
    5.67 +        + "var ev = window.document.createEvent('MouseEvents');\n "
    5.68 +        + "ev.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);\n "
    5.69 +        + "e.children[pos].dispatchEvent(ev);\n "
    5.70 +    )
    5.71 +    private static native void triggerChildClick(String id, int pos);
    5.72  }
     6.1 --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java	Fri Mar 22 17:03:32 2013 +0100
     6.2 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java	Mon Mar 25 11:50:36 2013 +0100
     6.3 @@ -23,6 +23,7 @@
     6.4  import java.util.List;
     6.5  import java.util.ListIterator;
     6.6  import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
     6.7 +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
     6.8  import org.apidesign.bck2brwsr.htmlpage.api.Page;
     6.9  import org.apidesign.bck2brwsr.htmlpage.api.Property;
    6.10  import static org.testng.Assert.*;
    6.11 @@ -168,6 +169,10 @@
    6.12          }
    6.13      }
    6.14      
    6.15 +    @OnFunction 
    6.16 +    static void doSomething() {
    6.17 +    }
    6.18 +    
    6.19      @ComputedProperty
    6.20      static int powerValue(int value) {
    6.21          return value * value;
     7.1 --- a/javaquery/demo-calculator/src/main/java/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calc.java	Fri Mar 22 17:03:32 2013 +0100
     7.2 +++ b/javaquery/demo-calculator/src/main/java/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calc.java	Mon Mar 25 11:50:36 2013 +0100
     7.3 @@ -17,9 +17,11 @@
     7.4   */
     7.5  package org.apidesign.bck2brwsr.demo.calc.staticcompilation;
     7.6  
     7.7 +import java.util.List;
     7.8  import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
     7.9  import org.apidesign.bck2brwsr.htmlpage.api.On;
    7.10  import static org.apidesign.bck2brwsr.htmlpage.api.OnEvent.*;
    7.11 +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
    7.12  import org.apidesign.bck2brwsr.htmlpage.api.Page;
    7.13  import org.apidesign.bck2brwsr.htmlpage.api.Property;
    7.14  
    7.15 @@ -38,7 +40,7 @@
    7.16  })
    7.17  public class Calc {
    7.18      static {
    7.19 -        new Calculator().applyBindings();
    7.20 +        new Calculator().applyBindings().setOperation("plus");
    7.21      }
    7.22      
    7.23      @On(event = CLICK, id="clear")
    7.24 @@ -76,6 +78,16 @@
    7.25          c.setMemory(0);
    7.26      }
    7.27      
    7.28 +    @OnFunction
    7.29 +    static void recoverMemory(Calculator c, double data) {
    7.30 +        c.setDisplay(data);
    7.31 +    }
    7.32 +
    7.33 +    @OnFunction
    7.34 +    static void removeMemory(Calculator c, double data) {
    7.35 +        c.getHistory().remove(data);
    7.36 +    }
    7.37 +    
    7.38      private static double compute(String op, double memory, double display) {
    7.39          switch (op) {
    7.40              case "plus": return memory + display;
    7.41 @@ -112,4 +124,9 @@
    7.42          }
    7.43          return "Attempt to compute " + memory + " " + operation + " " + display + " = " + compute(operation, memory, display);
    7.44      }
    7.45 +    
    7.46 +    @ComputedProperty
    7.47 +    static boolean emptyHistory(List<?> history) {
    7.48 +        return history.isEmpty();
    7.49 +    }
    7.50  }
     8.1 --- a/javaquery/demo-calculator/src/main/resources/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calculator.xhtml	Fri Mar 22 17:03:32 2013 +0100
     8.2 +++ b/javaquery/demo-calculator/src/main/resources/org/apidesign/bck2brwsr/demo/calc/staticcompilation/Calculator.xhtml	Mon Mar 25 11:50:36 2013 +0100
     8.3 @@ -79,8 +79,13 @@
     8.4          
     8.5          <h4>Previous Results</h4>
     8.6          
     8.7 +        <div data-bind="if: emptyHistory">No results yet.</div>
     8.8          <ul data-bind="foreach: history">
     8.9 -            <li data-bind="text: $data"/>
    8.10 +            <li>
    8.11 +                <span data-bind="text: $data"/> -
    8.12 +                <a href="#" data-bind="click: $root.recoverMemory">Use</a>
    8.13 +                <a href="#" data-bind="click: $root.removeMemory">Remove</a>
    8.14 +            </li>
    8.15          </ul>
    8.16          
    8.17          <div data-bind="text: displayPreview"></div>