block by boeric cbb5b416091e74c70b8480e68b6d21e1

CSS Box Model and Flexbox Using D3

Full Screen

CSS Box Model and Flexbox Demo

The Gist demos the following:

  1. The effects of margin, border, padding and inner content dimensions of the overall size of DOM elements (here, span elements)
  2. That outline has no effect on the element size and layout
  3. The horizontal or vertical layout of elements
  4. The optional use of Flexbox in laying out elements
  5. The use of various justify-content settings when using Flexbox
  6. The effect of setting dimensions on the item container div
  7. How to create a DOM structure including input and select elements with event handlers using D3
  8. How to calculate the full dimension of an element
  9. How to extract current inline styles of elements

The Gist is live here: https://bl.ocks.org/boeric

index.js

/* By Bo Ericsson, https://www.linkedin.com/in/boeric00/ */

/* eslint-disable no-multi-spaces, operator-linebreak, indent, no-plusplus, no-nested-ternary,
   func-names */
/* global d3 */

/*
  Please note that indentation rules follow D3 praxis, that is, any method that alters the
  selection is indented with two spaces, and all other methods are indented with four spaces.
  There is no easy eslint rule to fix this...
*/

const itemCount = 9;
const data = [];

// Body dimensions
const bodyMargin = 0;
const bodyWidth = 940;  // To fit https://bl.ocks.org width requirement
const bodyHeight = 480; // To fit https://bl.ocks.org height requirement
const bodyPadding = 10;

// Title and subtitle container height
const titleHeight = 20;

// Controls container dimensions
const controlsHeight = 20;
const controlsMarginBottom = 30;

// Content container dimensions
const contentContainerMarginTop = 10;

const contentContainerMaxHeight =
  bodyHeight -                             // Total height of body
  titleHeight -                            // Height of title container
  controlsHeight - controlsMarginBottom -  // Total height of controls container
  titleHeight -                            // Height of subtitle
  contentContainerMarginTop;               // Content container top margin

// Set body dimensions
d3.select('body')
    .style('margin', `${bodyMargin}px`)
    .style('height', `${bodyHeight}px`)
    .style('width', `${bodyWidth}px`)
    .style('padding', `${bodyPadding}px`);

// Item dimensions
const width = 40;
const height = 15;
const margin = 10;
const border = 3;
const padding = 8;
const outline = 1;

// Create data
for (let i = 0; i < itemCount; i++) {
  data.push(`Item ${i}`);
}

// Options for the select control
const justifyContentOptions = [
  { value: 'space-evenly', default: false, idx: 0 },
  { value: 'space-between', default: true, idx: 1 },
  { value: 'space-around', default: false, idx: 2 },
  { value: 'center', default: false, idx: 3 },
  { value: 'flex-end', default: false, idx: 4 },
  { value: 'flex-start', default: false, idx: 5 },
];

// Set initial flexbox direction
let direction = 'row';

// Set default flexbox direction
const defaultJustifyContent = justifyContentOptions.find(d => d.default);
const defaultJustifyIdx = defaultJustifyContent.idx;
let justifyContent = defaultJustifyContent.value;

// Define the input controls
const controls = {
  useMargin: {
    type: 'checkbox',
    pos: 0,
    label: 'Use Margin',
    checked: true,
    title: 'Use Margin',
  },
  useBorder: {
    type: 'checkbox',
    pos: 1,
    label: 'Use Border',
    checked: true,
    title: 'Use Border',
  },
  usePadding: {
    type: 'checkbox',
    pos: 2,
    label: 'Use Padding',
    checked: true,
    title: 'Use Porder',
  },
  useOutline: {
    type: 'checkbox',
    pos: 3,
    label: 'Use Outline',
    checked: true,
    title: 'Use Outline',
  },
  useScroll: {
    type: 'checkbox',
    pos: 4,
    label: 'Overflow scroll',
    checked: true,
    title: 'Scroll or hide overflow',
  },
  useMaxSize: {
    type: 'checkbox',
    pos: 5,
    label: 'Max Container Size',
    checked: true,
    title: 'Use Max Size of Container',
  },
  flexRow: {
    type: 'radio',
    pos: 6,
    label: 'Row',
    checked: direction === 'row',
    title: 'Row Direction',
  },
  flexColumn: {
    type: 'radio',
    pos: 7,
    label: 'Column',
    checked: direction === 'column',
    title: 'Column Direction',
  },
  useFlex: {
    type: 'checkbox',
    pos: 8,
    label: 'Use Flexbox',
    checked: true,
    title: 'Use Flex',
  },
};

