テキストの取得と変更

中心はやっぱり.firstChild.nodeValue

最少の知識で操作するDOMでも述べたように、テキストの取得と変更のおすすめは、.firstChild.nodeValueです。

これでほとんどの要求が満足させられることを説明し、その他の方法についても若干述べたいと思います

.firstChild.nodeValueを使う条件の確認

.firstChild.nodeValue を使う方法は、ターゲットとなるエレメントノードに変更したい文字列だけが入っていることを想定しています。

これは2つのことを言っているのです。

(1)必ず文字列が入っていること。最初は空にしておきたいなら、半角スペースをいれるか、そこで改行しておかないとテキストノードがない状態になるのでうまくいきません。

(2)他のエレメントノードが入っていないこと。firstChildがテキストノードでなければ、nodeValueを得たり、設定したりできません。テキストの途中にエレメントノード(<b>とか<br>とか)があると、それ以降が対象外になってしまいます。

他の方法でも読み取る範囲にエレメントノードが含まれているときには、それぞれ困難が生じますし、書き込むときには「要素の作成と追加」の併用でしのげます。なによりも「授業で使う...」という自分でhtmlもjavascriptも書くという状況では、htmlの設計でどうにでもできることです。

.firstChild.nodeValue を使うことで、ブラウザによる動作の違いの心配がなくなるのは最大のメリットです。

ということで再掲載します。

一般的にjavascriptで文字列を表示したい場合は次のようなタグを作っておいてidで場所を特定し、そこに表示するということで十分でしょう。

<p id="result">結果はここに表示されます</p>

初期状態で空白にしたければ、スペースを入れます。

<td>計算結果:</td><td id="sum"> </td>

計算結果が数値であっても、自動的に文字列にして入れてくれます。

.firstChild.nodeValueの意味

firstChild(=最初の子)は、Nodeのプロパティで、Node.firstChild は、対象となるNodeを親にする最初の子ノードを返します。これもNodeとなります。ノードがテキストノードだと Node.nodeValue で中の文字列を表します。

Node.firstChild :最初の子ノード。なければ null
Node.nodeValue  :テキストノード等の値(getとset両方に使う)。エレメントノードの値は null

これらはNodeインターフェースのプロパティと呼ばれるものです。

エレメントノード(span)
<span id="ex02">

ex02というidを持つspan要素です

</span>

テキスト

firstChild(テキストノード)

という構造です。

javascriptを使って説明しておきましょう。


<span id="ex02">ex02というidを持つspan要素です</span>

簡単な例

Node.nodeValueプロパティで値の読み出せますし、値をセットすることもできます。javaで言えばインスタンスのフィールド値でしょう。両方できることを一度で示すため、テキストの交換をしてみます。

(1) ナイターはなんの説明もなくナイトゲームと言われるようになった。

(2) ロスタイムはなんの説明もなくアディショナルタイムと言われるようになった。

(3) ホームページもウェブページと言われるようにならないものか。

<p id="nd01">(1) ナイターはなんの説明もなくナイトゲームと言われるようになった。</p>
<p id="nd02">(2) ロスタイムはなんの説明もなくアディショナルタイムと言われるようになった。</p>
<p id="nd03">(3) ホームページもウェブページと言われるようにならないものか。</p>
<p><button type="button" onclick="changetext()">テキストの交換</button></p>
<script>
function changetext(){
    var tnd01 = document.getElementById('nd01').firstChild;
    var tnd02 = document.getElementById('nd02').firstChild;
    var tnd03 = document.getElementById('nd03').firstChild;
    var tmptxt = tnd01.nodeValue;
    tnd01.nodeValue = tnd02.nodeValue;
    tnd02.nodeValue = tnd03.nodeValue;
    tnd03.nodeValue = tmptxt;
}
</script>

nodevalueではなく、nodeを交換してもいいじゃないかと思われるかもしれません。この例のようにfirstChildしかない状態なら問題ないと考えられますが、2つ以上の子ノードがあると格納する位置が変わってしまうかもしれません。今回はそこには首を突っ込まないことにします。

それより、複数の子ノードがある時のことを考えたいと思います。

テキストに他のエレメントノードが含まれる場合

複数の子ノードがあっても全部エレメントノードならば、それぞれidをつけることができますから、問題になるとしたらテキスト内にエレメントノードが含まれる場合でしょう。

例えばこんな状況です。

