SVG の 要素を JavaScript で生成して DOM ツリーに追加しても何も表示されない←んなこたぁない このエントリーをはてなブックマークに追加

(投稿直後に追記: この問題は既に解決しております…)

ご無沙汰しております…。決算期が近づいて参りましたね。今期はほとんど売上立ってませんが… orz

それはさておき、新しい仕事で使うことになりそうな技術の調査がてらプロトタイプ作りを進めようとしているのですが、HTML5 のインライン SVG を DOM から操作する際、 ID から要素を参照してその写像を表示する <use> 要素の扱いについて疑問点があったので、ちょっとメモしておくことに。

<use> エレメントについてはW3C の仕様勧告にサンプルがあるのですが (インライン SVG として簡略化して書くと以下のような感じ)、

<svg width="10cm" height="3cm" viewBox="0 0 100 30">
  <defs>
    <rect id="MyRect" width="60" height="10"/>
  </defs>
  <rect x=".1" y=".1" width="99.8" height="29.8" fill="none" stroke="blue" stroke-width=".2" />
  <use x="20" y="10" xlink:href="#MyRect" />
</svg>

これと同じものを JavaScript から DOM ツリーとして追加していく場合、以下のような実装になりそうなものです。

var svgns = "http://www.w3.org/2000/svg";