// Update the visualization
function updateViz() {
  // Update container
  const flex = controls.useFlex.checked;
  const maxSize = controls.useMaxSize.checked;
  const scroll = controls.useScroll.checked;

  // Compute dimensions
  const marginUsed = controls.useMargin.checked ? margin : null;
  const borderUsed = controls.useBorder.checked ? border : null;
  const paddingUsed = controls.usePadding.checked ? padding : null;
  const outlineUsed = controls.useOutline.checked ? outline : null;

  // Generate dimension strings
  const totalMargin = (marginUsed || 0) * 2;
  const totalBorder = (borderUsed || 0) * 2;
  const totalPadding = (paddingUsed || 0) * 2;

  const totalWidth = width + totalPadding + totalBorder + totalMargin;
  const totalHeight = height + totalPadding + totalBorder + totalMargin;

  const coreStr = `Core: ${width}px/${height}px`;
  const marginStr = `Margin: ${totalMargin}px`;
  const borderStr = `Border: ${totalBorder}px`;
  const paddingStr = `Padding: ${totalPadding}px`;
  const totalStr = `Total: ${totalWidth}px/${totalHeight}px`;
  const dimStr = `${coreStr}, ${paddingStr}, ${borderStr}, ${marginStr}, ${totalStr}`;

  // Update content container
  const contentContainer = d3.select('.contentContainer')
      .style('display', flex ? 'flex' : 'inline-block')
      // .style('display', flex ? 'flex' : 'inline')
      .style('flex-direction', flex ? direction : null)
      .style('justify-content', flex ? justifyContent : null)
      .style('width', maxSize ? '100%' : 'auto')
      .style('height', maxSize ? `${contentContainerMaxHeight}px` : null)
      .style('overflow', scroll ? 'scroll' : 'hidden');

  // Determine selection
  const updateSelection = contentContainer.selectAll('span').data(data);
  const enterSelection = updateSelection.enter();
  const enterSelectionSize = enterSelection.size();
  const selection = enterSelectionSize === 0 ? updateSelection : enterSelection.append('span');

  // Update items
  selection
      .attr('class', 'innerElem')
      .style('margin', marginUsed !== null ? `${marginUsed}px` : null)
      .style('border', borderUsed !== null ? `${borderUsed}px solid #2196f3` : null)
      .style('padding', paddingUsed !== null ? `${paddingUsed}px` : null)
      .style('outline', outlineUsed !== null ? `${outlineUsed}px solid red` : null)
      .style('width', `${width}px`)
      .style('height', `${height}px`)
      .style('display', direction === 'row' ? 'inline-block' : 'block')
      .text((d) => d);

  // Update overlay
  const overlay = d3.select('.overlay');
  overlay.selectAll('*').remove();

  const overlayItem = overlay
    .append('div')
      .style('display', 'flex')
      .style('flex-direction', 'row');

  // Add an outer wrapper to the 'Item' that will be added below
  const itemContainer = overlayItem
    .append('div')
      .style('outline', '1px solid orange')
      .style('display', 'inline-block');

  // Add an item instance inside the item container
  itemContainer.append('span')
      .attr('class', 'innerElem')
      .style('margin', marginUsed !== null ? `${marginUsed}px` : null)
      .style('border', borderUsed !== null ? `${borderUsed}px solid #2196f3` : null)
      .style('padding', paddingUsed !== null ? `${paddingUsed}px` : null)
      .style('outline', outlineUsed !== null ? `${outlineUsed}px solid red` : null)
      .style('width', `${width}px`)
      .style('height', `${height}px`)
      .style('display', direction === 'row' ? 'inline-block' : 'block')
      .text('Item');

  // Add text to the right of the item
  overlayItem.append('div')
      .style('margin-left', '10px')
      .text('Orange rectangle represents total dimension');

  const infoContainer = overlay.append('div')
      .attr('class', 'infoContainer');

  const itemDimensionContainer = infoContainer.append('div')
      .attr('class', 'textBold')
      .style('margin-top', '10px')
      .text('Item Dimensions');

  itemDimensionContainer.append('div')
      .attr('class', 'textNormal')
      .style('margin-left', '10px')
      .text(dimStr);

  const itemStyleContainer = infoContainer.append('div')
      .attr('class', 'textBold')
      .style('margin-top', '10px')
      .text('Item Inline Styles');

  itemStyleContainer.append('div')
      .attr('class', 'textNormal')
      .style('margin-left', '10px')
      .text(selection.nodes()[0].style.cssText);

  const contentStyleContainer = infoContainer.append('div')
      .attr('class', 'textBold')
      .style('margin-top', '10px')
      .text('Container Inline Styles');

  contentStyleContainer.append('div')
      .attr('class', 'textNormal')
      .style('margin-left', '10px')
      .text(contentContainer.nodes()[0].style.cssText);
}

