d3-view

CircleCI Dependency Status devDependency Status

Coverage

This is a d3 plugin for building interactive web interfaces. It provides data-reactive components with a simple and flexible API.

  • Modern javascript
  • Minimal footprint - use only what you need
  • Built on top of d3

Contents

Installing

If you use NPM, npm install d3-view. Otherwise, download the latest release. You can also load directly from giottojs.org, as a standalone library or unpkg. AMD, CommonJS, and vanilla environments are supported. In vanilla, a d3 global is exported. Try d3-view in your browser.

<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-collection.v1.min.js"></script>
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
<script src="https://d3js.org/d3-ease.v1.min.js"></script>
<script src="https://d3js.org/d3-selection.v1.min.js"></script>
<script src="https://d3js.org/d3-timer.v1.min.js"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-transition.v1.min.js"></script>
<script src="https://giottojs.org/latest/d3-let.min.js"></script>
<script src="https://giottojs.org/latest/d3-view.min.js"></script>
<script>

var vm = d3.view();
...
vm.mount("#my-element");

</script>

Getting Started

d3.view is a d3 plugin for building data driven web interfaces. It is not a framework as such, but you can easily build one on top of it.

Importantly, this library does not make any choice for you, it is build on top of the modular d3 library following very similar design patterns.

Create a view-model object

To create a view object for you application, invoke the d3.view function

var vm = d3.view({
    model: {...},
    components: {...},
    directives: {...}
});

You can create more than one view:

var vm2 = d3.view({
    model: {...},
    components: {...},
    directives: {...}
});

All properties in the input object are optionals and are used to initialised the view with custom data (model), components and directives.