エレメントノード(p)
<p id="ex03">

ex03というidを持つp要素に

<strong>

strongタグに囲まれた要素

</strong>

が入っています

</p>

テキスト

テキスト

テキストノード(firstChild) エレメントノード(strong) テキストノード

テキスト

テキストノード

この場合図に示したように、テキストノード、エレメントノード(strong)、テキストノードという3つのノードがあると解釈をします。

.firstChild.nodeValue は、最初の子ノードがテキストノードであった場合に、そのテキストノードの内容だけを書き換えて、2つ目以降の子ノードには影響を与えません。

実際にやってみましょう。strongには文字に色を付けています。

ex03というidを持つp要素にstrongタグに囲まれた要素が入っています

プログラムです。

<p><button type="button" onclick="chgex03()">firstChildのテキストの交換</button></p>
<script>
var alttxt03 = "ここがp要素のfirstChildです。";
var orgtxt03 = "";
function chgex03(){
  var pelement = document.getElementById('ex03');
  var tmptxt = pelement.firstChild.nodeValue;
  if (orgtxt03 == "") orgtxt03 = tmptxt; 
  if (alttxt03 != tmptxt) {
     pelement.firstChild.nodeValue = alttxt03;
  } else {
     pelement.firstChild.nodeValue = orgtxt03;
  }
}
</script>

functionの外の文はload中に実行されます。グローバル変数のような扱いになります。load中にgetElementById()をしてもDOMが完成していない場合がありますので、orgtxt03=""にしています。

secondChildはないけどlastChildはあります

lastChildも古くから標準になっているので安心して使えます。私はほとんど使う機会はなかったのですけど。

ex04というidを持つp要素にstrongタグに囲まれた要素が入っています

プログラムです。

<p><button type="button" onclick="chgex04()">lastChildのテキストの交換</button></p>
<script>
var alttxt04 = "ここがp要素のlastChildです。";
var orgtxt04 = "";
function chgex04(){
  var pelement = document.getElementById('ex04');
  var tmptxt = pelement.lastChild.nodeValue;
  if (orgtxt04 == "") orgtxt04 = tmptxt; 
  if (alttxt04 != tmptxt) {
     pelement.lastChild.nodeValue = alttxt04;
  } else {
     pelement.lastChild.nodeValue = orgtxt04;
  }
}
</script>

n番目のChildは危険

Node.childNodes というプロパティは使えます。

