arinoth's memo

arinothのメモ

Meteorで非公開ツール用のログインフォームを作る(その2)

前回に引き続いて非公開Webアプリ用のログインフォームの作成をば。ログイン中の画面が表示されるところまで作ったので、この画面を折りたためるようにして追加のユーザーを作成できるようにします。

f:id:arinoth:20140116020215p:plain

f:id:arinoth:20140116020225p:plain

f:id:arinoth:20140116020238p:plain

折りたたみ、つまり要素の一時的な表示/非表示は、HTMLを書き換えるMeteorのテンプレートでやると無駄が多そうなので、jQueryのshow/hideを使ったほうがよさげです。 meteorでは、次のコマンドで簡単にjQueryが組み込めます(どうも最新バージョンではなさげですが)。

meteor add jquery

jQueryを使うときの注意もろもろ

jQueryを組み込むのは楽なんですが、実際に使おうとすると色々と勝手が違うことに気付きます。

まず、初期状態ではフォームが折りたたまれた状態にしたいのですが、MeteorではHTMLを動的に生成するので、jQueryで操作する対象の要素が確実に存在するタイミングでhide()などを実行しないといけません。テンプレートがレンダリングされたときに呼び出されるrenderedというコールバック関数があるので、これの中に処理を書いておけばよさそうです。

2つめはjQueryのイベント処理が利用できないらしいということ。試しにやってみましたがダメでした(追記:どうもこれは勘違いっぽいです。イベントが使えなかったらjQueryプラグインの大半が動かないし、そんははずはない)。 それっぽい英語の記事(Using Jquery $(this) in Meteor)を見つけたので、どうやらイベント処理はMeteor側で書いたほうがいいようです。

client/logging.html(ファイル名を変えました)

<template name="logincontrol">
    <ul>
        <li><a id="logout">ログアウト</a></li>
        {{#if superuser}}
        <li class="expandable"><a>新規ユーザー作成</a></li>
        {{> createuserform}}
        <li class="expandable"><a>ユーザー管理</a></li>
        <div>管理画面</div>
        {{/if}}
    </ul>
</template>

client/logging.js(ファイル名を変えました)

//ログイン後のコントロール
Template.logincontrol.events({
  'click #logout': function (event) {
    Meteor.logout();
  }
});
Template.logincontrol.superuser = function(){
  return Meteor.user().profile.superuser;
};
//隠しコントロールの表示/非表示
Template.logincontrol.rendered = function(){
  $('.expandable').next().hide();
};
Template.logincontrol.events({
  'click .expandable': function (event) {
    $(event.currentTarget).next().toggle();
  }
});

テンプレートの再レンダリングに注意

ユーザー作成のテンプレートもそのまま流用できたので楽勝……かと思いきや、試しにフォームに文字を入力するとなぜか勝手にフォームが折りたたまれてしまう現象が。

しばらく意味がわからなかったのですが、実はフォームのバリデーション(ユーザー名の重複チェックとパスワードの一致チェック)の結果をMeteorのテンプレートの機能で表示させていたのがいかんようです。つまり、バリデーションの結果を変更するためにHTMLが書き換わると、それが親テンプレートのrenderdを呼び出してしまい、jQueryのhide()が呼び出されてしまうと。公式リファレンスのrenderedの項をよく読んだら、「〜each time any part of the template is re-rendered.」って書いてあります。

というわけで、バリデーションの結果表示などもjQueryのshow()/hide()を使う形に書き換えます。 crient/logging.htmlのcreateuserformテンプレートに警告用のテキストを追加。

<template name="createuserform">
    <div class="form-signin" role="form">
        <dl>
            <dt>UserName(半角英数)</dt>
            <dd><input type="text" id="newusername">
            <span class="alert" id="alert_username">すでに使われています(not unique)</span>
            </dd>
            <dt>UserFullName(日本語可)</dt>
            <dd><input type="text" id="newuserfullname"></dd>
            <dt>Password</dt>
            <dd><input type="password" id="newuserpassword1"></dd>
            <dt>Password(確認)</dt>
            <dd><input type="password" id="newuserpassword2"></dd>
            <span class="alert" id="alert_password">一致していません(not the same)</span>
            <dt><input type="checkbox" id="chk_superuser"></dt>
            <dd>スーパーユーザー</dd>
        </dl>
        <button id="btn_createnewuser" class="btn btn-lg btn-primary btn-block">Create</button>
    </div>
</template>

crient/logging.jsでテンプレートのレンダリング時に警告を隠すようにして、

//ユーザー作成フォームの警告を隠す
Template.createuserform.rendered = function () {
  $('.alert').hide();
};

keyup時にバリデーションしてだめだったらjQueryのshow()で警告を表示するように……。

//createuserformのイベント処理
Template.createuserform.events({
  //ユニークネーム検出
  'keyup #newusername':function(event){
    var val = event.currentTarget.value;
    Meteor.call('isUniqueName', val, function (error, result) {
      if(result == true) $('#alert_username').hide();
      else $('#alert_username').show();
    });
  },
  //パスワード一致検出
  'keyup #newuserpassword1, keyup #newuserpassword2':function(event){
    var pass1 = $('#newuserpassword1').val();
    var pass2 = $('#newuserpassword2').val();
    // console.log(val1 + ':' + val2);
    if(pass1 != pass2) $('#alert_password').show();
      else $('#alert_password').hide();;
  },
……後略……

サーバ側とクライアント側のどちらで実行するかで関数の動作が違う

ついでに、Accounts.createUser()がサーバ側で実行されるように変更。クライアントで実行するとユーザー作成後に新しいユーザーで再ログインする仕様なのですが、スーパーユーザーの人が普通ユーザーをまとめて作成する使い方を想定しているので。これはMeteor.methods()とMeteor.call()をかませばいいはず。

//createuserformのイベント処理
Template.createuserform.events({
……中略……
  //ユーザー作成
  'click #btn_createnewuser':function(event){
    var user = $('#newusername').val();
    var fullname = $('#newuserfullname').val();
    var pass1 = $('#newuserpassword1').val();
    var supermode = $('#chk_superuser').prop('checked');
    if(Session.get('uniquename')==false || Session.get('notvalidpass')==true){
      return;
    }
    if(user=='' || pass1=='' || fullname =='') return;
    Meteor.call('createUserOnServer',
        user, pass1, fullname, supermode, function (error, result) {
            if(!error){
                $('#newusername').val('');
                $('#newuserfullname').val('');
                $('#newuserpassword1').val('');
                $('#newuserpassword2').val('');
                $('#chk_superuser').prop('checked', false);
                showLoginFormStatus('ユーザー'+result+'を作成しました');
                Meteor.call('isFirstUser', 1, function (error, result) {
                  Session.set('firstuser', result);
                });
            } else {
                showLoginFormStatus('ユーザー作成に失敗しました');
            }
    });
  }
});

server/bootstrap.js

Meteor.methods({
……中略……
    //ユーザー作成
    createUserOnServer:function(user, pass, fullname, supermode){
        console.log('createUser '+user);
        return Accounts.createUser({
          username: user,
          password: pass,
          profile: {
            name: fullname,
            superuser: supermode
          }
        });
    }
});

公式ドキュメントを読むと、パスワードなしメールアドレスありでサーバ側で実行すると、パスワード設定用のメールをユーザーに送るとか何かすごい色々やってくれるようです。

ここまでの成果(ZIP)