Storing SVG objects in an array for loading

2.5k Views Asked by At

I am having some trouble with using the function Element.append(el) in the Snap.svg API. I need to store an instance of that SVG in an array of variables so that I can access specific ones throughout runtime.

When I try this code with just storing it into groupViews, it works. But, putting it an array (groupView) brings me this error:

Uncaught TypeError: Cannot read proerty 'append' of undefined

The console shows me the same output when I log groupViews and groupView[i] so I am not sure what else to try.

Any ideas as to how I can solve this? Thanks.

for(var i = 0; i < numOfGroups; i++)
  {
    var sel = '#GroupView' + i;
    groupView[i] = Snap(sel);
    console.log(groupView[i]);
    groupViews = Snap('#GroupView0');
    console.log(groupViews);


    Snap.load("/svg/groupView-12_13_2016-01.svg", function(f) {
      groupView[i].append(f);
      groupViews.append(f);


    }); //end of Snap.load(groupView)


  }

Edit (1/5):

Here is the HTML code where I have empty SVGs loaded:

<div class = "groupView0">
    <svg viewBox='0 0 3640.9 540.5' id='GroupView0'></svg>
</div>

<div class = "groupView1">
    <svg viewBox='0 0 3640.9 540.5' id='GroupView1'></svg>
</div>

Hence, this is why I am loading the specific SVG file and appending it in JS. Is there a better way to do this?

Edit (1/6):

Here is a small portion of my svg file:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 3640.9 540.5" style="enable-background:new 0 0 3640.9 540.5;" xml:space="preserve">

<g id="A" onclick="detailedView();" cursor="pointer">
    <rect id="PanelBackground" x="35.4" y="55.5" class="st2" width="173.2" height="155"/> <!--height="444" -->
    <g id="UpperBar">
        <linearGradient id="Header_20_" gradientUnits="userSpaceOnUse" x1="35.3937" y1="67.0279" x2="208.5774" y2="67.0279">
            <stop  offset="0" style="stop-color:#F68E1E"/>
            <stop  offset="0.5" style="stop-color:#F16A22"/>
            <stop  offset="0.903" style="stop-color:#F05F22"/>
            <stop  offset="1" style="stop-color:#F05C22"/>
        </linearGradient>
        <polygon id="Header" class="st3" points="35.4,55.5 208.6,55.5 208.6,78.6 35.4,78.6      "/>
        <g>
            <rect x="82.1" y="61.8" class="st4" width="121" height="10.8"/>
            <text id="GroupName" transform="matrix(1 0 0 1 82.0885 71.6627)" class="st5 st6 st7">A</text>
            <text id="FloorNumber" transform="matrix(1 0 0 1 182.3287 141.8773)" class="st2 st19 st7">X</text>
        </g>
     </g>
</svg>

After executing the code you posted.. If I try to run code like:

groupName[0] = groupView[0].select('#GroupName');
groupName[0].attr({
    text: "something"
});

I get this error: Uncaught TypeError: Cannot set property '0' of undefined

2

There are 2 best solutions below

10
Ian On BEST ANSWER

Try something like this, it uses clone() as it looks like you are loading in the same SVG file each time, so we could load it in once, then clone() it to save repeated calls to the server.

This relies on the basis that the svg file will have an outer markup in it. If it's partial svg, and only has a element, change the select('svg') to select('group') or whatever it is.

Then we store a reference to the clone, and then append that element to the indexed svg element.

Here is a full example, the svg test.svg loaded in is simply..

<svg>
  <rect x="20" y="20" width="100" height="100" />
</svg>

Main html/js

<body>
<div class="groupView0">
  <svg id="groupView0"></svg>
</div>
<div class="groupView1">
  <svg id="groupView1"></svg>
</div>
<div class="groupView2">
  <svg id="groupView2"></svg>
</div>

<script>

var sel = "#groupView";
var groupView = [];
var numOfGroups = 3;

Snap.load("test.svg",onSVGLoaded);

function onSVGLoaded ( fragment ) {
  groupView[0] = Snap( sel + '0' ).append( fragment ).select('svg');

  for( var i = 1; i < numOfGroups; i++ ) {
    groupView[i] = groupView[0].clone().appendTo( Snap( sel + i ) );
  } 

  groupView[0].attr({ fill: 'yellow' });
  groupView[1].attr({ fill: 'red' });
  groupView[2].attr({ fill: 'green' });
} 

Working example

4
MiltoxBeyond On

The problem is that you are using an asynchronous function inside of the loop, so the file doesn't load until after it has finished (especially since you call the same file numOfGroups - 1 times. Basically when the result does return, i has already processed the entire array and the value of i is no longer in the range you expect it to be (numOfGroups to be exact).

Since groupView[numOfGroups] was never set to a value it is undefined.

The ideal solution is to either wait for the external file to load before looping (in the callback function) like so:

var sel = '#GroupView';
var groupView = [];
Snap.load("/svg/groupView-12_13_2016-01.svg", function(f) {
      for(var i = 0; i < numOfGroups; i++) {
          groupView[i] = new Snap(sel+i);
          groupView[i].append(f);
      }      
}); 

Or to define a variable within the loop scope to trick the callback function into using a scope variable when assigning the value of the load, again like so:

for(var i = 0; i < numOfGroups; i++)
  {
    var sel = '#GroupView' + i;
    var cur = i; //This creates a scope variable for the current callback.
    groupView[i] = Snap(sel);
    console.log(groupView[i]);
    groupViews = Snap('#GroupView0');
    console.log(groupViews);


    Snap.load("/svg/groupView-12_13_2016-01.svg", function(f) {
      groupView[cur].append(f);
      groupViews.append(f);


    }); //end of Snap.load(groupView)
  }

However, the second solution actually is very inefficient considering it makes requests numOfGroups of times for the same file, and loads numOfGroups callbacks into memory. The only time it should be necessary is if you are actually going to call different files.

Finally I'll give you a simple example of what is happening.

var d = document.getElementById("testOutput");
var b = document.getElementById("trigger");

function RunTest() {
  var arr = [];
  d.innerHTML = "";
  for (var i = 0; i < 10; i++) {
    d.innerHTML += "Loop: " + i;
    arr[i] = i;
    setTimeout(function() {
      d.innerHTML += "i is " + i + "<br />";
    }, 1000); //Simulates Delay while loading...
    d.innerHTML += "...Finished Loop </br>";
  }
}
<input id="trigger" type="button" onClick="RunTest()" value="Test" />
<div id="testOutput"></div>