// Set dimensions of the container that contains the input controls (checkboxes, radios and select)
const controlsContainer = d3.select('.controlsContainer')
    .style('height', `${controlsHeight}px`)
    .style('margin-bottom', `${controlsMarginBottom}px`);

// Create span elem wrapper for each input control
const keys = Object.keys(controls).sort((a, b) => (a.pos > b.pos ? 1 : a.pos < b.pos ? -1 : 0));
const spans = controlsContainer.selectAll('span')
    .data(keys)
    .enter()
  .append('span')
    .style('margin-right', '5px');

// Create checkboxes and radios, and add event handler
spans.append('input')
    .attr('type', (d) => controls[d].type)
    .attr('name', (d) => d)
    .attr('title', (d) => controls[d].title)
    .property('checked', (d) => controls[d].checked)
    .on('change', function(d) {
      const checked = d3.select(this).property('checked');

      // Manage radios without form
      const radios = d3.selectAll('input[type="radio"]');

      switch (d) {
        case 'useFlex':
          controls[d].checked = checked;
          // Disable select if flexbox is not used
          d3.select('.justifySelect').property('disabled', !checked);
          break;
        case 'flexRow':
          direction = 'row';
          controls.flexRow.checked = true;
          controls.flexColumn.checked = false;
          // Update the radios
          radios.nodes()[0].checked = true;
          radios.nodes()[1].checked = false;
          break;
        case 'flexColumn':
          direction = 'column';
          controls.flexRow.checked = false;
          controls.flexColumn.checked = true;
          // Update the radios
          radios.nodes()[0].checked = false;
          radios.nodes()[1].checked = true;
          break;
        default:
          controls[d].checked = checked;
      }

      // Update content
      updateViz(data);
    });

// Create labels for the checkboxes and radios
spans.append('label')
    .attr('for', (d) => d)
    .attr('title', (d) => controls[d].title)
    .text((d) => controls[d].label);

// Create select control
const selectControl = controlsContainer.append('select')
    .attr('class', 'justifySelect')
    .on('change', function () {
      const option = d3.select(this).node().value;
      justifyContent = option;
      updateViz();
    });

// Add options to select
selectControl.selectAll('option')
    .data(justifyContentOptions)
    .enter()
  .append('option')
    .text((d) => d.value);

// Set default (pre-preselected value) of select
selectControl.property('selectedIndex', defaultJustifyIdx);

// Add height to title container
d3.select('.titleContainer')
    .style('height', `${titleHeight}px`);

// Add height to subtitle container
d3.select('.subTitleContainer')
    .style('height', `${titleHeight}px`);

// Set top margin and background color of content container
d3.select('.contentContainer')
    .style('margin-top', `${contentContainerMarginTop}px`);

// Initial update
updateViz();

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CSS Box Model and Flexbox Demo</title>
  <link rel="stylesheet" href="styles.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js"></script>
  <script src="index.js" defer></script>
</head>
<body>
  <div class="outerContainer">
    <div class="subTitleContainer">Controls</div>
    <div class="controlsContainer"></div>
    <div class="subTitleContainer">Container</div>
    <div class="contentContainer"></div>
  </div>
  <div class="overlay"></div>
</body>

styles.css

body {
  font-family: helvetica;
  font-size: 12px;
  outline: 1px solid lightgray;
}
span {
  height: max-content;
  width: max-content;
}
select {
  font-size: 12px;
}
.innerElem {
  background-color: lightblue;
}
.subTitleContainer {
  font-size: 15px;
  font-weight: bold;
}
.contentContainer {
  background-color: aliceblue;
  outline: 1px solid lightgray;
  overflow: scroll;
}
.overlay {
  position: absolute;
  top: 200px;
  left: 125px;
  width: 450px;
  height: 240px;
  border: 1px solid #bbb;
  border-radius: 1px;
  background-color: white;
  padding: 10px;
}
.infoContainer {
  position: absolute;
  top: 70px;
  left: 0px;
  width: 450px;
  height: 170px;
  margin: 10px;
}
.textBold {
  font-weight: bold;
}
.textNormal {
  font-weight: normal;
}