Lets consider one view for the sake of this introduction. The vm obtained by the `view function is a view-model object which can be mounted into the DOM via the view mount function.

Expressions

The text we put inside directive's values are called binding expressions. In d3-view, a binding expression consists of a single JavaScript expression but not operations. The difference between expressions and operations is akin to the difference between a cell in an Excel spreadsheet vs. a proper JavaScript program.

Valid expression are:

"The sun"               //  literal
theme                   //  An identifier (a property of a model)
dosomething()           //  A function
[theme, number]         //  Arrays of identifiers
x ? "Hi" : "goodbye"    //  Conditionals

and complex combinations of the above

user.groups().join(", ")
[theme, user.groups(), "Hi"]

View

View API

With the exception of the mount and use methods, the view API is available once the view has been mounted to an HTML element, i.e. once the mount method has been called.

view.model

The model bound to the view, the combo gives the name to the object, the view-model object.

view.parent

The parent of a view, always undefined, a view is always the root element of a view mounted DOM.

view.el

Root HTMLElement of the view.

view.createElement(tag)

Create a new HTML Element with the given tag. Return a [d3.selection][] of the new element.

view.mount(element)

Mount a view model into the HTML element. The view only affect element and its children. This method can be called once only for a given view model.

view.use(plugin)

Install a plugin into the view model. This method can be called several times with as many plugins as one needs, however it can be called only before the view is mounted into an element.

Model

At the core of the library we have the Hierarchical Reactive Model. The Model comparable to angular scope but its implementation is different.

  • Hierarchical: a root model is associated with a d3-view object.

  • Reactive:

A model can be associated with more than one element, new children model are created for elements that needs a new model. For example, a component that specify the model object during initialisation, creates its own model, a child model of the model associated with its parent element.

Model API

# model.parent

Get the ancestor of the model if it exists. It it does not exist, this is the root model.

# model.$get(attribute)

Get an attribute value from the model, traversing the tree. If the attribute is not available in the model, it will recursively retrieve it from its parent.

# model.$set(attribute, value)

Set an attribute value from in the model, traversing the tree.

# model.$on(attribute, callback)

Add callback to a model reactive attribute. The callback is invoked when the attribute change value only. It is possible to pass the callback only, in which case it is triggered when any of the model own attributes change.

# model.$child(intials)

Crate a child model with prototypical inheritance from the model.

# model.$new(intials)

Create a child model, with no inheritance from the parent (an isolated model).

Directives

Directives are special attributes with the d3- prefix. Directive attribute values are expected to be binding expressions. The library provides several directives for every day task.

For example the d3-html directive binds an expression to the inner Html of the element containing the directive:

<div id="entry">
    ...
    <p d3-html='paragrah'></p>
    ...
</div>

Here the paragraph is a reactive attribute of the View model.

d3.view({
    model: {
        paragraph: 'TODO'
    }
}).mount('#entry');

Core Directives

d3-attr

The d3-attr directive creates a one-way binding between a model property and an HTML element attribute

<input d3-attr-name="code" d3-attr-placeholder="description || code">

The attr can be omitted for class, name , disabled, readonly and required.

<input d3-name="code" d3-class="bright ? 'bright' : 'dull'">

code and bright are reactive properties of the view-model.

d3-for

As the name suggest, the d3-for directive can be used to repeat the element once per item from a collection. Each element gets its own model, where the given loop variable is set to the current collection item, and index is set to the item index or key.

d3-html

The d3-html directive creates a one-way-binding between a model property and the innerHtml property of the hosting HTML element. You can use it to attach html or text to element dynamically.

d3-if

With d3-if you can create conditionals.

d3-model

The d3-model directive is a special directive and the first to be mounted, if available, into the hosting element. As the name suggests, the directive creates a new model on the element based on data from parent models.

d3-on

The d3-on directive attaches an event listener to the element. The event type is denoted by the argument. The expression should be model method call, the event callback. if the attribute is omitted it is assumed to be a click event. The event callback listens to native DOM events only.

<button d3-on-click="submit()">Submit</button>

d3-show

The d3-show directive display or hide an element. The display style is preserved.

d3-value

The d3-value directive establish a two-way data binding for HTML elements supporting the value property. The binding is two ways because

  • an update in the model attribute causes an update in the HTML value property
  • an update in the HTML value property causes an update in the model attribute

Custom Directive

Creating a custom directive involve the following steps:

  • Create a (reusable) directive object:
var mydir = {
    create: function (expression) {
        return expression;
    },
    mount: function (model) {
        return model;
    },
    refresh: function (model, value) {
    },
    destroy: function () {

    }
};
  • Add the directive to the view constructor:
var vm = d3.view({
    el: '#entry',
    ...
    directives: {
        mydir: mydir
    }
};
  • Use the directive via the d3-mydir attribute.

A directive is customized via the four methods highlighted above. None of the method needs implementing, and indeed for some directive the refresh method is the only one which needs attention.

Directives can also be added via plugins

Directive API

directive.el

The HTML Element hosting the directive, available after initialisation and therefore accessible by all API methods.

directive.expression

The parsed expression, available after the create method has been called.

directive.create(expression)

The create method is called once only, at the end of directive initialisation, no binding with the HTML element or model has yet occurred. The expression is the attribute value, a string, and it is not yet parsed. This method must return the expression for parsing (it doesn't need to be the same as the input expression). However, if it returns nothing, the directive is not executed.

directive.mount(model)

The mount method is called once only, at the beginning of the binding process with the HTML element. The expression returned by the create method has been parsed and available in the this.expression attribute. This method must return the model for binding (it doesn't need to be the same as the input model, but usually it is). However, if it returns nothing, the binding execution is aborted.

directive.refresh(model, newValue)

This method is called every time the model associated with the element hosting the directive, has changed value. It is also called at the end of a successful mount.

directive.destroy(model)

Called when the element hosting the directive is removed from the DOM.

Components

Components help you extend basic HTML elements to encapsulate reusable code. They are custom elements that d3.view attach specified behavior to.

Registration

In order to use components you need to register them with the view object:

d3.view({
    components: {
        tag1: component1,
        ...
        tagN: componentN
    }
});

A component is either an object:

var component1 = {
    render: function () {
        return this.htmlElement('<p>Very simple component</p>');
    }
};

or a function, the component render method:

function component1 () {
    return this.htmlElement('<p>Another very simple component</p>');
}

Components API

# component.el

The HTML element created by the component render method. Available after the component is mounted.

# component.model

The model bound to the component

Creating a component

A component is defined by the render method. However, there optional properties and methods that can be used to customize construction and lifecycle of a component.

var component = {
    model: {...},
    init (options) {
    },
    render (data, attr) {
    },
    mounted () {
    },
    destroy () {
    }
};

component.model

A function or an object which specifies the default values of the component model.

Once the component has been mounted, this is becomes the model associated with the component and therefore an API property of the component.

Some component have their own model, other they use the model of the parent component.

component.init(options)

Hook called once only at the beginning of the component initialisation process, before it is mounted into the DOM.

component.render(data, attrs)

This is the only required hook. It is called once only while the component is being mounted into the DOM and must return a single HTMLElement or a selector with one node only. The returned element replaces the component element in the DOM. Importantly, this function can also return a Promise which resolve in an HTMLElement or selector.

  • data is the data object in the component element
  • attrs is an object containing the key-value of attributes in the component element

component.mounted()

Hook called after the component has been mounted in to the DOM. In this state the component has the full API available and all its children elements are mounted too.

component.destroy()

Called when the component HTML element is removed from the DOM.

Component API

The most important part of a component is the render method. This sections deals with the API available to the component once it is created. The API is very similar to the view-api since components and views share the same constructor.

component.events

Events object which can be used for registering event listeners or firing events.

component.parent

The parent component. If not defined this is the root view, not a component.

component.root

The view object the component belongs to.

component.uid

Component unique identifier

Plugins

Plugins, usually, add functionality to a view-model. There is no strictly defined scope for a plugin but there are typically several types of plugins you can write:

  • Add a group of components
  • Add a group of directives
  • Add some components methods by attaching them to components prototype.
  • Add providers to the view.providers object

A plugin can be an object with the install method or a simple function (same signature as the install method).

var myPlugin = {
    install: function (vm) {
        // add a component
        vm.addComponent('alert', {
            model: {
                style: "alert-info",
                text: "Hi! this is a test!"
            },
            render: function () {
                return view.htmlElement('<div class="alert" :class="style" d3-html="text"></div>');
            }
        });
        // add another component
        vm.addComponent('foo', ...);

        // add a custom directive
        vm.addDirective('mydir', {
            refresh: function (model, value) {
                ...
            }
        });
    }
}

A plugin is installed in a view via the chainable use method:

var vm = d3.view();
vm.use(myPlugin).use(anotherPlugin);

Form Plugin

This library include a form plugin for creating dynamic forms from JSON layouts. The plugin adds the d3form component to the view-model:

import {view, viewForms} from 'd3-view';

var vm = view().use(viewForms);

Importing

If you are using rollup to compile your javascript application, the form plugin will be included in your compiled file only if

import {viewForms} from 'd3-view';

is present somewhere in your code. Otherwise, it will be eliminated thanks to tree-shaking.

Form API

# form.setSubmit()

Sets the form model formSubmitted and formSubmitted reactive attribute to true and returns a Promise which resolves in true or false depending if the form inputs pass validation.

# form.isValid()

Check if the form inputs pass validation, return true or false.

Bootstrap Plugin

It is possible to use bootstrap layouts for d3 forms by importing and using the viewBootstrapForms plugin:

import {view, viewForms, viewBootstrapForms} from 'd3-view';

var vm = view().use(viewForms).use(viewBootstrapForms);

Providers

TODO

Other Frameworks

In order of complexity

D3 plugins