Giter Site home page Giter Site logo

curran / d3-component Goto Github PK

View Code? Open in Web Editor NEW
108.0 10.0 10.0 251 KB

A lightweight component abstraction for D3.js.

License: BSD 3-Clause "New" or "Revised" License

JavaScript 100.00%
component user-interface html5 web-application data-visualization interactive-visualizations interaction

d3-component's Introduction

d3-component

A lightweight component abstraction for D3.js.

Features:

Examples:

Todos
Clock
Airport Clocks
example-viewer (Redux, ES6)
Fractal Pie Chart (ES6)

Using this component abstraction, you can easily encapsulate data-driven user interface components as conceptual "boxes-within-boxes", cleanly isolating concerns for various levels of your DOM tree. This component abstraction is similar in concept and functionality to React Stateless Functional Components. Everything a component needs to render itself and interact with application state gets passed down through the component tree at render time. Components don't store any local state; this is the main difference between d3-component and the Towards Reusable Charts pattern. No special treatment is given to events or event delegation, because the intended use is within a unidirectional data flow architecture like Redux.

Installing

If you use NPM, npm install d3-component. Otherwise, download the latest release. You can also load directly from unpkg.com as a standalone library. AMD, CommonJS, and vanilla environments are supported. In vanilla, a d3 global is exported:

<script src="https://unpkg.com/d3@4"></script>
<script src="https://unpkg.com/d3-component@3"></script>
<script>
  var myComponent = d3.component("div");
</script>

API Reference

Note: There was a recent major version release, and along with it there were substantial API Changes.

In summary, the API looks like this:

var myComponent = d3.component("div", "some-class")
  .create((selection, d, i) => { ... }) // Invoked for entering component instances.
  .render((selection, d, i) => { ... }) // Invoked for entering AND updating component instances.
  .destroy((selection, d, i) => { ... }); // Invoked for exiting instances, may return a transition.

// To invoke the component,
d3.select("body") // create a selection with a single element,
  .call(myComponent, "Hello d3-component!"); // then use selection.call().

To see the full API in action, check out this "Hello d3-component" example.

# component(tagName[, className]))

Creates a new component generator that manages and renders into DOM elements of the specified tagName.

The optional parameter className determines the value of the class attribute on the DOM elements managed.

# component.create(function(selection, d, i))

Sets the create function of this component generator, which will be invoked whenever a new component instance is created, being passed a selection containing the current DOM element, the current datum (d), and the index of the current datum (i).

# component.render(function(selection, d, i))

Sets the render function of this component generator. This function will be invoked for each component instance during rendering, being passed a selection containing the current DOM element, the current datum (d), and the index of the current datum (i).

# component.destroy(function(selection, d, i))

Sets the destroy function of this component generator, which will be invoked whenever a component instance is destroyed, being passed a selection containing the current DOM element, the current datum (d), and the index of the current datum (i).

When a component instance gets destroyed, the destroy function of all its children is also invoked (recursively), so you can be sure that this function will be invoked before the compoent instance is removed from the DOM.

The destroy function may optionally return a transition, which will defer DOM element removal until after the transition is finished (but only if the parent component instance is not destroyed). Deeply nested component instances may have their DOM nodes removed before the transition completes, so it's best not to depend on the DOM node existing after the transition completes.

# component.key(function)

Sets the key function used in the internal data join when managing DOM elements for component instances. Specifying a key function is optional (the array index is used as the key by default), but will make re-rendering more efficient in cases where data arrays get reordered or spliced over time.

# component(selection[,data[,context]])

Renders the component to the given selection, a D3 selection containing a single DOM element. A raw DOM element may also be passed in as the selection argument. Returns a D3 selection containing the merged Enter and Update selections for component instances.

  • If data is specified and is an array, one component instance will be rendered for each element of the array, and the render function will receive a single element of the data array as its d argument.
    • Useful case: If data is specified as an empty array [], all previously rendered component instances will be removed.
  • If data is specified and is not an array, exactly one component instance will be rendered, and the render function will receive the data value as its d argument.
  • If data is not specified, exactly one component instance will be rendered, and the render function will receive undefined as its d argument.

