WebDesign Dackel

お手軽MVVMなVue.jsを使って、ユーザ管理アプリ的なものを作ってみた

お手軽MVVMなVue.jsを使って、ユーザ管理アプリ的なものを作ってみた

Hatena0
Google+0
Pocket0
Feedly0

今年に入って注目を浴びている?フロントエンドフレームワークのVue.jsを初めて触ってみました。

とりあえず何か作ってみる

手始めに何か作りたいと思い、よくあるTODOアプリでも良かったのですが、どうせなら実際のアプリケーション構築を想定したものが良いと思い、Webアプリケーション内のユーザ管理的なサンプルアプリを作ってみました。
サンプルコードは少し長いので一番下にひと通り載せています。

DEMO

お手軽MVVMなVue.jsを使って、ユーザ管理アプリ的なものを作ってみた

http://webdesign-dackel.com/demo/vuejs/

ユーザの新規登録、編集、削除(CRUD)をひと通り実装してみました。左側のユーザリストはダブルクリックでも編集状態になります。
今回はとりあえずデータの永続化などは行っていません。(サーバサイドのAPIと連動して使うときはどんな感じに実装していくのがいいものなのでしょうか??)

感動したところ

データと見た目(DOM)が自動で連動していく様がとても楽しいです。しかも直感的で簡単に掛けます。

これまでは素のJSjQueryなどででデータと見た目の振る舞いをちょこちょこ操作していた部分を、完全にフレームワーク側に任せられる感じが素敵です!

よくブログ等に、他のフレームワークに比べると日本語の情報が少ないとあり心配でした。
しかし公式に上がっているサンプルが、数こそ少ないですが基本的な部分を抑えるのには充分で良くまとまっている感じなので、あまり苦に感じませんでした。デザインもスッキリとしていて見やすかったです。

ちょっと残念だったところ

実際に作る前に、バリデーション周りはどうするんだろう?と思っていて調べたら「vue-validator」というプラグインがとても良さそうだったので、安心していました。
しかし、作り始めた時使っていたVue.jsのバージョンが0.11.xでプラグイン側が対応しておらず、エラーが出てしまっていました。。 サポートしているVue.jsのバージョンは0.10.3 – 0.10.5のようです。(2014年12月時点)

プラグインを使うためにバージョンを下げてしまうと、ドキュメントの情報を参照しづらくなってしまうので、今回はとりあえずプラグインは使わずになんとかしています。

ただ、Githubを見ると0.11.xへ対応したものを開発中であることが書いてあったのでそれまでは我慢して、プラグインの完成を楽しみにしていようと思いました。

2015.03.10 追記

気がついたらvue-validatorが0.11.xに対応していましたので早速触ってみました!

Vue.jsでバリデーション、vue-validatorを使ってみる

所感

僕は新しい技術にどうも疎くて、この手のフレームワークはBackbone.jsを少し触ったことのあるぐらいだったのですが、Vue.jsは割りと直感的に触れた印象でした。まだ少ししか触っていないので分からない事や使っていない機能(Custom ComponentやDirective)が沢山ありますが、もう少し触ってみて自由に使いこなせるようになりたいなと思いました。

まだまだ開発中ということもあり、バージョンによって書き方を変えるところがちょこちょことあるみたいですが、もっと注目を浴びて開発がガツガツと進み、安定してくれば凄く魅力的なフレームワークだなと感じます。
これからの機能改善や便利なプラグインの登場が非常に楽しみです。

大変参考になったサイト

vue.js
Vue.js v0.11の変更点(予定)まとめ – blog.koba04.com
Vue.jsから手軽に始めるJavaScriptフレームワーク
Vue.js概要?

サンプルコード

余分なところは省いてしまっていますが大体こんな感じです。

