Skip to content

tarkant/svg-dom-arrows

Repository files navigation

🏹 SVG DOM Arrows CircleCI

Draw an arrow from one HTML element to another with a simple API and 0 dependencies.

svg-dom-arrows-drag-n-drop-demo

How to get started? ✨

To get started, you'll need 3 things:

  • Your starting DOM element.
  • Your ending DOM element.
  • The style of your SVG path.

Simple implementation:

    const options: PathOptions = {
      start: {
        element: document.getElementById('myGreenBox'),
      },
      end: {
        element: document.getElementById('myGreyBox'),
      },
      style: 'stroke:black;stroke-width:4;fill:transparent',
      appendTo: document.body, // Optional
    };
    const arrow = new LinePath(options);

    // Output

    arrow = {
        containerDiv: HTMLDivElement,// The generated div containing the SVG element
        startBbox: DOMRect, // Internal object used by the class to calculate the arrow path
        endBbox: DOMRect, // Internal object used by the class to calculate the arrow path
        options: PathOptions, // The options object passed
        svgElement: SVGSVGElement, // The generated SVG element containing the path
        svgPathLine: SVGPathElement, // The generated path
    };

ℹ Note: If you don't specify the appendTo option, the SVG path will be rendered but not appended to the document. It will be up to you to append it to a div for example.

Below a screenshot resulting from the code:

line-path-example

Below some demos using vanilla TS/JS and other frameworks/

Demos ✨

You can checkout some demos here with few libs and frameworks so you can get yourself started with the implementation. In a way, svg-dom-arrows will work with anything, it has no dependencies and just uses plain DOM elements. But just in case, I've tried it with Angular, Vue and React as you can see below:

Positioning ➕

Positioning is based on a left and top axis, below a simple visualization of how the axes are based on the element. 0 is at the left of the element, 1 would be at 100% left from the origin.

positioning-axis-guide

You might have noticed that the drawn path starts from position 0,0 of myGreenBox to 0,0 at myGreyBox. If no position is specified, the tool will assume you want it to start from 0,0. You can of course set the position as you like as follows:

    const options: PathOptions = {
      start: {
        element: document.getElementById('myGreenBox'),
        position: {
          top: .5, // 0 being the top of the element
          left: 1, // 0 is at the right of the element
        },
      },
      end: {
        element: document.getElementById('myGreyBox'),
        position: {
          top: .5,
          left: 0,
        },
      },
      style: 'stroke:black;stroke-width:4;fill:transparent',
      appendTo: document.body,
    };
    const arrow = new LinePath(options);

The result would be as follows:

line-path-example-axis

ℹ Note: 0 to 1 are the max values inside the BBox of the DOM element. This does not mean that you're limited to those values. You can experiment with negative values or < 1 values.

Styling 🎨

The style options will be copied directly to the SVG path's style attribute. This being an SVG, it's a little bit different than the usual CSS. Besides what's mostly important is that:

  • stroke:<color> to set the color of the line.
  • stroke-width:<number> to set the thickness of your line.
  • fill:transparent to keep only the line and avoid having a default black color.

You can find a lot of resources about styling your SVG path to suit your needs.

Markers ↗

The API lets you add markers to your SVG path. Markers can be a path or a group, the API just expects an SVGMarkerElement with an ID attribute that it can then append to the defs.

You just need to add the markerId for your start or end element and it will do the job, eg:

/**
 * This function just returns an `SVGMarkerElement` that's already pre-styled and so on
 */
const createMarker  = (): SVGMarkerElement => {
  const arrow = document.createElementNS(SVGNS, 'path');
  const marker = document.createElementNS(SVGNS, 'marker');

  arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
  arrow.setAttribute('style', 'fill:white;stroke-width:0.801524;stroke-miterlimit:4;stroke-dasharray:none');

  marker.setAttribute('id', 'marker1'); // <== Make sure to set an id attribute
  /**
   * The below attributes and values are specific to this marker, you'll have to know how markers work
   * to really do something fun. For now you'll have to deal with it manually but I might work on an SVG
   * marker utility.
   */
  marker.setAttribute('refX', '5');
  marker.setAttribute('refY', '5');
  marker.setAttribute('viewBox', '0 0 10 10');
  marker.setAttribute('orient', 'auto-start-reverse'); // <== There is a trick here, be sure to read after the code snippet
  marker.setAttribute('markerWidth', '6');
  marker.setAttribute('markerHeight', '6');
  marker.appendChild(arrow);

  return marker;
};