In summary, components can be rendered using the following signatures:

  • selection.call(myComponent, dataObject) → One instance, render function d will be dataObject.
  • selection.call(myComponent, dataArray)dataArray.length instances, render function d will be dataArray[i]
  • selection.call(myComponent) → One instance, render function d will be undefined.

If a context object is specified, each data element in the data array will be shallow merged into a new object whose prototype is the context object, and the resulting array will be used in place of the data array. This is useful for passing down callback functions through your component tree. To clarify, the following two invocations are equivalent:

var context = {
  onClick: function (){ console.log("Clicked!");
};
selection.call(myComponent, dataArray.map(function (d){
  return Object.assign(Object.create(context), d);
}));
var context = {
  onClick: function (){ console.log("Clicked!");
};
selection.call(myComponent, dataArray, context);

d3-component's People

Contributors

curran avatar janwillemtulp avatar micahstubbs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

d3-component's Issues

Support Recursive Components

What if a component calls itself?

Since we're only using the class name provided in d3.selectAll, and selectAll operates over the entire tree (not just direct DOM children), then there's a possibility of operating over component instances that are at the wrong level of the DOM tree.

Need to come up with a test to check this.

Bring back className argument

While updating the examples, I found myself missing the second argument className. Perhaps it's best to bring it back?

Without it, here's what it looks like to have a component that has a class (from counter example).

    // A component that renders the count.
    var countDisplay = d3.component("div")
      .enter(function (){
        d3.select(this).attr("class", "count-display");
      })
      .update(function (d){
        d3.select(this).text(d.count); 
      });

With the className argument, it would look like this:

    // A component that renders the count.
    var countDisplay = d3.component("div","count-display")
      .update(function (d){
        d3.select(this).text(d.count); 
      });

Props storage regression

Props are being obliterated on setState(). This is a regression, there were no explicit tests for this case.

For example, the following test fails:

var propsStorage = d3.component("div")
  .create(function (selection, setState){
    propsStorage.setState = setState;
  })  
  .render(function (selection, props){
    propsStorage.props = props;
    selection.text(props.text);
  }); 

tape("Props storage.", function(test) {
  var div = d3.select(jsdom.jsdom().body).append("div");
  div.call(propsStorage, { text: "Foo" });
  test.equal(div.html(), "<div>Foo</div>");
  propsStorage.setState({});
  test.equal(div.html(), "<div>Foo</div>");
  test.end();
});

Specificity Issues

Sometimes component types get mixed because selecting them relies only on tag name and class name, which is not always specific enough.

For example:

var paragraph = d3.component("p")
  .render(function (selection, props){
    selection.text(props.text);
  });

var paragraphOptionalText = d3.component("p", "optional-text")
  .render(function (selection, props){
    selection.text(props.text || "");
  });

tape("Specificity.", function(test) {
  var div = d3.select(jsdom.jsdom().body).append("div")
      .call(paragraphOptionalText)
      .call(paragraph);
  test.equal(div.html(), '<p class="optional-text"></p><p></p>'); // This test fails
  test.end();
});

The value we end up with for div.html() in the above test is <p class="optional-text"></p>, because the paragraph component operated on the <p> created by the paragraphOptionalText, which it should not.

Upgrade examples to v3

Add API Documentation

Main contracts:

  • Component.tagName
  • Component.className (optional)
  • component(context, data)
  • component.destroy()

document eslint

@micahstubbs How are you running the ESLint added in #73 ?

I tried ./node_modules/.bin/eslint . and saw the following:

Oops! Something went wrong! :(

ESLint couldn't find the plugin "eslint-plugin-jsx-a11y". This can happen for a couple different reasons:

Perhaps it would be nice to add a "lint" script to package.json, so as not to depend on a global eslint installation nor require developers to use ./node_modules/.bin ?

Find a place for prose

The following feels like it does not belong in the README. Maybe a Medium post would be better? Or in a separate .md file? Stashing it here for now.

Composing Components

Components can use other components in their update functions. Some useful patterns for component composition are:

Nesting

Nesting is useful when instances of one component will contain instances of other components. Nesting can be achieved by invoking child components within the update function of the parent component, possibly deriving the value of children props from the props passed into the parent component.

Here's an example of a post component that composes two other components, heading and paragraph.

var heading = d3.component("h1")
      .update(function (selection, props){
        selection.text(props.text);
      }),
    paragraph = d3.component("p")
      .update(function (selection, props){
        selection.text(props.text);
      }),
    post = d3.component("div", "post")
      .update(function (selection, props){
        selection
          .call(heading, { text: props.title })
          .call(paragraph, { text: props.content });
      });

Here's how we would render an instance of the post component.

d3.select("#some-container-div");
  .call(post, {
    title: "Title",
    content: "Content here."
  });

The following DOM structure will be rendered.

<div id="some-container-div">
  <div class="post">
    <h1>Title</h1>
    <p>Content here.</p>
  </div>
</div>

Here's an example of rendering multiple component instances.

d3.select("#some-container-div")
  .call(post, [
    { title: "A Title", content: "a content" },
    { title: "B Title", content: "b content" },
  ]);

The following HTML structure will be rendered.

<div id="some-container-div">
  <div class="post">
    <h1>A Title</h1>
    <p>a content</p>
  </div>
  <div class="post">
    <h1>B Title</h1>
    <p>b content</p>
  </div>
</div>

For a full working example using the components above, see Posts with d3-component.

Containment

Sometimes children components are not known in advance. This is often the case for components that serve as "boxes" that can contain arbitrary content, such as cards or dialogs. There are no special constructs provided for achieving this, but it can be achieved by using a pattern in which the child component and its props are both passed in via the props of the parent component.

Here's an example of a card component that can render arbitrary children.

var card = d3.component("div", "card")
  .enter(function (selection){
    selection
      .append("div").attr("class", "card-block")
      .append("div").attr("class", "card-text");
  })
  .update(function (selection, props){
    selection.select(".card-text")
        .call(props.childComponent, props.childProps);
  });

Here's how we can use this card component to render a card that contains instances of the post component.

d3.select("#some-container-div")
  .call(card, {
    childComponent: post,
    childProps: [
      { title: "A Title", content: "a content" },
      { title: "B Title", content: "b content" },
    ]
  });

The following DOM structure will be rendered.

<div id="some-container-div">
  <div class="card">
    <div class="card-block">
      <div class="card-text">
        <div class="post"><h1>A Title</h1><p>a content</p></div>
        <div class="post"><h1>B Title</h1><p>b content</p></div>
      </div>
    </div>
  </div>
</div>

Conditional Rendering

Sometimes components should render sub-components only under certain conditions. To achieve this, the props passed into the sub-component can either be [] to render zero component instances, or any other value to render one or many component instances. Even if a sub-component is not rendered, it still needs to be invoked with its props as [], in the case that it was rendered previously and its instances need to be removed from the DOM.

Here's an example of a fruit component that conditionally renders either apple or orange components.

var apple = d3.component("span", "apple")
    orange = d3.component("span", "orange")
    fruit = d3.component("div", "fruit")
      .update(function (selection, props){
        selection
          .call(apple, props.type === "apple"? {} : [])
          .call(orange, props.type === "orange"? {} : [])
      });

Here's how we can use this fruit component.

d3.select("#some-container-div")
  .call(fruit, [
    { type: "apple" },
    { type: "orange" },
    { type: "apple" },
    { type: "apple" },
    { type: "orange" }
  ]);

The following DOM structure will be rendered.

<div id="some-container-div">
  <div class="fruit"><span class="apple"></span></div>
  <div class="fruit"><span class="orange"></span></div>
  <div class="fruit"><span class="apple"></span></div>
  <div class="fruit"><span class="apple"></span></div>
  <div class="fruit"><span class="orange"></span></div>
</div>

Return merged selection

It might be nice to return the merged selection from a render pass, so it can be further manipulated by parent components.

Adding this feature could greatly simplify some parts of Scatter Plot with Menus.

Optimize children selection

I feel like this could be improved in terms of efficiency.

...
      .selectAll(children)
      .filter(belongsToMe)
...
  function children(){
    return this.children;
  }

  function belongsToMe(){
    var instance = instanceLocal.get(this);
    return instance && instance.owner === component;
  }

The filtering could be done inside children, something like this:

  function children(){
    var mine = [];
    for(var i = 0; i < this.children.length; i++){
      if(this.children[i].tagName === tagName){ // Could check class too
        var instance = instanceLocal.get(this.children[i]);
        if(instance && instance.owner === component){
          mine.push(this)
        }
      }
    }
    return mine;
  }

More efficient context handling

We can avoid copying all the properties of the context object by instead creating a new object whose prototype is the context object.

data = data.map(function (d){
  return Object.assign(Object.create(context), d);
});

Rename data to props

With the additional of local state #8 , it makes more sense to use the term props, for conceptual parity with React components.

Performance & clarity concerns with context object

One thing that doesn't quite feel right is how so many new objects are created during the render pass when using a context object:

// Computes the data to pass into the data join from component invocation arguments.
function dataArray(data, context) {
  data = Array.isArray(data) ? data : [data];
  return context ? data.map(d => Object.assign(Object.create(context), d)) : data;
}

What if instead of creating new objects and shallow merging each datum with the context object, we split up the API such that the lifecycle hooks refer to the context object explicitly, rather than having stuff from the context object implicitly end up on d?

Before:

const myComponent = d3.component("div")
  .render((selection, d) => {
    selection
        .text("d")
        .on("click", d.onClick);
  });

After:

const myComponent = d3.component("div")
  .render((selection, d, context) => {
    selection
        .text("d")
        .on("click", context.onClick);
  });

Pros:

  • Clear separation of "data" and programmatic elements like callbacks (which would go on the context).
  • The data array is unmodified, no surprises when inspecting the datum objects.

Cons:

  • Custom logic will be required to specify a data property for all data elements.
  • More burden on developers to think about what goes where.

Use enter, update, exit names

As a follow-on to #40, inspired by @mbostock, perhaps it would be sensible to rename the methods:

  • create -> enter
  • render -> update
  • destroy -> exit

The reason why the names were create, render, destroy in the first place is that they do different things from the original enter, update, exit, and I didn't want to introduce confusion around overloading these names. However, they are indeed invoked within those very phases of the general update pattern, and it's clear enough that they are invoked on instance of d3.component, not selections. Maybe it would make sense to use these names after all.

This is how the API would look after the name change:

var myComponent = d3.component("div")
  .enter(function (d, i){
    d3.select(this).style("font-size", "30px");
  })
  .update(function (d, i){
    d3.select(this).text(d.text);
  })
  .exit(function (d, i){
    return d3.select(this).transition().style("font-size", "0px");
  });

Any thoughts?

Specify key function

The data join key function would be useful to be able to specify, for the sake of efficiency.

Add callback variant of setState

It would be nice to be able to access the previous state when setting the state.

React has a nice solution to this where you can pass in a callback like this:

setState(function (state){
  var newState = { foo: "bar" };
  return newState;
}

This seems like an essential feature for, e.g. a counter that you can increment and decrement.

Idempotence

Idempotence is the property of certain operations
in mathematics and computer science, that can be
applied multiple times without changing the result
beyond the initial application.

Archive of old README

d3-component

An idiomatic D3 component API specification.

This is an attempt to express a baseline API structure for interactive data visualizations. The main purpose for this is to enable interoperability between reusable visualization components.

In addition to an API specification, this project aims to provide:

  • A library that can validate whether or not a given implementation conform to the specification.
  • A library that provides utilities that make it easier to implement the specification.

Introduction

One of the most respected and emulated pattern for reusable D3 components is expressed in the piece Towards Reusable Charts (by Mike Bostock, 2012). In order to try to understand this pattern, I created the following bl.ocks:

Previously, I had attempted to build a library to support reusable D3 components, Model.js. This does solve the problem to some extent, however it requires that developers add Model.js as a dependency to their component. It somehow seems like one should be able to build reusable visualization components without depending on any particular library.

The Towards Reusable Charts approach comprises of two fundamental patterns:

  • chainable getter-setters, and
  • constructing a function that gets called with a D3 selection.

The merits of these patterns are numerous, and are quite well explained in the original Towards Reusable Charts piece. The chainable getter-setter pattern provides a clean and flexible API for constructing and manipulating objects. Constructing a function that gets called with a D3 selection provides the "rubber stamp" nature of the D3 Enter, Update, Exit paradigm. This makes it straightforward to, for example, implement small multiples for any visualization technique.

However, the Towards Reusable Charts pattern does not specify any particular way to deal with mutable state, events, and computed properties. For example, how should the visualization respond when the browser window is resized? To implement resizing visualization, one needs to establish some means by which to update the state of the visualization (width and height) and re-render the visualization.

There are many existing solutions for managing mutable state, events, and computed properties, however there is no clear concensus as to which solution is "the best". One of the main goals of this project is to establish a simple and well defined API specification for exposing mutable state to consumers of the component APIs. It should be possible to implement this specification using straightforward JavaScript patterns, using D3 utilities (such as dispatch and rebind), or using additional libraries such as Backbone, Angular, or React.

Another goal is to specify an API structure that transcends D3. It should be possible to implement interactive visualizations that conform to this API specification but do not use D3 with SVG to implement the graphics. For example, Leaflet may be used to implement a choropleth map, or Three.js may be used to implement a three dimensional scatter plot. It may also make sense to use Canvas to implement the graphics for a visualization. All of these options should remain open with this component API specification.

API Specification

A module that defines a component must export a component constructor function.

  • If CommonJS, the module.exports should be the constructor function.
  • If ES6, the constructor function must be the default export (using ES6 syntax export default).

A component's constructor function must take zero arguments.

  • Let's call the constructor function Component;

A component's constructor function must work regardless of whether the new keyword is used.

For example, the following two ways of invoking the constructor should have exactly the same behavior:

Without using new:

var component = Component();

Using new:

var component = new Component();

A component's constructor function must return a function, the component instance.

  • Let's call the component instance component.

The component instance must be callable with a D3 selection.

This invocation might look something like this:

d3.select("#chart")
  .datum(data)
  .call(component);

In this invocation, the selected element must be a <div>.

The HTML for the page in which this is invoked might look something like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>D3 Component Use</title>
    <script src="my-component.js"></script>
  </head>
  <body>
    <div id="chart"></div>
    <script> // invocation goes here. </script>
  </body>
</html>

Validation

Utilities

Related work:

Local State

In most cases, it makes sense to use unidirectional data flow like Redux to manage state at the root of a component tree (e.g. a stopwatch app). However, in certain cases it's useful to have local component state. In the React world, the utility of local state has been proven time and again, see this thread Question: How to choose between Redux's store and React's state? #1287.

For example a spinner. A spinner needs to use a timer to re-render itself as it spins. Also, the spinner is the only thing that will be using this timer, and its time elapsed (it "owns the datum"), so it doesn't make sense to put it in the state of the Redux store.

Therefore this library needs to have local state, similar to React's local state. Therefore it also needs to have lifecycle hooks, create and destroy.

Here's an API sketch for what local state could look like.

var spinner = d3.component("svg")
  .create(function (setState){
    setState({
      timer: d3.timer(function (elapsed){
        setState({
          elapsed: elapsed
        });
      })
    });
  })
  .render(function (selection, props, state){
    selection.call(spinnerWheel)
      .select("g")
        .attr("transform", "rotate(" + state.elapsed*props.speed + ")");
  }
  .destroy(function(state){
    state.timer.stop();
  });

More motivation for renaming d to props.

Create React Interop Examples

(Excerpted from a discussion on the D3 Slack)

The thought "d3-component as a replacement for React's Component, as it basically replicates its functionality" is correct. But I also recognize that they need to play well together. It is possible to create a general-purpose shim between them, to create a React component "wrapper" or "wrapper factory" around any component authored using d3-component. I've been meaning to make this, or at least a PoC for it, but have not yet. It would translate into simply passing the props from the React component render function into the d3-component instance. I'd welcome a PoC for this.

Accept DOM Node

..in place of a D3 selection, to support easy integration with non-D3 stuff.

More D3-like API

  • D3 methods typically pass the DOM node as this, but currently this module passes selection as the first argument, which is the same thing as d3.select(this). It may be better to have all the methods conform to the same signature as the selection.each callback of D3.

  • The props notion is actually the same notion as the datum, or d, so maybe it's best to use that convention?

  • The whole notion of setState is starting to feel like an antipattern, given that the primary use case (for me at least) is with Redux, which has its own state management scheme. Perhaps it's better to have this library only support "pure functional" components.

  • Passing in the className as the second argument feels like unnecessary surface area for this API. Setting the class can easily be done using D3's built-in mechanisms.

  • Parity with selection.each would be nice - passing (d, i, nodes) for all callbacks (enter, update, exit).

I'd like to propose the following API:

var myComponent = d3.component("div")
  .create(function (d, i, nodes){
    d3.select(this)
      .attr("class", "some-class")
      .style("font-size", "30px");
  })
  .render(function (d, i, nodes){
    d3.select(this).text(d.text);
  })
  .destroy(function (d, i, nodes){
    return d3.select(this).transition().style("font-size", "0px");
  });

What do you think?

The above example is trivial, and would be perhaps simpler to actually implement with raw D3, but gives an example that shows the whole API.

This change will also make it clear what the real purpose of this library is - what it provides that raw D3 does not, namely:

  • Support for isolating multiple components that have the same tag (and possibly class).
  • Support for arbitrarily nested components (e.g. recursive components, where using selectAll("div.some-class") would yield unwanted entries).
  • Support for reliable destroy hooks that will be executed recursively for all children of a component instance being destroyed.

v3 API Changes

Summary of API changes between v2 and v3:

  • No longer using this in create(), render(), destroy()
  • Passing a selection as the first argument to create(), render(), destroy()
  • No longer passing nodes into create(), render(), destroy()
  • No longer passing i into create(), render(), destroy()

Before: .render(function (d, i, nodes){ var selection = d3.select(this);

After: .render(function (selection, d){

One driving force is ES6 considerations. Using d3-component is awkward with ES6, because the API relies heavily on use of non-lexically-bound this. For the next major version of d3-component, I'd like to make the API more ES6-friendly.

The decision for the original API was intentional, to mirror the API of D3's selection.each. However, it's unfortunate because we can't use ES6 arrow functions.

ES5:

var paragraph = d3.component("p")
  .render(function (d){
    d3.select(this).text(d);
  }),

ES6 (ideal):

const paragraph = d3.component("p")
  .render((d) => d3.select(this).text(d)); // Breaks due to lexically bound "this"

Maybe we could pass in a selection:

ES6 (idea):

const paragraph = d3.component("p")
  .render((selection, d) => selection.text(d)); // Would work fine.

/cc @micahstubbs

Use D3 API style

Before:

var paragraph = d3.component({
  render: function (selection, d){ selection.text(d); },
  tagName: "p"
});

var heading = d3.component({
  render: function (selection, d){ selection.text(d); },
  tagName: "h"
});

var post = d3.component({
  render: function (selection, d){
    selection
      .call(heading, d.title)
      .call(paragraph, d.content);
  },
  tagName: "div"
});

After:

var paragraph = d3.component("p")
  .render(function (selection, d){ selection.text(d); });

var heading = d3.component("h")
  .render(function (selection, d){ selection.text(d); });

var post = d3.component("div")
  .render(function (selection, d){
    selection
      .call(heading, d.title)
      .call(paragraph, d.content);
  });
``

Add tests for index

Currently there are no tests that cover the fact that i is passed in the enter, update and exit callbacks.

Use names create, render, destroy

Currently the names enter, update, and exit are used (from #42). This doesn't feel quite right though, as they are the same names used as methods on D3 selections. Imagine a scenario where the general update pattern is used in the update function, you'd see enter and exit all over the place and it might be confusing. Also, update applies to updating AND entering component instance, so it's a bit misleading.

Support Custom Exit Transitions

This is kind of the last frontier with this component system.

Let's say a component wants to do some kind of whiz-bang transition when it is about to get destroyed. For example, a spinner could fade out rather than abruptly disappear.

This should be supported.

Proposed API: return a transition from the destroy hook.

Pass additional "context" object

In the Todos example, the need came up to attach some properties to each element of the data array. Time will tell, but my gut feeling is that this need will come up over and over again, as it will be common to pass functions, actions, or Rexux's dispatch down through to child components that are driven by a data array.

Here's the pattern that cropped up, using data.map:

    // Displays the visibility filter controls.
    var footer = (function (){
      var data = [
        { text: "All", filter: "SHOW_ALL", comma: true },
        { text: "Active", filter: "SHOW_ACTIVE", comma: true },
        { text: "Completed", filter: "SHOW_COMPLETED" }
      ];
      return d3.component("span")
        .update(function (d){
          d3.select(this)
              .text("Show: ")
              .call(filterLink, data.map(function (_){  // <----- This bit could be avoided.
                return Object.assign({}, _, {
                  onClick: function (){
                    d.actions.setVisibilityFilter(_.filter);
                  }
                });
              }));
        });
    }());

Proposed new argument:

    // Displays the visibility filter controls.
    var footer = (function (){
      var data = [
        { text: "All", filter: "SHOW_ALL", comma: true },
        { text: "Active", filter: "SHOW_ACTIVE", comma: true },
        { text: "Completed", filter: "SHOW_COMPLETED" }
      ];
      return d3.component("span")
        .update(function (d){
          d3.select(this)
              .text("Show: ")
              .call(filterLink, data, { // Third argument here is new.
                onClick: function (){
                  d.actions.setVisibilityFilter(_.filter);
                }
              });
        });
    }());

Datum passed to destroy should be most recent

This test fails if added to /test/exit-test.js.

var datum,
    customExit = d3.component("p")
      .destroy(function (selection, d){
        datum = d;
        return selection.transition().duration(10);
      });

tape("Datum passed to destroy should be most recent.", function(test) {
  var div = d3.select(jsdom.jsdom().body).append("div");

  div.call(customExit, "a");
  div.call(customExit, []);
  test.equal(datum, "a");

  div.call(customExit, "a");
  div.call(customExit, "b");
  div.call(customExit, []);
  test.equal(datum, "b"); // Fails here, uses "a"

  test.end();
});

Invoke Destroy Hook Recursively

Currently, the following test fails.

var created,
    destroyed,
    apple = d3.component("span", "apple")
      .create(function (){ created++; })
      .destroy(function(){ destroyed++; }),
    orange = d3.component("span", "orange")
      .create(function (){ created++; })
      .destroy(function(){ destroyed++; }),
    fruit = d3.component("div", "fruit")
      .render(function (selection, props){
        selection
          .call(apple, props.type === "apple"? {} : [])
          .call(orange, props.type === "orange"? {} : [])
      });

tape("Recursive destruction.", function(test) {
  var div = d3.select(jsdom.jsdom().body).append("div");

  // Enter.
  created = destroyed = 0;
  div.call(fruit, [
    { type: "apple"},
    { type: "orange"}
  ]);
  test.equal(created, 2);
  test.equal(destroyed, 0);

  // Exit.
  created = destroyed = 0;
  div.call(fruit, []);
  test.equal(created, 0);
  test.equal(destroyed, 2); // <---------------------------------------The value is 0

  test.end();
});

The library currently only invokes the destroy hook on the component instances themselves, not nested child components.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.