Skip to content

Commit

Permalink
Recognize attribute bindings in templates. (#905)
Browse files Browse the repository at this point in the history
Fixes #625.
  • Loading branch information
denis-anisimov committed Jun 2, 2016
1 parent c6cdde7 commit 0403809
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 44 deletions.
24 changes: 24 additions & 0 deletions hummingbird-client/src/main/java/com/vaadin/client/WidgetUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.gwt.core.client.JavaScriptObject;

import elemental.client.Browser;
import elemental.dom.Element;
import elemental.html.AnchorElement;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
Expand Down Expand Up @@ -113,6 +114,29 @@ public static String toPrettyJson(JsonValue json) {
}
}

/**
* Updates the {@code attribute} value for the {@code element} to the given
* {@code value}.
* <p>
* If {@code value} is {@code null} then {@code attribute} is removed,
* otherwise {@code value.toString()} is set as its value.
*
* @param element
* the DOM element owning attribute
* @param attribute
* the attribute to update
* @param value
* the value to update
*/
public static void updateAttribute(Element element, String attribute,
Object value) {
if (value == null) {
element.removeAttribute(attribute);
} else {
element.setAttribute(attribute, value.toString());
}
}

// JsJsonValue.toJson with indentation set to 4
private static native String toPrettyJsonJsni(JsonValue value)
/*-{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,7 @@ private void updateStyleProperty(MapProperty mapProperty, Element element) {

private void updateAttribute(MapProperty mapProperty, Element element) {
String name = mapProperty.getName();

if (mapProperty.hasValue()) {
element.setAttribute(name, String.valueOf(mapProperty.getValue()));
} else {
element.removeAttribute(name);
}
WidgetUtil.updateAttribute(element, name, mapProperty.getValue());
}

private EventRemover bindSynchronizedPropertyEvents(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import com.vaadin.client.hummingbird.nodefeature.NodeList;
import com.vaadin.client.hummingbird.util.NativeFunction;
import com.vaadin.hummingbird.shared.NodeFeatures;
import com.vaadin.hummingbird.template.StaticBindingValueProvider;

import elemental.client.Browser;
import elemental.dom.Element;
Expand Down Expand Up @@ -76,16 +75,7 @@ protected void bind(StateNode stateNode, Element element, int templateId,

bindClassNames(stateNode, templateNode, element);

JsonObject attributes = templateNode.getAttributes();
if (attributes != null) {
for (String name : attributes.keys()) {
Binding binding = WidgetUtil.crazyJsCast(attributes.get(name));
// Nothing to "bind" yet with only static bindings
assert binding.getType()
.equals(StaticBindingValueProvider.TYPE);
element.setAttribute(name, getStaticBindingValue(binding));
}
}
bindAttributes(stateNode, templateNode, element);

JsArray<Double> children = templateNode.getChildrenIds();
if (children != null) {
Expand Down Expand Up @@ -125,6 +115,13 @@ protected void bind(StateNode stateNode, Element element, int templateId,
}
}

private void bindAttributes(StateNode stateNode,
ElementTemplateNode templateNode, Element element) {
bind(stateNode, templateNode.getAttributes(),
(name, value) -> WidgetUtil.updateAttribute(element, name,
value.orElse(null)));
}

private void registerEventHandlers(StateNode stateNode,
ElementTemplateNode templateNode, Element element) {
JsonObject eventHandlers = templateNode.getEventHandlers();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

public class GwtTemplateBinderTest extends ClientEngineTestBase {

private static final String MODEL_KEY = "key";
private Registry registry;
private StateTree tree;
private StateNode stateNode;
Expand Down Expand Up @@ -147,10 +148,10 @@ public void testPropertyBindingTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addProperty("prop", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, "key"));
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createElement(templateNode);

Reactive.flush();
Expand All @@ -162,15 +163,15 @@ public void testUpdatePropertyBindingTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addProperty("prop", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, "key"));
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createElement(templateNode);

Reactive.flush();

map.getProperty("key").setValue("bar");
map.getProperty(MODEL_KEY).setValue("bar");

Reactive.flush();

Expand All @@ -181,10 +182,10 @@ public void testUnregister_propeprtyBindingUpdateIsNotDone() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addProperty("prop", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, "key"));
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createElement(templateNode);

assertEquals(null, WidgetUtil.getJsProperty(domNode, "prop"));
Expand All @@ -199,24 +200,88 @@ public void testPropertyBindingNoValueTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addProperty("prop", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, "key"));
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));
Node domNode = createElement(templateNode);

Reactive.flush();

assertEquals(null, WidgetUtil.getJsProperty(domNode, "prop"));
}

public void testAttributeBindingTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addAttribute("attr", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty(MODEL_KEY).setValue("foo");
Element domNode = createElement(templateNode);

Reactive.flush();

assertEquals("foo", domNode.getAttribute("attr"));
}

public void testUpdateAttributeBindingTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addAttribute("attr", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty(MODEL_KEY).setValue("foo");
Element domNode = createElement(templateNode);

Reactive.flush();

map.getProperty(MODEL_KEY).setValue("bar");

Reactive.flush();

assertEquals("bar", domNode.getAttribute("attr"));
}

public void testUnregister_sttributeBindingUpdateIsNotDone() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addAttribute("attr", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty(MODEL_KEY).setValue("foo");
Element domNode = createElement(templateNode);

assertEquals(null, domNode.getAttribute("attr"));

stateNode.unregister();

Reactive.flush();
assertEquals(null, domNode.getAttribute("attr"));
}

public void testAttributeBindingNoValueTemplate() {
TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");
templateNode.addAttribute("attr", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));
Element domNode = createElement(templateNode);

Reactive.flush();

assertEquals(null, domNode.getAttribute("attr"));
}

public void testClassNameBinding() {
MapProperty property = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP)
.getProperty("key");
.getProperty(MODEL_KEY);

TestElementTemplateNode templateNode = TestElementTemplateNode
.create("div");

templateNode.addClassName("static", "true");
templateNode.addClassName("dynamic", TestBinding
.createBinding(ModelValueBindingProvider.TYPE, "key"));
.createBinding(ModelValueBindingProvider.TYPE, MODEL_KEY));

Element element = createElement(templateNode);

Expand Down Expand Up @@ -253,9 +318,9 @@ public void testClassNameBinding() {

public void testTextValueTemplate() {
TestTextTemplate templateNode = TestTextTemplate
.create(TestBinding.createTextValueBinding("key"));
.create(TestBinding.createTextValueBinding(MODEL_KEY));
NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createText(templateNode);

Reactive.flush();
Expand All @@ -265,14 +330,14 @@ public void testTextValueTemplate() {

public void testUpdateTextValueTemplate() {
TestTextTemplate templateNode = TestTextTemplate
.create(TestBinding.createTextValueBinding("key"));
.create(TestBinding.createTextValueBinding(MODEL_KEY));
NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createText(templateNode);

Reactive.flush();

map.getProperty("key").setValue("bar");
map.getProperty(MODEL_KEY).setValue("bar");

Reactive.flush();

Expand All @@ -281,9 +346,9 @@ public void testUpdateTextValueTemplate() {

public void testUnregister_textBinsingUpdateIsNotDone() {
TestTextTemplate templateNode = TestTextTemplate
.create(TestBinding.createTextValueBinding("key"));
.create(TestBinding.createTextValueBinding(MODEL_KEY));
NodeMap map = stateNode.getMap(NodeFeatures.TEMPLATE_MODELMAP);
map.getProperty("key").setValue("foo");
map.getProperty(MODEL_KEY).setValue("foo");
Node domNode = createText(templateNode);

assertEquals("", domNode.getTextContent());
Expand All @@ -296,7 +361,7 @@ public void testUnregister_textBinsingUpdateIsNotDone() {

public void testTextNoValueTemplate() {
TestTextTemplate templateNode = TestTextTemplate
.create(TestBinding.createTextValueBinding("key"));
.create(TestBinding.createTextValueBinding(MODEL_KEY));
Node domNode = createText(templateNode);

Reactive.flush();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public default void addProperty(String name, TestBinding binding) {
doGetProperties().put(name, binding.asJson());
}

@JsOverlay
public default void addAttribute(String name, TestBinding binding) {
doGetAttributes().put(name, binding.asJson());
}

@JsOverlay
public default void addClassName(String name, String staticValue) {
doGetClassNames().put(name,
Expand Down Expand Up @@ -102,12 +107,18 @@ public default JsonObject doGetClassNames() {
}

@JsOverlay
public default void addAttribute(String name, String staticValue) {
public default JsonObject doGetAttributes() {
JsonObject attributes = getAttributes();
if (attributes == null) {
attributes = Json.createObject();
setAttributes(attributes);
}
attributes.put(name, TestBinding.createStatic(staticValue).asJson());
return attributes;
}

@JsOverlay
public default void addAttribute(String name, String staticValue) {
doGetAttributes().put(name,
TestBinding.createStatic(staticValue).asJson());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,26 @@ private void setBinding(Attribute attribute, ElementTemplateBuilder builder,
String className = key.substring("class.".length());

String classAttribute = element.attr("class");
if (classAttribute != null
&& Stream.of(classAttribute.split("\\s+"))
.anyMatch(className::equals)) {
throw new TemplateParseException(
"The class attribute can't contain '" + className
+ "' when there's also a binding for [class."
+ className + "]");
if (Stream.of(classAttribute.split("\\s+"))
.anyMatch(className::equals)) {
throw new TemplateParseException(String.format(
"The class attribute can't contain '%s' "
+ "when there's also a binding for [class.%s]",
className, className));
}

builder.setClassName(className, binding);
} else if (key.startsWith("attr.")) {
String attributeName = key.substring("attr.".length());

if (element.hasAttr(attributeName)) {
throw new TemplateParseException(String.format(
"The '%s' attribute can't present when there "
+ "is also a binding for [attr.%s]",
attributeName, attributeName));
}

builder.setAttribute(attributeName, binding);
} else {
builder.setProperty(key, binding);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,40 @@ public void parseTemplateIncorrectProperty() {
parse("<input [value='foo'></input>");
}

@Test
public void parseTemplateAttribute() {
ElementTemplateNode rootNode = (ElementTemplateNode) parse(
"<input [attr.value]='foo'></input>");

Assert.assertEquals("input", rootNode.getTag());

Assert.assertEquals(1, rootNode.getAttributeNames().count());
Assert.assertEquals(0, rootNode.getClassNames().count());
Assert.assertEquals(0, rootNode.getPropertyNames().count());

Optional<BindingValueProvider> binding = rootNode
.getAttributeBinding("value");
Assert.assertTrue(binding.isPresent());

StateNode node = new StateNode(ModelMap.class);

Assert.assertNull(binding.get().getValue(node));

node.getFeature(ModelMap.class).setValue("foo", "bar");

Assert.assertEquals("bar", binding.get().getValue(node));
}

@Test(expected = TemplateParseException.class)
public void parseTemplateIncorrectAttribute() {
parse("<input [attr.value]='foo' value='bar'>");
}

@Test(expected = TemplateParseException.class)
public void parseTemplateIncorrectEmptyAttribute() {
parse("<input [attr.value]='foo' value>");
}

@Test
public void parseClassName() {
ElementTemplateNode rootNode = (ElementTemplateNode) parse(
Expand Down
Loading

0 comments on commit 0403809

Please sign in to comment.