<!-- index.html -->
<div id="app-root">

  <div class="header">
    <h1>Vue.js Sample</h1>
  </div>

  <div class="contents">
    <form class="form-horizontal" v-on="submit : updateUser" v-validator>

      <div class="panel panel-default">
        <div class="panel-heading">
          <h2 class="panel-title">ユーザの{{editUserId == -1 ? "追加" : "編集"}}</h2>
        </div>
        <div class="panel-body">
          <div class="form-group" v-show="editUserId > -1">
            <label class="col-md-2 control-label">ID</label>
            <div class="col-md-10">
              <p class="form-control-static">{{user.id}}</p>
            </div>
          </div>
          <div class="form-group">
            <label class="col-md-2 control-label">ユーザ名</label>
            <div class="col-md-10">
              <p><input class="form-control input-lg" type="text" name="" v-model="user.name | nameValidator" placeholder="Your name"></p>
              <p class="text-danger v-fade" v-if="!validation.name" v-transition>{{validation.msg.name}}</p>
            </div>
          </div>
          <div class="form-group">
            <label class="col-md-2 control-label">ログインID</label>
            <div class="col-md-10">
              <p><input class="form-control" type="text" name="" v-model="user.login | loginValidator" placeholder="Login ID"></p>
              <p class="text-danger v-fade" v-if="!validation.login" v-transition>{{validation.msg.login}}</p>
            </div>
          </div>
          <div class="form-group">
            <label class="col-md-2 control-label">メールアドレス</label>
            <div class="col-md-10">
              <p><input class="form-control" type="text" name="" v-model="user.email | emailValidator" placeholder="exampla@mail.com"></p>
              <p class="text-danger v-fade" v-if="!validation.email" v-transition>{{validation.msg.email}}</p>
            </div>
          </div>

        </div>
        <div class="panel-footer text-right">
          <button class="btn btn-danger v-fade" type="submit" v-on="click : deleteUserById(editUserId)" v-if="editUserId > -1">削除</button>
          <button class="btn btn-primary v-fade" type="submit" v-if="isValid">{{editUserId > -1 ? "更新" : "追加"}}</button>
        </div>
      </div>

    </form>
  </div>

  <div class="sidebar">
    <div v-repeat="user : users"
    v-on="dblclick : selectUser(user)"
    class="list-item v-fade {{user.edit ? 'active' : ''}}">
    <dl>
      <dt>{{user.name}}</dt>
      <dd>{{user.email}}</dd>
    </dl>
    <button class="btn-edit" v-on="click : selectUser(user)"><i class="mdi-editor-mode-edit"></i></button>
    <button class="btn-delete" v-on="click : deleteUser(user)"><i class="mdi-content-clear"></i></button>
  </div>
</div>

</div>
// app.js