const s = document.querySelector('.case-1');
const e = document.querySelector('.case-4');
const line = new LinePath({
  start: {
    element: s,
    position: {
      top: .45,
      left: 1,
    },
    markerId: '#marker1', // <== We attribute the marker id for the start of the path
  },
  end: {
    element: e,
    position: {
      top: .5,
      left: 0,
    },
    markerId: '#marker1',  // <== We attribute the same marker to the end of the path
  },
  style: 'stroke:white;stroke-width:4;fill:transparent',
  appendTo: document.body,
  markers: [createMarker()], // Supply your markers as an array to this option
  // Custom CSS classes that you can add for additional styling
  // CSS classes should be as follows: `foo bar baz` and NOT `.foo.bar.baz`
  customClass: {
    container: `container-${idx} foo bar`,
    svgPath: `path-${idx} baz`,
    svgElement: `element-${idx}`,
  }
}, true);

Below the result with an arrow going from the starting to ending DIVs with an arrow marker:

line-path-example-w-markers

As you can see, the marker orients itself correctly, this is due to marker.setAttribute('orient', 'auto-start-reverse') and how the path was drawn it the first place.

To put it simply, you really need to know how markers work to make nice arrows. I'll follow up with a tool that may help solve this issue in the future hopefully! Of course, if you know a guy who knows a guy who knows how to deal with this, please tell them to chime in!

Other options ��

You can checkout PathOptions.ts to see what other options you can supply. The most useful for now is manualRender that if you set to true will not render your SVG. You'll have then to call render() to do it.

Adding another Path style 🔌

Adding your own path style is pretty straightforward. The only "drawback" is that you'll have to do it in TypeScript. Anyway, let me walk you through how you can do this (I'll take CurvyPath as an example):

  1. Create your CurvyPath.ts file under src/paths/.
  2. Extend the LinePath class as it's your base class.
  3. Implement your own logic in getPath() and svgPath().
  4. Export your CurvyPath.ts in the barrel file src/paths/index.ts.
  5. Done 🎉.

To get ahold of how this all works, you can checkout LinePath.ts which is your base class. It provides you with everything you need.

And of course you can checkout the /src/paths/ folder to understand how extending the base class makes things easier.

Note ℹ: The only thing you need to respect is to use PathOptions.ts to keep the API consistent. If you need to add options, please add them as optional properties.

💻 How to run the development server ?

Easy, run this command and your server will be on http://localhost/3000:

npm start

🆘 Issues and contributions

If you have an issue with this library or want to contribute, please let me know I'll be happy to interact with you.

⏲ Changelog

  • v2.2.3 : Fixed an issue with the TS types not being built, thanks to @unitythemaker in #47 for his report 👌 .
  • v2.2.2 : Fixed the readme example gif
  • v2.2.1 : Fixed build issue with webpack
  • v2.2.0 : Add example page and expose the options property in Path.ts for ease of use
  • v2.1.6 : Apply security fixes for npm packages
  • v2.1.5 : Deprecated the method recalculate() and renamed it to redraw() suggested by @calumk in #35
  • v2.1.4 : Integrated @JacerOmri PR !34
  • v2.1.3 : Updated .npmignore rules + updated the CI + added github package publish
  • v2.1.2 : Exposed the SVG path, SVG element and div container + general tests fixes
  • v2.1.1 : Fix auto-publish github action
  • v2.1.0 : Added the option to set custom CSS classes for the div container, svg element and svg path as suggested by @rbozan in #32 and #33
  • v2.1.0-beta.1 : Added the option to set custom CSS classes for the div container, svg element and svg path as suggested by @rbozan in #32 and #33
  • v2.0.8 : npm audit fixes + test new github action
  • v2.0.7 : Fixed issue #30 reported by @Flambe related to the besier behavior on small offsets + added github action for auto npm publish 🎉
  • v2.0.7-beta.1 : Fixed issue #30 reported by @Flambe related to the besier behavior on small offsets
  • v2.0.6 : Fixed issue #31 reported by @Flambe.
  • v2.0.6-beta.1 : Fixed issue #31 reported by @Flambe.
  • v2.0.5: e2e tests + fixed issue #29 with typings not being bundled after the build thanks to @Flambe.
  • v2.0.4: Fixed issue #27 related to positioning relatively to the appendTo element + fixed unit tests thanks to @jan-dolejsi.
  • v2.0.4-beta.1: Fixed issue #27 related to positioning relatively to the appendTo element.
  • v2.0.2: Documented the code, added SvgProportions type, recalculate() method, and improved readme.
  • v2.0.1: Fixed issue with typings not being packaged.
  • v2.0.0a: Rewrote from the ground up the full API and implementation.
  • v1.0.0: Forked and improved from sasza2/arrows