var pelement = document.getElementById('ex03');
var children = pelement.childNodes;
  for(var i=0;children.length>i;i++){
     children[i].....

とできますが、children[i]がテキストノードであるかエレメントノードであるかでプログラムを分岐する必要があり、htmlの方を書き換えた時の対応も面倒になります。あまりおすすめできません。

特に、エレメントノードが連続したときに、htmlファイル上で改行するとその改行がテキストノードになってしまう場合があります。これはブラウザによります。これはかなり厄介です。htmlファイル上の改行は表示に影響がないですから、htmlの書き換え時に気軽にやってしまいがちです。それでjavascriptの動作に支障が出た場合に、原因が特定しにくくなります。

n番目のChildのは代替策は

おすすめは、「全部をspanでくくる」です。

<p id="ex05">
<span>ex05というidを持つp要素に</span>
<span><strong>strongタグに囲まれた要素</strong></span>
<span>が入っています</span></p>

さらにstrongにしたかった視覚効果上の理由があるのでしょうから、こうします。

<p id="ex05">
<span>ex05というidを持つp要素に</span>
<span class="strongstyle">strongタグに囲まれた要素</span>
<span>が入っていたのもの代替です</span></p>

「これではstrongにしたのは視覚効果だけが目的ではない。大切だという「意味」でマークアップしたんだ」という方、すみませんが別の方法を考えてください。

全部がエレメントノードですから、forで同じ扱いをすることができます。

var pelement = document.getElementById('ex05');
var children = pelement.getElementsByTagName("span");
  for(var i=0;children.length>i;i++){
     children[i].firstChild.nodeValue....

では実際にやってみます。3つのspan全部書き換えます。上記のようにspanごとに改行していません。改行すると半角スペース分空いてしまいます。これは単語をスペースで区切る言語に合わせた仕様です。エレメントノード間で改行するとテキストノードができるという仕様は、ここから考えると理由がありますね。

ex05というidを持つp要素にstrongタグに囲まれた要素が入っていたのもの代替です

プログラムです。

<p><button type="button" onclick="chgex05()">全部のspan要素のテキスト交換</button></p>
<script>
var orgtxt05 = new Array(0);
function chgex05(){
  var pelement = document.getElementById('ex05');
  var children = pelement.getElementsByTagName("span");
  if(orgtxt05.length==0){
    for(var i=0;children.length>i;i++){
      orgtxt05.push( children[i].firstChild.nodeValue );
    }
  }
  for(var i=0;children.length>i;i++){
    if (orgtxt05[i] != children[i].firstChild.nodeValue ) {
      children[i].firstChild.nodeValue = orgtxt05[i];
    } else {
      children[i].firstChild.nodeValue = i+"番目のspanです";
    }
  }
}
</script>

改行の入った文字列

実際にはエレメントノードを挟んだテキストを書き換えようというのは、余り必要にならないのですが、<br>を挟めたくなったことは一度あります。RGBの値で、10進数、16進数、% を縦に並べたかったのです。

innerHTMLを使うという手もあることはあるのですが、spanの配列にしました。

<td><span>10進数</span><br><span>16進数</span><br><span>%</span></td>

<br>が入っていても、

var children = pelement.getElementsByTagName("span");

でspan要素だけの配列になります。これは楽ちん。

.firstChild.nodeValue以外の選択肢

2009年ごろに調査した時の印象ではIEの独自機能でmozilla系では使えないinnerHTMLと、標準ではあるけれどIEで使えないtextContentというものでした。今(2018年)ざっと調べた(脚注)ところでは初心者向けでinnerHTMLをテキストのみで使うもの、ちょっと上級者向けでinnerTextとtextContentを比較しているもの、innerHTMLの使い方に注意を促すものなどさまざまですが、古いIE(8ぐらいまでの)でもなければ、どれも使えるようです。

そこで、試してみます。

ex06はex03の構造に戻しています

ex06というidを持つp要素にstrongタグに囲まれた要素が入っています

出力はここ

firstChild.nodeValue以外は、対象となるエレメントノードに複数のノードが含まれていても全部のテキスト部分を読み込みます。

書くときにも、firstChild.nodeValueは最初の子だけですが、その他は対象ノード全体を書き換えてしまいます。

innerHTMLはstrongタグを含めてコピーしますので、innerHTMLのあとにfirstChild.nodeValueをするとstrongタグ以下は残ってしまいます。

最初の子だけを書き換えるfirstChild.nodeValueよりも、全体を書き換えるtextContentやinnerTextの方が初心者には理解しやすいかもしれません。ただ、要求が高くなったときに対応しやすいのはfirstChild.nodeValueだと思います。

innerHTMLはクロスサイトスクリプティングへの注意が必要であるとともに、文書の構造を変化させるので、createElement(), addChild()などの知識がないままに使うのは混乱を生むかもしれないと思います。

結論として、firstChild.nodeValue を工夫しながら使うか、高度な書き換えが必要であれば、innerHTMLではなく、createElement(), addChild() を使ってエレメントノードを自分で作って加えることをお勧めします

脚注

MSDNでは内部検索から見つかるページは Dynamic HTML のもので、innerHTMLとinnerTextについてあっさりと書いてあるだけです。DOMを探すべきかと「HTML と DOM のリファレンス」を探し出したのですが、そこにある基本的な DOM リファレンスのリンクはクリックすると、Web APIs | MDNが出てきます。

MDNでは、Specificationやブラウザの対応バージョンなども書いてあって参考になります。innerHTMLについてはクロスサイトスクリプティングの危険性について説明されています。

W3Cのチュートリアルは要素の内容の書き換えに、innerHTMLを使っていて、初心者にこれから紹介するのは手軽だけれど、ちょっと危険と思わせられます。

innerHTMLがW3CやWHATWGの標準になったという記述も見た気がするのですが、改めて探すと見当たらず、W3CのDOM4や WHATWGの Living Standard からも nodeValueとtextContents が見つかるだけです。W3C Editor's Draft 03 November 2017 の DOM Parsing and Serialization には書かれています。

MDNによれば、

innerHTMLElementのプロパティ前述の DOM Parsing and Serialization で初めて定義されたもの
innerTextNodeのプロパティHTML Living Standard
textContentNodeのプロパティDOM Level 3 で定義されていて IEは9から対応
nodeValueNodeのプロパティDOM Level 2 で定義