;(function(){

    var app = new Vue({
        el: "#app-root",

        data: {
            users: [
                {
                    id: 0,
                    login: "user1",
                    name: "ユーザ名1",
                    email: "example@mail.com",
                    edit: false
                },
                {
                    id: 1,
                    login: "user2",
                    name: "ユーザ名2",
                    email: "example@mail.com",
                    edit: false
                },
                {
                    id: 2,
                    login: "user3",
                    name: "ユーザ名3",
                    email: "example@mail.com",
                    edit: false
                }
            ],

            user: {
                id: -1,
                login: "",
                name: "",
                email: ""
            },

            editUserId: -1,

            validation: {
                login: false,
                name: false,
                email: false,
                msg: {
                    login: "未入力です",
                    name: "未入力です",
                    email: "未入力です"
                }
            },

            validator: {}
        },

        computed: {
            isValid: function(){
                var valid = true;
                for (var key in this.validation) {
                    if (!this.validation[key]) {
                        valid = false;
                    }
                }
                return valid;
            }
        },

        created: function(){
        },

        filters: {
            loginValidator: {
                write: function(val){
                    var validation = this.validation;

                    if( isEmpty(val) ){
                        validation.login = false;
                        this.validation.msg.login = "未入力です";
                    }else if( !isAlphaNumeric(val) ){
                        validation.login = false;
                        this.validation.msg.login = "半角英数字で入力して下さい";
                    }else{
                        validation.login = true;
                    }
                    return val;
                }
            },
            nameValidator: {
                write: function(val){
                    var validation = this.validation;

                    if( isEmpty(val) ){
                        validation.name = false;
                        validation.msg.name = "未入力です";
                    }else if( val.length < 4 ){
                        validation.name = false;
                        validation.msg.name = "4文字以上必要です";
                    }else{
                        validation.name = true;
                    }
                    return val;
                }
            },
            emailValidator: {
                write: function(val){
                    var validation = this.validation;

                    if( isEmpty(val) ){
                        validation.email = false;
                        validation.msg.email = "未入力です";
                    }else if( !isEmail(val) ){
                        validation.email = false;
                        validation.msg.email = "形式が正しくありません";
                    }else{
                        validation.email = true;
                    }
                    return val;
                }
            }
        },

        methods: {
            userMouseOver: function(){
                console.log("mouseover", arguments);
            },

            // ユーザの更新・作成
            updateUser: function(e){
                e.preventDefault();

                // 入力内容にエラーがあればスルー
                if( !this.isValid ){
                    return;
                }

                // 登録用ユーザ生成
                var user = {
                    id: this.user.id,
                    name: this.user.name,
                    login: this.user.login,
                    email: this.user.email,
                    edit: false
                };

                // create
                if( user.id < 0 || user.id === false ){
                    user.id = parseInt(this.users[ this.users.length - 1 ].id) + 1;
                    this.users.push( user );
                    this.releaseEditState();

                // update
                }else{
                    var currentUser = this.getUserById(user.id);
                    currentUser.name = user.name;
                    currentUser.login = user.login;
                    currentUser.email = user.email;
                }
            },

            // ユーザの選択
            selectUser: function(user){
                // 編集中のユーザなら、編集を解除
                if( this.editUserId == user.id ){
                    this.releaseCurrentUser();
                    this.releaseEditState();

                // それ以外は編集状態へ
                }else{
                    this.releaseEditUser(this.getUserById(this.editUserId));
                    this.setCurrentUser(user);
                }
            },

            // idを指定したユーザを取得
            getUserById: function(id){
                var user = _.select(this.users, function(obj){
                    return obj.id == id;
                });
                return user[0];
            },

            // 指定のユーザを編集状態へ
            setCurrentUser: function(user){
                user.edit = true;
                this.user.id = this.editUserId = user.id;
                this.user.name = user.name;
                this.user.login = user.login;
                this.user.email = user.email;

                this.validation.login = true;
                this.validation.name = true;
                this.validation.email = true;
            },

            // ユーザの編集状態を解除
            releaseEditUser: function(user){
                if( user ) user.edit = false;
            },

            // 編集中のユーザを解除
            releaseCurrentUser: function(){
                if( this.editUserId > -1 ){
                    this.releaseEditUser( this.getUserById(this.editUserId) );
                }
            },

            // 編集状態を解除
            releaseEditState: function(){
                this.editUserId = -1;
                this.user.id = -1;
                this.user.name = "";
                this.user.login = "";
                this.user.email = "";

                this.validation.login = false;
                this.validation.name = false;
                this.validation.email = false;
                this.isValid = false;
            },

            // 指定したidのユーザを削除
            deleteUserById: function(id){
                this.deleteUser(this.getUserById(id));
            },

            // ユーザを削除
            deleteUser: function(user){
                var _this = this;

                // 編集中のユーザなら先に編集状態を開放
                if( _this.editUserId == user.id ){
                    _this.releaseEditUser(user);
                    _this.releaseEditState();
                }

                // 対象ユーザを削除
                _.some(_this.users, function(obj, i){
                    if( obj && obj.id == user.id ){
                        _this.users.$remove(obj);
                        return false;
                    }
                });
            }
        }
    });

    function isEmpty(value){
        return !value || value == "";
    }

    function isAlphaNumeric(value){
        return value.match(/^[0-9a-zA-Z_-]+$/) ? true : false;
    }

    function isEmail(value){
        var emailRe = /^(([^<>()[].,;:s@"]+(.[^<>()[].,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;
        return emailRe.test(value);
    }

}());