window.addEventListener("load", function() {
  var container = document.getElementById("container");  // コンテナになる 
とか var svg = document.createElementNS(svgns, "svg"); svg.setAttribute("width", "10cm"); svg.setAttribute("height", "3cm"); svg.setAttribute("viewBox", "0 0 100 30"); container.appendChild(svg); var defs = document.createElementNS(svgns, "defs"); var rect = document.createElementNS(svgns, "rect"); rect.id = "MyRect"; rect.setAttribute("width", 60); rect.setAttribute("height", 10); defs.appendChild(rect); svg.appendChild(defs); rect = document.createElementNS(svgns, "rect"); rect.setAttribute("x", 0.1); rect.setAttribute("y", 0.1); rect.setAttribute("width", 99.8); rect.setAttribute("height", 29.8); rect.setAttribute("fill", "none"); rect.setAttribute("stroke", "blue"); rect.setAttribute("stroke-width", 0.2); svg.appendChild(rect); // 問題はここから… var use_node = document.createElementNS(svgns, "use"); use_node.setAttribute("x", 20); use_node.setAttribute("y", 10); use_node.setAttribute("xlink:href", "#MyRect"); // ←注目! svg.appendChild(use_node); });

しかしこの実装だと、<use> 要素によって表示されるはずの中央の黒い矩形が表示されません。

この件について Twitter 上でつぶやいていたところ@DEFGHI1977 さんという方から以下のような返信を頂きました。

はて? <use> 要素の DOM インタフェースは SVGUseElement なのですが、 W3C の仕様勧告にも、あるいは MDN のドキュメントにも、 instanceRootanimatedInstanceRoot といった属性はあるものの、 href という属性については書かれていません。

しかし、実際に Firebug 上で SVGUseElement のインスタンスをウォッチしてみると、確かに XMLAnimatedString インスタンスとして href 属性が存在しており、逆に仕様勧告には記述されている instanceRootanimatedInstanceRoot といった属性は存在しませんでした。

そして、 static に記述したインライン SVG 内の <use> 要素を document.getElementById() して取得した SVGUseElement インスタンスには href.baseVal 属性に然るべき URI 文字列が設定されているのに対し、 JavaScript から生成して <svg> 要素に appendChild() した方の <use> 要素を document.getElementById() して取得した SVGUseElement インスタンスには href.baseVal 属性に空文字列が設定されていました。つまり、 document.createElementNS() によって生成した SVGUseElement インスタンスに対して setAttribute("xlink:href", uri) しても、 href.baseVal 属性に値が設定されず、結果として表示にも反映されなかった、ということのようなのです。

そこで、 @DEFGHI1977 さんが仰っていた通り、先ほどのコード例の

  var use_node = document.createElementNS(svgns, "use");
  use_node.setAttribute("x", 20);
  use_node.setAttribute("y", 10);
  use_node.setAttribute("xlink:href", "#MyRect");
  svg.appendChild(use_node);

の部分を、

  var use_node = document.createElementNS(svgns, "use");
  use_node.setAttribute("x", 20);
  use_node.setAttribute("y", 10);
  use_node.href.baseVal = "#MyRect";  // ←修正!!
  svg.appendChild(use_node);

と書き直してみたところ、なるほど確かに中央の黒い矩形も表示され、期待通りの動作が得られたのでした (Windows 版の firefox および safari にて確認)。

しかしこの結果はちょっと釈然としないものがあります。まず、仕様勧告には書かれていなかった href 属性を用いるのは、標準から外れた解決策なのではないかという点です。特定のブラウザにおける現状の実装に依存した解決策であるならば、それは Web 標準化の観点からすれば選択すべきではない方法でしょう。しかしこの点については、私の標準に対する調査が甘いのかも知れません。より新しいバージョンではこのような仕様になっている可能性、あるいはより古いバージョンでこうした仕様になっており、現状多くのブラウザがそうした古いバージョンを採用している可能性もあるかも知れません。

setAttribute() メソッドを利用せずに、インタフェースが用意する属性を直接弄るというやり方にも抵抗があります。 HTML の DOM とは異なり、 SVG の DOM では、多くの SVG 要素を表すインタフェースにおいて、属性は参照用として用意されており、仕様勧告では readonly とされています。実際の所、 readonly とされている属性値は直接書き換えて動作としても反映できるケースが多いようですが (SVGRectElement インスタンスの width.baseVal.newValueSpecifiedUnits() で矩形の幅が変更できちゃうとか)、 readonly とされている以上、そういった実装は (やはり Web 標準化の観点からすれば) 避けるべきなのではないでしょうか。

今回のケースについても、実は必ずしも setAttribute("xlink:href", uri) が期待通りに動かないわけではありません。 document.createElementNS() して生成した SVGUseElement インスタンスには効果がありませんでしたが、 static なインライン SVG 内の <use> 要素に対して document.getElementById() して取得した SVGUseElement インスタンスに対してはちゃんと動作するのです。以下のサンプルでは中央には黒い矩形ではなく赤い楕円が表示されます。

<!DOCTYPE html>
<html>
<head>
	<title>use 要素を DOM から扱うサンプル</title>
	<meta charset="UTF-8">
	<script type="text/javascript"><!--
var svgns = "http://www.w3.org/2000/svg";

window.addEventListener("load", function() {
	var use_node = document.getElementById("myUse");
	use_node.setAttribute("xlink:href", "#MyEllipse");
});
//	--></script>
</head>
<body>
<div>
<svg width="10cm" height="3cm" viewBox="0 0 100 30">
  <defs>
    <rect id="MyRect" width="60" height="10"/>
    <ellipse id="MyEllipse" cx="30" cy="5" rx="30" ry="8" fill="red"/>
  </defs>
  <rect x=".1" y=".1" width="99.8" height="29.8" fill="none" stroke="blue" stroke-width=".2" />
  <use id="myUse" x="20" y="10" xlink:href="#MyRect" />
</svg>
</div>
</body>
</html>

この辺、どなたかきっちり説明できる詳しい方がいらっしゃったら、是非ご教示いただきたく願います…。m(_ _)m


(直後に追記)

…といった内容のブログ記事を書いて 1分と経たないうちに、 @teramako 大先生から以下のご指摘を頂きました。

うわーなるほど、そういうことなら超納得です… 早速最初のサンプルの先頭に

var xlinkns = "http://www.w3.org/1999/xlink";

を追加し、

  var use_node = document.createElementNS(svgns, "use");
  use_node.setAttribute("x", 20);
  use_node.setAttribute("y", 10);
  use_node.setAttribute("xlink:href", "#MyRect");
  svg.appendChild(use_node);

の部分を

  var use_node = document.createElementNS(svgns, "use");
  use_node.setAttribute("x", 20);
  use_node.setAttribute("y", 10);
  use_node.setAttributeNS(xlinkns, "href", "#MyRect");
  svg.appendChild(use_node);

に書き換えてあげたところ、ちゃんと期待通りに黒い矩形が表示されました。

DOM の名前空間に対する理解が浅かったのが敗因だったわけですね。お恥ずかしい… (///) しかしブログに書いてみて本当によかった。晴れて解決、めでたしめでたしなのであります。


(さらに追記2)

@DEFGHI1977 さまからも追加の情報頂きました。

継承関係は追っていたつもりだったんですが完全に見落としていたみたいです… こちらも情報 thanks です!

2012 年 8 月 10 日 by 村山 俊之

タグ: , ,

コメントをどうぞ