Skip to content

Commit

Permalink
Add feature to toggle classes
Browse files Browse the repository at this point in the history
Add feature to toggle classes
  • Loading branch information
ErikOnBike authored May 5, 2020
2 parents 9d93d63 + 8d9fd57 commit 4700391
Show file tree
Hide file tree
Showing 8 changed files with 1,825 additions and 1,453 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Templates look like they do in most other templating tools: rendering data by pl
The general usage is probably best described by an example (see example on [bl.ocks.org](https://bl.ocks.org/ErikOnBike/f36ce2b4c88ef525d0cfe34a766d8067)):

```HTML
<div id="person">
<div id="person" data-class-alive="{{!d.deathdate}}">
<div>
<span>{{`Name: ${d.name}`}}</span>
</div>
Expand Down Expand Up @@ -73,6 +73,7 @@ The general usage is probably best described by an example (see example on [bl.o
.render({
name: "Alan Turing",
birthdate: new Date(1912, 5, 23),
deathdate: new Date(1954, 5, 7),
honours: {
received: [
"Order of the British Empire",
Expand Down Expand Up @@ -110,7 +111,7 @@ To install via [npm](https://www.npmjs.com) use `npm install d3-template-plugin`

The following *features* are present:
* Data rendering onto attributes and text directly.
* Data can also be rendered on attributes, styles or properties indirectly (through `data-attr-<name>`, `data-style-<name>` and `data-prop-<name>`). Properties can be useful for setting the `value` or `checked` property of HTML input elements for example. The indirect rendering is especially useful for SVG because most browsers do not like 'invalid' attribute values:
* Data can also be rendered on attributes, styles, properties or class fields indirectly (through `data-attr-<name>`, `data-style-<name>`, `data-prop-<name>` and `data-class-<name>`). Properties can be useful for setting the `value` or `checked` property of HTML input elements for example. The indirect rendering is especially useful for SVG because most browsers do not like 'invalid' attribute values:

```SVG
<!-- Most browsers do not like this (invalid according to SVG spec) -->
Expand Down Expand Up @@ -201,7 +202,8 @@ If *options* is specified it should be an object containing properties describin
importAttribute: "data-import",
indirectAttributePrefix: "data-attr-",
indirectStylePrefix: "data-style-",
indirectPropertyPrefix: "data-prop-"
indirectPropertyPrefix: "data-prop-",
indirectClassPrefix: "data-class-"
}
```

Expand Down
3,107 changes: 1,681 additions & 1,426 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "d3-template-plugin",
"version": "2.0.11",
"version": "2.0.13",
"description": "Template plugin for D3js.",
"keywords": [
"d3",
Expand All @@ -27,20 +27,20 @@
"prepublishOnly": "npm run test"
},
"dependencies": {
"d3-selection": "^1",
"d3-transition": "^1",
"help": "^3",
"npm": "^6"
"d3-selection": "^1.4.1",
"d3-transition": "^1.3.2",
"help": "3",
"npm": "^6.14.2"
},
"devDependencies": {
"d3-color": "^1",
"d3-interpolate": "^1",
"eslint": "^6",
"jsdom": "^15",
"nyc": "^14",
"rollup": "^1",
"rollup-plugin-uglify": "^6",
"tape": "^4",
"lodash": ">=4.17.13"
"d3-color": "^1.4.0",
"d3-interpolate": "^1.4.0",
"eslint": "6",
"jsdom": "16",
"lodash": "4",
"nyc": "15",
"rollup": "1",
"rollup-plugin-uglify": "6",
"tape": "4"
}
}
24 changes: 24 additions & 0 deletions src/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ AttributeRenderer.prototype.render = function(templateElement, transition) {
}
};

// ---- ClassRenderer class ----
// I am a Renderer and I render class fields inside template elements.
//
// Implementation: I render class fields of HTML or SVG elements/tags.
export function ClassRenderer(element, dataFunction, className) {
if(dataFunction.isTweenFunction) {
throw new Error("Tween-function not allowed for class fields");
}
Renderer.call(this, element, dataFunction);
this.className = className;
}
ClassRenderer.prototype = Object.create(Renderer.prototype);
ClassRenderer.prototype.constructor = ClassRenderer;

// ---- ClassRenderer instance methods ----
// Render the class field onto the template element specified, based on the receiver's data
// The (optional) transition is ignored since a class name is either present or not.
ClassRenderer.prototype.render = function(templateElement /*, transition */) {

// Set or clear the name in the class field
var element = this.resolveToRenderElement(templateElement);
element.classed(this.className, this.getDataFunction());
};

// ---- StyleRenderer class ----
// I am a Renderer and I render styles inside template elements.
//
Expand Down
20 changes: 13 additions & 7 deletions src/template-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,11 @@ GroupingNode.prototype.joinData = function(rootElement) {
});

// Join data on newly created childs
this.childNodes.forEach(function(childNode) {
childNode.joinData(updatedElements);
});
if(updatedElements.size() > 0) {
this.childNodes.forEach(function(childNode) {
childNode.joinData(updatedElements);
});
}

return this;
};
Expand Down Expand Up @@ -304,9 +306,11 @@ GroupingNode.prototype.applyEventHandlers = function(elements) {
// Render data onto the child nodes of the template
GroupingNode.prototype.renderNodes = function(templateElements, transition) {
var childElements = templateElements.selectAll(ALL_DIRECT_CHILDREN);
this.childNodes.forEach(function(childNode) {
childNode.render(childElements, transition);
});
if(childElements.size() > 0) {
this.childNodes.forEach(function(childNode) {
childNode.render(childElements, transition);
});
}

return this;
};
Expand Down Expand Up @@ -434,7 +438,9 @@ ImportNode.prototype.joinData = function(rootElement) {
});

// Join data on the newly created structure
templateNode.joinData(importedElement);
if(importedElement.size() > 0) {
templateNode.joinData(importedElement);
}
});

return this;
Expand Down
14 changes: 11 additions & 3 deletions src/template.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { select } from "d3-selection";
import { TemplateNode, RepeatNode, IfNode, WithNode, ImportNode } from "./template-node";
import { AttributeRenderer, StyleRenderer, PropertyRenderer, TextRenderer } from "./renderer";
import { AttributeRenderer, StyleRenderer, PropertyRenderer, ClassRenderer, TextRenderer } from "./renderer";

// ---- Defaults ----
var defaults = {
Expand All @@ -10,7 +10,8 @@ var defaults = {
importAttribute: "data-import",
indirectAttributePrefix: "data-attr-",
indirectStylePrefix: "data-style-",
indirectPropertyPrefix: "data-prop-"
indirectPropertyPrefix: "data-prop-",
indirectClassPrefix: "data-class-"
};

// ---- Fix for IE (small kneefall because difficult to fix otherwise) ----
Expand Down Expand Up @@ -108,6 +109,7 @@ export function template(selection, options) {
options.indirectAttributeRegEx = new RegExp("^" + options.indirectAttributePrefix + "(.*)$", REG_EX_FLAG);
options.indirectStyleRegEx = new RegExp("^" + options.indirectStylePrefix + "(.*)$", REG_EX_FLAG);
options.indirectPropertyRegEx = new RegExp("^" + options.indirectPropertyPrefix + "(.*)$", REG_EX_FLAG);
options.indirectClassRegEx = new RegExp("^" + options.indirectClassPrefix + "(.*)$", REG_EX_FLAG);

// Create templates from the current selection
selection.each(function() {
Expand Down Expand Up @@ -214,7 +216,7 @@ TemplateParser.createDataFunction = function(expression) {
var isTweenFunction = false;
if(expression.startsWith("tween:")) {
isTweenFunction = true;
expression = expression.slice(6).trim();
expression = expression.slice(6);
}

// Create data function
Expand Down Expand Up @@ -406,6 +408,12 @@ TemplateParser.prototype.parseAttributeRenderers = function(element, templateNod
if(nameMatch) {
renderAttributeName = nameMatch[1]; // Render the referenced property
renderClass = PropertyRenderer;
} else {
nameMatch = renderAttributeName.match(options.indirectClassRegEx);
if(nameMatch) {
renderAttributeName = nameMatch[1]; // Render the referenced class name
renderClass = ClassRenderer;
}
}
}
}
Expand Down
58 changes: 58 additions & 0 deletions test/render-attr-class-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var tape = require("tape");
var jsdom = require("./jsdom");
var d3 = Object.assign({}, require("d3-selection"), require("../"));

tape("render class names: data-class with literal value", function(test) {
global.document = jsdom("<div class='fixed' data-class-checked='{{d}}'></div>");
var selection = d3.select("div").template();
selection.render(true);
test.equal(selection.classed("checked"), true, "Class checked is rendered on element through true");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
selection.render(false);
test.equal(selection.classed("checked"), false, "Class checked is no longer present through false");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
selection.render(1);
test.equal(selection.classed("checked"), true, "Class checked is rendered on element through 1");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
selection.render(0);
test.equal(selection.classed("checked"), false, "Class checked is no longer present through 0");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
selection.render("hello");
test.equal(selection.classed("checked"), true, "Class checked is rendered on element through \"hello\"");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
selection.render("");
test.equal(selection.classed("checked"), false, "Class checked is no longer present through \"\"");
test.equal(selection.classed("fixed"), true, "Class fixed is still present");
test.end();
});

tape("render class names: data-class with object value", function(test) {
global.document = jsdom("<div><span data-class-red='{{d.red}}' data-class-green='{{d.green}}' data-class-blue='{{d.blue}}'></span></div>");
var selection = d3.select("div").template();
selection.render({ red: true, green: "", blue: [] });
test.equal(selection.select("span").classed("red"), true, "Class ref is rendered on element");
test.equal(selection.select("span").classed("green"), false, "Class green is not rendered on element");
test.equal(selection.select("span").classed("blue"), true, "Class blue is rendered on element");
test.end();
});

tape("render class names: data-class with object value and logic", function(test) {
global.document = jsdom("<div><span class='fixed' data-class-red='{{d.red >= 50}}' data-class-green='{{d.green >= 50}}' data-class-blue='{{d.blue >= 50}}'></span></div>");
var selection = d3.select("div").template();
selection.render({ red: 0, green: 100, blue: 100 });
test.equal(selection.select("span").classed("red"), false, "Class ref is not rendered on element - 1");
test.equal(selection.select("span").classed("green"), true, "Class green is rendered on element - 1");
test.equal(selection.select("span").classed("blue"), true, "Class blue is rendered on element - 1");
test.equal(selection.select("span").classed("fixed"), true, "Class fixed is still present");
selection.render({ red: 50, green: 50, blue: 50 });
test.equal(selection.select("span").classed("red"), true, "Class ref is rendered on element - 2");
test.equal(selection.select("span").classed("green"), true, "Class green is rendered on element - 2");
test.equal(selection.select("span").classed("blue"), true, "Class blue is rendered on element - 2");
test.equal(selection.select("span").classed("fixed"), true, "Class fixed is still present");
selection.render({ red: 0, green: 0, blue: 0 });
test.equal(selection.select("span").classed("red"), false, "Class ref is not rendered on element - 2");
test.equal(selection.select("span").classed("green"), false, "Class green is not rendered on element - 2");
test.equal(selection.select("span").classed("blue"), false, "Class blue is not rendered on element - 2");
test.equal(selection.select("span").classed("fixed"), true, "Class fixed is still present");
test.end();
});
19 changes: 19 additions & 0 deletions test/render-group-import-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ tape("render group-import: import combined with if", function(test) {
test.end();
});

tape("render group-import: if with import nested", function(test) {
global.document = jsdom("<body><div id='component' data-if='{{d.test}}'><div><span class='value'>{{d.value}}</span><div data-import='{{\"#component\"}}' data-with='{{d.child}}'></div></div></div></body>");
var component = d3.select("#component").template();
component.render({
test: true,
value: 'hello',
child: {
test: true,
value: 'world',
child: {
test: false,
value: 'xxx'
}
}
});
test.equal(d3.selectAll(".value").size(), 2, "Two elements created");
test.end();
});

tape("render group-import: import with child present", function(test) {
global.document = jsdom('<div data-import="{{d}}"><span>I should not be here</span></div>');
var selection = d3.select("div");
Expand Down

0 comments on commit 4700391

Please sign in to comment.