20200811のvue.jsに関する記事は11件です。

【Vue.js】Vue CLIグローバルフィルタの作成

【Vue.js】Vue CLIグローバルフィルタの作成

Vue CLIで作成したサイトでグローバルフィルタを使う方法。
グローバルでフィルタを使用するには、filter関数とfilter名の登録が必要になる。

下記ページを参考にさせていただきました。
https://teratail.com/questions/281227

手順

  1. srcフォルダ配下にfilterディレクトリとindex.jsを作成
  2. vueのインポート
  3. フィルタの作成
  4. main.jsで作成したフィルタをインポート
  5. フィルタを適用する

1. Sorceフォルダ配下にfilterディレクトリとindex.jsを作成する

filterのライブラリとしてディレクトリを作成する。

フォルダやファイルの場所や名前は任意だが、以下では、srcディレクトリ配下に作成する例とする。

・ディレクトリとファイル
*PJ > src > filter > index.js *

image.png


2. vueのインポート

フィルタ関数とIDを登録するために、まずはvue自信をインポートする。

index.js
import Vue from "vue"



<import Vue from "vue"について>
import Vue from "vue"は下記式と同義らしい。
何をしているかは明確ではないが、グローバルで使用できるVueインスタンスを生成しているとみられる。

import Vue from "vue"

const Vue = require('vue')

const vue = require('./node_modules/vue/dist/vue.runtime.common.js');
 
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./vue.runtime.common.prod.js')
} else {
  module.exports = require('./vue.runtime.common.dev.js')
}


3. フィルタの作成

Vue.filterを使い、フィルタ名と処理を定義する。
Vue.filter('フィルタ名', function(引数){return 処理})

1000桁区切りをするフィルタ1000separatorを作成する場合。

index.js
import Vue from "vue"

Vue.filter('1000separator', function(value) {
  return value.toLocaleString();
})


4. main.jsで作成したフィルタをインポートする。

srcディレクトリ配下のmain.jsファイルに移動し、filterディレクトリをインポートする。

main.js
import "./filter"


5. フィルタを適用する

フィルタを適用したい箇所に移動し、フィルタを呼び出す。
呼び出し方は、マスタッシュ展開を用いる方法と、v-bindを用いる方法の2種類。

(1)Mustache展開
{{式| フィルタ名}}

(2)v-bind
<要素 v-bind:属性="プロパティ名 | フィルタ名">



マスタッシュ展開でフィルタを使用する例。

マスタッシュ展開
 <em>${{number | 1000separator}}</em>

dataオプションのnumberプロパティに格納された数値を1000桁区切りにする。



以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Vue CLIでaxiosを使う方法

【Vue.js】Vue CLIでaxiosを使う方法

Vue CLIで外部APIからデータ取得するためにaxiosを使う方法について。

通常のhtmlファイル上でaxiosを使うのと方法が異なるので注意が必要。

手順

  1. プロジェクトにaxiosをインストールする
  2. main.jsにaxiosをインポートする
  3. axiosを使いたいファイルにaxiosをインポートする
  4. axiosを使った処理を記述する

1. プロジェクトにaxiosをインストールする

初めに、プロジェクト自体にaxiosをインストールする必要がある。

ターミナルでプロジェクトフォルダに移動後、下記を実行。

$ npm install axios

インストールが完了すると、「package.json」ファイルのdependencies(PJに依存するパッケージ)にaxiosが追加される。

image.png


2. main.jsにaxiosをインポートする

main.jsにインポートすることで、他のファイルでもaxiosを呼び出せるようにする。

main.js
import axios from 'axios'

<以下補足>
Vue.use(axios)を記述するといったページも散見されたが、これを記述するとエラーが発生する。
そして、記載せずとも無事APIが動く。

Vue.use()はvue.js用のライブラリを読み込む場合に使用する。axiosはvue.js専用でないため読み込み不要。


3. axiosを使いたいファイルにaxiosをインポートする

axiosを実行したいファイルのscriptタグ内で、axiosをインポートする。

今回はBitcoinAPI.vueファイルに記述。

BitcoinAPI.vue
import axios from 'axios'


4. axiosを使った処理を記述する

scriptタグのexport defaultオプション内に、axios.get('APIエンドポイントのURL')を使って処理を記述する。

export-default
<script>
import axios from 'axios'

export default {
  name: 'BitcoinAPI',

//API格納用のプロパティ
  data: function(){
    return{
      data:{
        bpi: null
      }
    }
  },

//axiosによるデータ取得処理
  mounted: function(){
    axios.get('https://api.coindesk.com/v1/bpi/currentprice.json')
    .then(function(response){
        //デバッグ用にconsoleに出力
        console.log(response.data.bpi)
        this.bpi = response.data.bpi
    }.bind(this))
    .catch(function(error){
        console.log(error)
    })
}
}  



▼取得したデータがconsoleに表示できている。

image.png

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】Nuxt実践編:ユーザーアイコン表示の切り替えをしよう

? この記事はWP専用です
https://wp.me/pc9NHC-ye

前置き

今回は簡単な実践編です??
作るものはこちら!

スクリーンショット 2020-08-09 9.28.22.png

マイページのあるサイトで
⬆️ユーザーアイコンの初期状態と
⬇️画像を設定した時の
表示の切り替えをやってみましょう??

コンポーネント間のやりとりや
演算子の復習になるのでぜひチャレンジしてください??

今回のような簡単な実践編も
今後増やしていこうと思ってます?

Let's try!

ディレクトリ

file
components/
--| atom/
----| icons/
------| IconUser.vue
--| molecule/
----| items/
------| ItemUserIconName.vue

pages/
--| user.vue

アトミックデザインに基づき作成します?
atomはアイコン部分。
他のmoleculeにも使い回す前提です。
moleculeはアイコンとユーザーの名前を追加したものです。

アドミックデザイン

要素の大きさごとにファイルを分けます?‍♀️
コンポーネントがどこにあるのか
分かりやすくなります?
大きさの分類はここが参考になります!
Atomic Designとは

atom

構成

IconUser.vueを作成しましょう?‍♀️
初期アイコンと、
アイコンが設定された場合の
表示を切り替えます。
アイコン画像は親で設定します。

背景は縦横64px
アイコンは縦横24px

アイコンはiconmonstrのsvgを使用しています?
https://iconmonstr.com/user-5-svg/

それではやってみましょう!!!

ticktack…

まずは切り替えをするので
v-ifとv-elseが使えますね…
親で画像を設定するのでpropsをstringで渡します。
stringを真偽値にしてv-ifとv-elseにしたい…

それではコードを見てみましょう✨?

解説

? 続きはWPでご覧ください?
https://wp.me/pc9NHC-ye

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【 Vue.js】Vue CLI 新規ページの追加方法

【 Vue.js】Vue CLI 新規ページの追加方法

Vue CLIで作成したサイトに新たにページを追加する方法。

手順

  1. viewsフォルダ配下に新規ファイルを作成する
  2. ルーティングを追加
  3. router-linkタグでリンクを追加

1. viewsフォルダ配下に新規ファイルを作成する

viewsフォルダ配下にページを作成する。ファイル名はコンポーネントの作成規則に則り大文字始まり、キャメルケースとする。

以下、BitcoinAPI.vueを作成場合。

image.png

2. ルーティングを追加

routerフォルダのindex.jsファイルのルーティングに作成したページを追加する。

必須なのは、①pathオプションと②componentオプションの2つ。

①path:URLを記述。
②component:指定の記述で作成したファイルを指定。
component: () => import('../views/ファイル名')

nameオプションを追加しておくと、リンクを設置する際に、pathではなく、nameで指定した値も使える。

name:'ルーティングの名前'

index.js
  const routes = [
  {
    path: '/bitcoin-api',
    component: () => import('../views/BitcoinAPI.vue'),
  }
]


3. router-linkタグでリンクを追加

リンクを設置したいページのtemplateタグ内に、router-linkタグを設置する。

リンク先のpathはto属性に記述する。

<router-link to="相対パス">アンカーテキスト</router-link>

templateタグ
<router-link to="/bitcoin-api">Bitcoin API</router-link>

設置したリンクから、該当URLでページが開けば完了。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLIをインストールしたらcommand not found: vueになったから解決したやり方共有!

はじめに

本日2回目の投稿になってしまいました。
先ほど、Vue CLIを導入しようと思って、ターミナルを操作してたら、なんかひたすらcommand not found: vueになってちょっと手間取ったので、無事解決したやり方を、こちらで共有させて頂きます。

【利用環境】
MacBook pro(macOS Catalina バージョン10.15.6)
ターミナル(zsh)
node.js v12.18.2
npm 6.14.5

Vue CLIをインストールしてみる

ターミナルに次のコマンドを入力し、インストールを試みました。

npm install -g @vue/cli

しばらく待つと...

+ @vue/cli@4.5.2
added 1307 packages from 704 contributors in 29.079s

っていうのが出てきたので、「いけた!」と思ったんです。
しかし!

vue --version

とすると...

zsh: command not found: vue

となってしまって、「vue createできないじゃん!」ってなりました。

解決策

次のコマンドをターミナルに入力してください。

export PATH=~/.npm_global/bin:$PATH

すると...

vue —version
@vue/cli 4.5.2

はい、解決しました!!!無事、vue createもできました!

もし、Macを利用していて同じような問題に遭遇してしまった方の手助けになれればと思います!

参考資料

以下のStack Overflowの質問・回答を参考にさせて頂きました!!

https://stackoverflow.com/questions/58590890/how-to-fix-zsh-command-not-found-vue

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(2/2)

前回の投稿で、WiiリモコンとヌンチャクとバランスボードをNode.jsで動作させてMQTTに接続するところまでできました。( WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2) )

今回は、それをブラウザからSubscribeして、ボタンの押下状態を見たり、加速度センサやバランスボードの重量をリアルタイムに表示させたりしてみます。

ソース一式をアップしたGitHubはこちら
 https://github.com/poruruba/WiiRemocon

ブラウザページはこちら
 https://poruruba.github.io/WiiRemocon/html/

ブラウザからのMQTT接続

以下のライブラリを使います。

Eclipse Paho Javascript Client
 https://www.eclipse.org/paho/clients/js/

こんな感じです。

start.js
    connect_mqtt: function(){
        mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id );
        mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
        mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;

        mqtt_client.connect({
            onSuccess: this.mqtt_onConnect
        });
    },

    mqtt_onConnect: function(){
        console.log("MQTT.onConnect");
        this.connected = true;

        wii = new WiiClient(mqtt_client, this.topic_cmd);
        this.init_graph();
        mqtt_client.subscribe(this.topic_evt);
    },

主要なファイル構成

〇js/start.js
 Web画面の制御と、MQTT Subscribeして受信したデータの処理を行います。
 加速度やバランスボードの重量のグラフ表示やヌンチャクのスティックの状態の表示に、Chart.jsを利用しています。また、ボタン押下状態や拡張コントローラ接続状態の表示には、双方向データバインディングが便利なVue.jsを利用しています。また、CSSテンプレートはBootstrap v3.4.1を利用しています。

・Chart.js
 https://www.chartjs.org/

・Vue 2.x
 https://jp.vuejs.org/v2/guide/

・Bootstrap v3.4.1
 https://getbootstrap.com/docs/3.4/

〇js/wiiclient.js
 Wiiリモコンたちに処理を依頼するMQTTのPublish処理をします。また、start.jsで行う受信データの解析に有用な関数群を提供します。

Javascriptソースコード

メインとなるstart.jsのソースコードです。

start.js
'use strict';

//var vConsole = new VConsole();

const mqtt_url = "【MQTTブローカのURL(WebSocket接続】";
var mqtt_client = null;

const MQTT_CLIENT_ID = "browser";
const MQTT_TOPIC_CMD = 'testwii_cmd';
const MQTT_TOPIC_EVT = 'testwii_evt';

const UPDATE_INTERVAL = 200;

const NUM_OF_DATA = 50;
const NUM_OF_STICK_DATA = 3;

var timer = null;

var wii;

var acc_x, acc_y, acc_z;
var nck_evt;
var blc_evt;
var blc_temperature;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        mqtt_url: mqtt_url,
        update_interval: UPDATE_INTERVAL,
        chk_btns: [],
        chk_nck_btns: [],
        battery: 0,
        wii_type: "remocon",
        btaddress: "",
        battery: 0,
        reporting_mode: WIIREMOTE_REPORTID_BTNS,
        flags: [],
        remote_address: "",
        blc_calibration: null,
        blc_battery: false,
        blc_total_weight: 0,
        connected: false,
        leds : [],
        rumble: false,
        topic_cmd: MQTT_TOPIC_CMD,
        topic_evt: MQTT_TOPIC_EVT,
        client_id: MQTT_CLIENT_ID,
    },
    computed: {
    },
    methods: {
        change_rumble_led: function(){
            if(!this.connected)
                return;
            var value = 0;
            if( this.leds[0] ) value |= WIIREMOTE_LED_BIT0;
            if( this.leds[1] ) value |= WIIREMOTE_LED_BIT1;
            if( this.leds[2] ) value |= WIIREMOTE_LED_BIT2;
            if( this.leds[3] ) value |= WIIREMOTE_LED_BIT3;
            if( this.rumble ) value |= WIIREMOTE_RUMBLE_MASK;

            var data = [WIIREMOTE_REPORTID_LED, value ];
            wii.writaValue(data);
        },
        change_reporting_mode: function(){
            var data = [WIIREMOTE_REPORTID_REPORTINGMODE, 0x00, this.reporting_mode ];
            wii.writaValue(data);
        },
        stop_graph: function(){
            if( timer != null ){
                clearTimeout(timer);
                timer = null;
            }
        },
        init_graph: function(){
            if( timer != null ){
                clearTimeout(timer);
                timer = null;
            }

            var labels = [];
            for( var i = 0 ; i < NUM_OF_DATA ; i++ )
                labels.push( -(NUM_OF_DATA - i - 1) * this.update_interval );

            for( var i = 0 ; i < myChart_acc.data.datasets.length ; i++ ){
                myChart_acc.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_DATA ; j++ )
                    myChart_acc.data.datasets[i].data.push(NaN);
            }
            myChart_acc.data.labels = labels;

            for( var i = 0 ; i < myChart_nck_acc.data.datasets.length ; i++ ){
                myChart_nck_acc.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_DATA ; j++ )
                    myChart_nck_acc.data.datasets[i].data.push(NaN);
            }
            myChart_nck_acc.data.labels = labels;

            for( var i = 0 ; i < myChart_nck_stk.data.datasets.length ; i++ ){
                myChart_nck_stk.data.datasets[i].data = [];
                for( var j = 0 ; j < NUM_OF_STICK_DATA ; j++ )
                    myChart_nck_stk.data.datasets[i].data.push(NaN);
            }

            timer = setInterval(() =>{
                this.update_graph();
            }, this.update_interval);
        },
        update_graph: function(){
            myChart_acc.data.datasets[0].data.push(acc_x);
            myChart_acc.data.datasets[1].data.push(acc_y);
            myChart_acc.data.datasets[2].data.push(acc_z);
            myChart_acc.data.datasets[0].data.shift();
            myChart_acc.data.datasets[1].data.shift();
            myChart_acc.data.datasets[2].data.shift();
            myChart_acc.update();

            if( nck_evt ){
                myChart_nck_acc.data.datasets[0].data.push(nck_evt.acc_x);
                myChart_nck_acc.data.datasets[1].data.push(nck_evt.acc_y);
                myChart_nck_acc.data.datasets[2].data.push(nck_evt.acc_z);
                myChart_nck_acc.data.datasets[0].data.shift();
                myChart_nck_acc.data.datasets[1].data.shift();
                myChart_nck_acc.data.datasets[2].data.shift();
                myChart_nck_acc.update();

                myChart_nck_stk.data.datasets[0].data.push({ x: nck_evt.stk_x, y: nck_evt.stk_y });
                myChart_nck_stk.data.datasets[0].data.shift();
                myChart_nck_stk.update();
            }

            if( this.blc_calibration && blc_evt ){
                var weight = wii.calcurateBalanceBoard(blc_evt, this.blc_calibration);
                myChart_blc.data.datasets[0].data[0] = weight.topright;
                myChart_blc.data.datasets[1].data[0] = weight.bottomright;
                myChart_blc.data.datasets[2].data[0] = weight.topleft;
                myChart_blc.data.datasets[3].data[0] = weight.bottomleft;
                this.blc_total_weight = weight.total_weight;
                myChart_blc.update();
            }
        },
        mqtt_onMessagearrived: function(message){
            try{
                var topic = message.destinationName;
                if( topic == this.topic_evt){
                    var msg = JSON.parse(message.payloadString);
                    console.log(msg);
                    if( msg.rsp == WIIREMOTE_CMD_EVT){
                        var event = wii.parseReporting(msg.evt);
                        if(event.btns != undefined){
                            for( var i = 0 ; i < 16 ; i++ )
                                this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0);
                        }
                        if(event.acc_x != undefined) acc_x = event.acc_x;
                        if(event.acc_y != undefined) acc_y = event.acc_y;
                        if(event.acc_z != undefined) acc_z = event.acc_z;
                        if(event.battery != undefined) this.battery = event.battery;
                        if(event.flags != undefined){
                            this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false;     
                            this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false;     
                            this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false;     
                            this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false;     
                        }
                        if(event.report_id == WIIREMOTE_REPORTID_BTNS_EXT8 || event.report_id == WIIREMOTE_REPORTID_BTNS_EXT19 ||
                            event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_EXT16 || event.report_id == WIIREMOTE_REPORTID_BTNS_IR10_EXT9 ||
                            event.report_id == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 || event.report_id == WIIREMOTE_REPORTID_EXT21)
                        {
                            if( this.wii_type == "remocon"){
                                nck_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_NUNCHUCK, event.extension);
                                for( var i = 0 ; i < 2 ; i++ )
                                    this.$set(this.chk_nck_btns, i, (nck_evt.btns & (0x0001 << i)) != 0);
                            }else if( this.wii_type = "balance"){
                                blc_evt = wii.parseExtension(WIIREMOTE_EXT_TYPE_BALANCEBOARD, event.extension);
                                if( blc_evt.temperature != undefined) blc_temperature = blc_evt.temperature;
                                if( blc_evt.battery != undefined) this.blc_battery = blc_evt.battery;
                            }
                        }
                    }else
                    if( msg.rsp == WIIREMOTE_CMD_REQ_STATUS){
                        var event = wii.parseReporting(msg.status);
                        console.log(event);
                        if(event.btns != undefined){
                            for( var i = 0 ; i < 16 ; i++ )
                                this.$set(this.chk_btns, i, (event.btns & (0x0001 << i)) != 0);
                        }
                        if(event.acc_x != undefined) acc_x = event.acc_x;
                        if(event.acc_y != undefined) acc_y = event.acc_y;
                        if(event.acc_z != undefined) acc_z = event.acc_z;
                        if(event.battery != undefined) this.battery = event.battery;
                        if(event.flags != undefined){
                            this.flags[0] = ( event.flags & WIIREMOTE_FLAG_BIT_BATTERY_EMPTY ) ? true : false;     
                            this.flags[1] = ( event.flags & WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED ) ? true : false;     
                            this.flags[2] = ( event.flags & WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED ) ? true : false;     
                            this.flags[3] = ( event.flags & WIIREMOTE_FLAG_BIT_IR_ENABLED ) ? true : false;     
                        }
                    }else
                    if( msg.rsp == WIIREMOTE_CMD_READ_REG_LONG){
                        if( msg.offset == WIIREMOTE_ADDRESS_BALANCE_CALIBRATION){
                            this.blc_calibration = wii.parseBalanceBoardCalibration(msg.value);
                            console.log(this.blc_calibration);
                        }

                    }else
                    if( msg.rsp == WIIREMOTE_CMD_REQ_REMOTE_ADDRESS){
                        if(msg.address)
                            this.remote_address = wii.addr2str(msg.address);
                        else
                            this.remote_address = "";
                    }else if( msg.rsp == WIIREMOTE_CMD_ERR ){
                        console.error("WIIREMOTE_CMD_ERR: " + msg.error);
                    }else{
                        throw 'unknown rsp';
                    }
                }else{
                    console.error('Unknown topic');
                }
            }catch(error){
                console.error(error);
            }
        },
        mqtt_onConnectionLost: function(errorCode, errorMessage){
            console.log("MQTT.onConnectionLost", errorCode, errorMessage);
            this.connected = false;
            this.stop_graph();
        },
        mqtt_onConnect: function(){
            console.log("MQTT.onConnect");
            this.connected = true;

            wii = new WiiClient(mqtt_client, this.topic_cmd);
            this.init_graph();
            mqtt_client.subscribe(this.topic_evt);
        },
        connect_wii: function(){
            if(!this.connected)
                return;
            wii.connect(this.btaddress);
        },
        disconnect_wii: function(){
            wii.disconnect();
        },
        request_status: function(){
            if(!this.connected)
                return;
            wii.requestStatus();
            wii.requestRemoteAddress();
        },
        enable_extension: function(){
            if(!this.connected)
                return;
            wii.enableExtension(true);
        },
        update_calibration: function(){
            if(!this.connected)
                return;
            wii.readRegisterLong(WIIREMOTE_ADDRESS_BALANCE_CALIBRATION, 0x20);
        },
        connect_mqtt: function(){
            mqtt_client = new Paho.MQTT.Client(this.mqtt_url, this.client_id );
            mqtt_client.onMessageArrived = this.mqtt_onMessagearrived;
            mqtt_client.onConnectionLost = this.mqtt_onConnectionLost;

            mqtt_client.connect({
                onSuccess: this.mqtt_onConnect
            });
        },
        disconnect_mqtt: function(){
            if(!this.connected)
                return;
            mqtt_client.disconnect();
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );

var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: "acc_x",
            fill: false,
            data: []
        },{
            label: "acc_y",
            fill: false,
            data: []
        }, {
            label: "acc_z",
            fill: false,
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 255 * 4,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});
var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), {
    type: 'line',
    data: {
        labels: [],
        datasets: [{
            label: "acc_x",
            fill: false,
            data: []
        },{
            label: "acc_y",
            fill: false,
            data: []
        }, {
            label: "acc_z",
            fill: false,
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 255 * 4,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});
var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), {
    type: 'scatter',
    data: {
        labels: ["nck_stk"],
        datasets: [{
            label: "stick",
            data: []
        }]
    },
    options: {
        scales: {
            xAxes: [{
                ticks: {
                    suggestedMax: 255,
                    suggestedMin: 0,
                }
            }],
            yAxes: [{
                ticks: {
                    suggestedMax: 255,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.RdYlBu11'
            },
        }
    }
});
var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), {
    type: 'bar',
    data: {
        labels: [],
        datasets: [{
            label: "top_right",
            data: []
        },{
            label: "bottom_right",
            data: []
        },{
            label: "top_left",
            data: []
        }, {
            label: "bottom_left",
            data: []
        }]
    },
    options: {
        animation: false,
        scales: {
            yAxes: [{
                ticks: {
                    suggestedMax: 34,
                    suggestedMin: 0,
                }
            }]
        },
        plugins: {
            colorschemes: {
                scheme: 'brewer.Paired12'
            },
        }
    }
});

〇グラフの準備

以下の部分でグラフの準備をしています。あとは、リアルタイムに現在値で反映すればよいだけです。

 var myChart_acc = new Chart( $('#chart_acc')[0].getContext('2d'), {
 var myChart_nck_acc = new Chart( $('#chart_nck_acc')[0].getContext('2d'), {
 var myChart_nck_stk = new Chart( $('#chart_nck_stk')[0].getContext('2d'), {
 var myChart_blc = new Chart( $('#chart_blc')[0].getContext('2d'), {

加速度に線グラフ、ヌンチャクのスティックに散布図、バランスボードの重量に棒グラフを採用しています。
色の選択が面倒なので以下のプラグインを使わせていただきました。

chartjs-plugin-colorschemes
 https://nagix.github.io/chartjs-plugin-colorschemes/ja/

〇グラフ再描画の仕組み

グラフは固定の周期で更新をかけています。
setIntevalを呼び出しておいて、周期的にupdate_graphを呼んでいます。

一方、MQTTで送られてくる受信データであるデータレポーティングは、非同期に受信されてきますので、受信したデータは変数に格納しておきます。

したがって、受信タイミングと描画タイミングは一致していません。もしもっとスムーズに動きを見たければ、再描画間隔を短くしてみてください。デフォルトで200msecにしています。50msecにすると、ヌンチャクのスティックの状態がかなりスムーズに見えるようになります。

〇受信データの解析

MQTTでSubscribeして受信したレポーティングデータは、mqtt_onMessagearrivedが呼び出されて受領できるようにしています。解析のための関数は、wiiclient.jsにまとめておきました。

〇バランスボードの重量の表示

バランスボードの重量の表示には、キャリブレーションデータが必要です。
そのため、生データのデータレポーティングが受信されていても、キャリブレーションデータも受信された状態となって初めてグラフに反映するようにしています。

Wiiへの要求のためのユーティリティ

Wiiへの要求のための各種ユーティリティです。
さきほど述べましたが、Wiiへの要求のみで、応答はstart.jsの方で処理します。

wiiclient.js
'use strict';

const WIIREMOTE_CMD_EVT = 0x00;
const WIIREMOTE_CMD_ERR = 0xff;
const WIIREMOTE_CMD_CONNECT = 0x01;
const WIIREMOTE_CMD_DISCONNECT = 0x02;
const WIIREMOTE_CMD_WRITE = 0x03;
const WIIREMOTE_CMD_ENABLE_SOUND = 0x04;
const WIIREMOTE_CMD_ENABLE_EXTENSION = 0x05;
const WIIREMOTE_CMD_REQ_REMOTE_ADDRESS = 0x06;
const WIIREMOTE_CMD_READ_REG = 0x07;
const WIIREMOTE_CMD_WRITE_REG = 0x08;
const WIIREMOTE_CMD_REQ_STATUS = 0x09;
const WIIREMOTE_CMD_READ_REG_LONG = 0x0a;

const WIIREMOTE_EXT_TYPE_NUNCHUCK = 0x01;
const WIIREMOTE_EXT_TYPE_BALANCEBOARD = 0x02;

const WIIREMOTE_REPORTID_RUMBLE = 0x10;
const WIIREMOTE_REPORTID_LED = 0x11;
const WIIREMOTE_REPORTID_REPORTINGMODE = 0x12;
const WIIREMOTE_REPORTID_IR_ENABLE = 0x13;
const WIIREMOTE_REPORTID_SPEAKER_ENABLE = 0x14;
const WIIREMOTE_REPORTID_STATUS_REQUEST = 0x15;
const WIIREMOTE_REPORTID_WRITE = 0x16;
const WIIREMOTE_REPORTID_READ = 0x17;
const WIIREMOTE_REPORTID_SPEAKER_DATA = 0x18;
const WIIREMOTE_REPORTID_SPEAKER_MUTE = 0x19;
const WIIREMOTE_REPORTID_IR2_ENABLE = 0x1a;
const WIIREMOTE_REPORTID_STATUS = 0x20;
const WIIREMOTE_REPORTID_READ_DATA = 0x21;
const WIIREMOTE_REPORTID_ACK = 0x22;
const WIIREMOTE_REPORTID_BTNS = 0x30;
const WIIREMOTE_REPORTID_BTNS_ACC = 0x31;
const WIIREMOTE_REPORTID_BTNS_EXT8 = 0x32;
const WIIREMOTE_REPORTID_BTNS_ACC_IR12 = 0x33;
const WIIREMOTE_REPORTID_BTNS_EXT19 = 0x34;
const WIIREMOTE_REPORTID_BTNS_ACC_EXT16 = 0x35;
const WIIREMOTE_REPORTID_BTNS_IR10_EXT9 = 0x36;
const WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6 = 0x37;
const WIIREMOTE_REPORTID_EXT21 = 0x3d;

const WIIREMOTE_FLAG_BIT_BATTERY_EMPTY = 0x01;
const WIIREMOTE_FLAG_BIT_EXTENSION_CONNECTED = 0x02;
const WIIREMOTE_FLAG_BIT_SPEAKER_ENABLED = 0x04;
const WIIREMOTE_FLAG_BIT_IR_ENABLED = 0x08;

const WIIREMOTE_RUMBLE_MASK = 0x01;
const WIIREMOTE_LED_MASK = 0xf0;
const WIIREMOTE_LED_BIT0 = 0x80;
const WIIREMOTE_LED_BIT1 = 0x40;
const WIIREMOTE_LED_BIT2 = 0x20;
const WIIREMOTE_LED_BIT3 = 0x10;

const WIIREMOTE_ADDRESS_BALANCE_CALIBRATION = 0xa40020;

class WiiClient {
  constructor(mqtt_client, topic_cmd) {
    this.mqtt_client = mqtt_client;
    this.topic_cmd = topic_cmd;
  }

  connect(address, retry = 2){
    var data = {
      cmd: WIIREMOTE_CMD_CONNECT,
      address: this.addr2bin(address),
      retry: retry
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  disconnect(){
    var data = {
      cmd: WIIREMOTE_CMD_DISCONNECT,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  writaValue(value){
    var data = {
      cmd: WIIREMOTE_CMD_WRITE,
      value: value
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  enableSound(enable){
    var data = {
      cmd: WIIREMOTE_CMD_ENABLE_SOUND,
      enable: enable,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  enableExtension(enable){
    var data = {
      cmd: WIIREMOTE_CMD_ENABLE_EXTENSION,
      enable: enable,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  requestRemoteAddress(){
    var data = {
      cmd: WIIREMOTE_CMD_REQ_REMOTE_ADDRESS,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  readRegister(offset, len){
    var data = {
      cmd: WIIREMOTE_CMD_READ_REG,
      offset: offset,
      len: len,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  writeRegister(offset, data){
    var data = {
      cmd: WIIREMOTE_CMD_WRITE_REG,
      offset: offset,
      data: data,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  requestStatus(){
    var data = {
      cmd: WIIREMOTE_CMD_REQ_STATUS,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  readRegisterLong(offset, len){
    var data = {
      cmd: WIIREMOTE_CMD_READ_REG_LONG,
      offset: offset,
      len: len,
    };
    var message = new Paho.MQTT.Message(JSON.stringify(data));
    message.destinationName = this.topic_cmd;
    this.mqtt_client.send(message);
  }

  calcurateBalanceBoard(data, base){
    var result = {
      topright: this.calc_balance(data.topright, base.topright ),
      bottomright: this.calc_balance(data.bottomright, base.bottomright ),
      topleft: this.calc_balance(data.topleft, base.topleft ),
      bottomleft: this.calc_balance(data.bottomleft, base.bottomleft ),
    };
    result.total_weight = (result.topright + result.bottomright + result.topleft + result.bottomleft);

    return result;
  }

  calc_balance(value, base){
    if( value <= base[0] ){
      return 0.0;
    }else if( value > base[0] && value <= base[1] ){
      return ((value - base[0]) / (base[1] - base[0])) * 17.0;
    }else if( value > base[0] && value <= base[1] ){
      return (((value - base[1]) / (base[2] - base[1])) * (34.0 - 17.0)) + 17.0;
    }else{
      return 34.0;
    }
  }

  parseBalanceBoardCalibration(data){
    var result = {
      topright: [(data[0x04] << 8) | data[0x05], (data[0x0c] << 8) | data[0x0d], (data[0x14] << 8) | data[0x15]],
      bottomright: [(data[0x06] << 8) | data[0x07], (data[0x0e] << 8) | data[0x0f], (data[0x16] << 8) | data[0x17]],
      topleft: [(data[0x08] << 8) | data[0x09], (data[0x10] << 8) | data[0x11], (data[0x18] << 8) | data[0x19]],
      bottomleft: [(data[0x0a] << 8) | data[0x0b], (data[0x12] << 8) | data[0x13], (data[0x1a] << 8) | data[0x1b]],
    };

    return result;
  }

  parseExtension(type, data) {
    if (type == WIIREMOTE_EXT_TYPE_NUNCHUCK) {
      var value = {
        stk_x: data[0],
        stk_y: data[1],
        acc_x: (data[2] << 2) | ((data[5] >> 2) & 0x03),
        acc_y: (data[3] << 2) | ((data[5] >> 4) & 0x03),
        acc_z: (data[4] << 2) | ((data[5] >> 6) & 0x03),
        btns: (~data[5] & 0x03),
      }
      return value;
    } else
    if (type == WIIREMOTE_EXT_TYPE_BALANCEBOARD) {
      var value = {
        topright: (data[0] << 8) | data[1],
        bottomright: (data[2] << 8) | data[3],
        topleft: (data[4] << 8) | data[5],
        bottomleft: (data[6] << 8) | data[7],
      };
      if (data.length >= 11) {
        value.temperature = data[8];
        value.battery = data[10];
      }
      return value;
    }
  }

  parseReporting(data) {
    if (data[0] == WIIREMOTE_REPORTID_STATUS) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        leds: data[3] & 0xf0,
        flags: data[3] & 0x0f,
        battery: data[6]
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_READ_DATA) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        size: (data[3] >> 4) & 0x0f + 1,
        error: data[3] & 0x0f,
        address: (data[4] << 8) | data[5],
        data: data.slice(6)
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_ACK) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        report: data[3],
        error: data[4]
      };
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS) {
      var report = {
        report_id: data[0],
        btns: ((data[1] << 8) | data[2]),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT8) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        extension: data.slice(3)
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR12) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        ir: data.slice(6),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_EXT19) {
      var report = {
        report_id: data[0],
        btns: ((data[1] << 8) | data[2]),
        extension: data.slice(3),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_EXT16) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        extension: data.slice(6),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_IR10_EXT9) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        ir: data.slice(3, 3 + 10),
        extension: data.slice(3 + 10),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_BTNS_ACC_IR10_EXT6) {
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        acc_x: (data[3] << 2) | ((data[1] >> 5) & 0x03),
        acc_y: (data[4] << 1) | ((data[2] >> 5) & 0x01),
        acc_z: (data[5] << 1) | ((data[2] >> 6) & 0x01),
        ir: data.slice(6, 6 + 10),
        extension: data.slice(6 + 10),
      };
      return report;
    } else
    if (data[0] == WIIREMOTE_REPORTID_EXT21) {
      var report = {
        report_id: data[0],
        extension: data
      };
      return report;
    } else
    if (data[0] == 0x3e || data[0] == 0x0f) {
      throw "not supported";
    } else {
      throw "unknown";
    }
  }

  addr2bin(address){
    return bytes_swap(hexs2bytes(address, ':'));
  }

  addr2str(address){
    return bytes2hexs(bytes_swap(address), ':');
  }  
}

function hexs2bytes(hexs, sep) {
  hexs = hexs.trim(hexs);
  if( sep == '' ){
      hexs = hexs.replace(/ /g, "");
      var array = [];
      for( var i = 0 ; i < hexs.length / 2 ; i++)
          array[i] = parseInt(hexs.substr(i * 2, 2), 16);
      return array;
  }else{
      return hexs.split(sep).map(function(h) { return parseInt(h, 16) });
  }
}

function bytes2hexs(bytes, sep) {
  var hexs = '';
  for( var i = 0 ; i < bytes.length ; i++ ){
      if( i != 0 )
          hexs += sep;
      var s = bytes[i].toString(16);
      hexs += ((bytes[i]) < 0x10) ? '0'+s : s;
  }
  return hexs;
}

function bytes_swap(bytes){
  for( var i = 0 ; i < bytes.length / 2 ; i++ ){
    var t = bytes[i];
    bytes[i] = bytes[bytes.length - 1 - i];
    bytes[bytes.length - 1 - i] = t;
  }

  return bytes;
}

使い方

まずは、BluetoothでWiiリモコンたちと接続するサーバを立ち上げます。
前回の投稿で示しました。

$ node index.js server testwii_cmd testwii_evt
MQTT_CLIENT_ID: server
MQTT_TOPIC_CMD: testwii_cmd
MQTT_TOPIC_EVT: testwii_evt
mqtt.connected.
mqtt.subscribed.

これで、testwii_cmdとtestwii_evtというトピック名でMQTTブローカに接続できました。

それでは、まずブラウザから表示させます。

image.png

この状態では、まだWiiにもMQTTにも接続されていない状態です。

mqtt_brokerのところに、MQTTブローカのURLを指定してください。Websocket接続のポート番号を指定する必要があります。
topic_cmdとtopic_evtには、サーバが接続したトピック名を指定します。さきのどは、testwii_cmdとtestwii_evtを指定しました。

それでは、MQTT Connectボタンを押下します。

image.png

これで接続はできました。
ですが、まだWiiリモコンは接続していません。
Bluetooth Addressに接続したいWiiリモコンのBluetooth Addressを入力します。
そして、Wiiリモコンの①と②のボタンを両方押して、Discoveryモードにして、Wii Connectボタンを押下します。

そうすると、サーバ側の方で、以下のように表示されて、Wiiリモコンとの接続が完了します。ブラウザからはちょっとわかりにくいので、改善の余地ありですね。。。

on.message topic: testwii_cmd message: {"cmd":1,"address":[XX,XX,XX,XX,XX,XX],"retry":2}
Uint8Array [ XX, XX, XX, XX, XX, XX ]
connect called
s_11 connect result=0
s_13 connect result=0
startContinuousRead called

この状態で、Wiiリモコンの各ボタンを押してみてください。ブラウザに押したボタンの色が濃くなるのがわかると思います。また、rumbleのチェックボックスをOnにするとWiiリモコンが振動し、LEDの1~4のチェックボックスをOnにするとLEDが点灯するのがわかるかと思います。

この状態は、レポーティングモードがBTNSですので、ボタンの状態しかPublishされてきません。そこで、report_idのところをBTNS_ACCに変更してsetボタンを押してみてください。すると、上側のグラフ(acc_xやacc_y、acc_z)にグラフが表示されます。これが、Wiiリモコンの加速度の状態です。Wiiリモコンを動かしてみるとわかります。

次に、ヌンチャクを動かしてみましょう。
まず、ヌンチャクをWiiリモコンに接続します。そうすると、extension_connectedがtrueに切り替わります。
ヌンチャクの情報の取得には、EXTが付いたreport_idを選択します。BTNS_ACC_EXT16を選択してsetボタンを押下します。グラフが表示されだしましたが、まだ値に動きがありません。それは拡張コントローラを有効にしていないためです。そこで、Enable Extensionボタンを押下すると、動き出します。
ヌンチャクを動かしたり、ヌンチャクのスティックを動かしたり、C/Zボタンを押したりしてみてください。

image.png

次に、バランスボードを接続してみます。
さきほどのWiiリモコンを接続するサーバは、すでにWiiリモコンを接続済みですので、もう一つサーバを立ち上げます。MQTTの名前が被らないようにします。testwii_cmd2、testwii_evt2にしてみました。

$ node index.js server2 testwii_cmd2 testwii_evt2
MQTT_CLIENT_ID: server2
MQTT_TOPIC_CMD: testwii_cmd2
MQTT_TOPIC_EVT: testwii_evt2
mqtt.connected.
mqtt.subscribed.

では、ブラウザ側も別のブラウザまたは別タブを立ち上げて表示させます。
今度は、topic_cmdとtopic_evtに先ほど指定したtestwii_cmd2とtestwii_evt2を指定し、client_idに他とかぶらない値を指定して、MQTT Connectボタンを押下します。
そして、Bluetooth Addressには、バランスボードのBluetooth Addressを指定し、wii_typeにはバランスボードを選択しておきます。

それでは、バランスボードの裏面の電池蓋を開けて、Syncボタンを押してから、Wii Connectボタンを押下しましょう。
接続が完了すると、バランスボードのPowerボタンを押すとボタンの色が変わりますし、LED 4のチェックボックスをOnにすると、PowerボタンのLEDが点灯したりします。

次に、report_idをBTNS_EXT19を選択してsetボタンを押下します。
ですが、まだ重量のグラフは表示されません。それはキャリブレーションデータを受信していないためです。Update Calibrationボタンを押下してみてください。
さあ、バランスボードに乗ってみてください。

image.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vuex】Stateを丸ごと置き換える

結論

Object.assignを使って置換できます。

mutation.js
export default {
  replaceState (state, { value }) {
    Object.assign(state, value)
  }
}

Vuexでstateを置き換えたいときに、
以下のように代入してもうまく動作しません。

export default {
  replaceState (state, { value }) {
    staet = value
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Vue CLIで共通テンプレートを作成する方法

【Vue.js】Vue CLIで共通テンプレートを作成する方法

Vue CLIで作成したページに共通ヘッダーや共通フッターを作成する方法。

作成手順

  1. componentsフォルダ配下に単一コンポーネントファイルを作成
  2. app.vueファイルで作成したコンポーネントを読み込み
  3. export defualtでモジュール化
  4. templateタグ内で呼び出し

以下、共通フッター(CommonFooter)コンポーネントを作成する場合で考える。


1. componentsフォルダ配下に単一コンポーネントファイルを作成

作成するコンポーネントは、componentsフォルダ内に作成し、各ページで呼び出す。

<コンポーネント作成時の注意点>

  • ファイル名は、大文字始まり、キャメルケースとする。
    • 例: CommonFooter.vue
  • ファイル内は、template, script, styleタグで構成する。
    • 単一ファイルコンポーネントと呼ぶ



<補足:単一ファイルコンポーネントについて>
文字通り、一つのファイルからなるパーツのことをいう。

従来のWEBページはhtml, css, jsファイルから成るが、Vue CLIの思想では、各パーツ(コンポーネント)毎にhtml, css, jsを管理する。

html部はtemplateタグで囲み、cssはstyleタグ、jsはscriptタグで囲む。


2. app.vueファイルで作成したコンポーネントを読み込み

全てのページで共通となる要素は「app.vue」ファイルに記載する。

まず初めに、作成したコンポーネント(CommonFooter)をapp.vueにインポートする。

scriptタグ内に以下を記述。

scriptタグ内(app.vue)
import CommonFooter from '@/components/CommonFooter'


export defualtでモジュール化

コンポーネントファイルを呼び出しただけでは使えない。templateタグ内で呼び出せるように、モジュール化する必要がある。

scriptタグ内のexports defaultで、componentsオプションを使用しインポートしたテンプレートを定義する。

scriptタグ内(app.vue)
import CommonFooter from '@/components/CommonFooter'

export default {
  components:{
    CommonFooter
  }
}

<export defaultsについて>
単一ファイルコンポーネントを使うためには、export defaultで囲む必要がある。

これをすることで、templateタグなど外部でそのコンポーネントが利用できるようになる。

呼び出し時にの名称としてテンプレート名を設定する。


4. templateタグ内で呼び出し

これまでで、作成したコンポーネントをtemplate内で呼び出し、画面に反映する準備が整った。

コンポーネントは呼び出したい箇所に<コンポーネント名/>を記述することで呼び出せる。

<コンポーネント名/>は<コンポーネント名></コンポーネント名>の省略形。どちらでも正常に動く。

templateタグ内(app.vue)
<template> 

  <CommonFooter/>

</template>

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gridsome入門 SPAを作ってみよう 【4日目 棒グラフ編】

スケジュール

前提条件

  • node.js v8.3以上
  • yarn or npmが入っている(Document見るとyarnの方が推奨とのこと)
  • Gridsomeのプロジェクトを作成している

前回同様vue-chart.jsでグラフを作成

その前に前回作ったcomponents名を変更

前回src/components/Chart.vueというコンポーネントを作りましたが、今回グラフのコンポーネントをもう一つ作るので名前を変更していきましょう。
変更前src/components/Chart.vue
変更後src/components/chart/Doughnut.vue
適宜このコンポーネントを読み込んでいたpageなども変更が必要です。

新たに棒グラフ用のコンポーネントを作る

棒グラフ用はimport { Bar } from 'vue-chartjs'で使えるようになります。
棒グラフを使うときの注意点はoptionsのscalesを指定しない場合、datasetsで渡した最小値が棒グラフのメモリの最小値になってしまうので見ずらかったです。
実際の作成したコンポーネントはこちら

src/components/chart/Bar.vue
<script>
import { Bar } from 'vue-chartjs'
export default {
  extends: Bar,
  name: 'bar',
  props: ['labels','chartData'],
  data () {
    return {
      chartLabel: 'Asset transition',
      options: {
        tooltips: {
          callbacks: {
            label: function(tooltipItem, data) {
              const currentData = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
              return '$' + currentData;
            },
          }
        },
        scales: {
          yAxes: [
            {
              ticks: {
                min: 0, // グラフのメモリ最小値
                max: this.upperData() // グラフのメモリ最大値(渡したdatasetsの最大*1.2にしている)
              }
            }
          ]
        }
      }
    }
  },
  mounted () {
    this.renderChart(
        {
          labels: this.labels,
          datasets: [
            {
              backgroundColor: 'rgba(137,255,255,1)',
              data: this.chartData,
              label: this.chartLabel,
            }
          ]
        },
        this.options)
  },
  methods: {
    upperData: function () {
      const maxData = Math.max.apply(null, this.chartData) * 1.2;
      return parseInt(maxData, 10);
    }
  }
}
</script>

また今回はpropsを使って親から子componentsに値を渡しています。props: ['labels','chartData'],ここで指定。

src/pages/asset-transition/index.vue
<template>
  <Layout>
    <h1>Asset transition</h1>
    // labels,chartDataというkeyで渡す
    <bar :labels="labels" :chartData="chartData"></bar>
  </Layout>
</template>

<script>
import Bar from "../../components/chart/Bar";
export default {
  components: {
    Bar
  },
  data() {
    return {
      labels: ['202001', '202002', '202003', '202004', '202005','202006','202007', '202008'],
      chartData: [10000, 11000, 11500, 11300, 12100, 12500, 20000, 19000],
    }
  },
  metaInfo: {
    title: 'Hello, world!'
  }
}
</script>

<style>
.home-links a {
  margin-right: 1rem;
}
</style>

出来上がった棒グラフはこちら

demo.gif

あとがき

昨日は折れ線グラフにしようかなあと思っていましたが、やっぱ棒グラフに変更。今回使うグラフこれで以上かな。たぶん。
明日はCSVからデータを読み込む処理に入る予定。
今日は暑いからアイスが美味しい。セブンのチョコミント氷最高。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでBootStrapをつまみ食い的に使う

概要

Vue.jsのVue CLIからBootStrapを使う方法。

「BootStrapを使おう!さぁ先ずは基本を学ぼう」と構えて臨むのではなく、「このデータ構造の表現にちょうどよいUI無いかな? お、BootStrapのxxのコンポーネントが良さ気じゃん」と気軽につまみ食いで使う、ことを目指すとする。

サンプルに用いるデータ構造

次のような配列データを、Vue.jsを用いたUIで表現する場合を考える。
表示対象は「datetime, type, notes」の3つとする。

activitylist = [
    {
        id: 5,
        datetime: "1596229200",
        type: 1,
        notes: '翌日の6時に起きたとする'
    },{
        id: 4,
        datetime: "1596223800",
        type: 1,
        notes: '翌日の4時半に目が覚めたとする'
    },{
        id: 3,
        datetime: "1596201000",
        type: 0,
        notes: '2020-07-31 22:10、つまり夜22時過ぎに寝た場合を仮定'
    },{
        id: 2,
        datetime: "1596164400",
        type: 2,
        notes: '薬を昼12時に飲んだとする。'
    }
];

typeに指定された値に対して、その値を配列番号と見なして、それぞれ配列要素のtitleキーに設定した文字列に置き換えて表示する、ものとする。

typelist = [
    { 
        title: '寝た'
    }, {
        title: '起きた'
    }, {
        title: '服薬'
    }
];

これらの配列データをViewListCard.vueで定義し、
コンポーネントItemCard.vueに配列をPropsで渡して、
表示の仕方はコンポーネントItemCard.vueに任せる、
という設計を仮定する。

具体的なサンプルコードは次のようになる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap

  • ./src/components/ViewListCard.vue
  • ./src/components/ItemCard.vue
    • ItemCard0.vueItemCard3.vue がそれぞれの段階ごとのサンプルコード

テキストをマスタッシュ構文でそのまま表示する

上述の配列データactivitylistの各要素の3項目を、次の変換のみを行って
マスタッシュ(Mustache)を用いてテキストで表示すると
次のようになる。

  • datetime: UNIX時間(秒)を「YYYY-MM-DD . HH:MM:00」の文字列に変換する
  • typeを、typelistの配列番号に応じた要素のtitleキーに設定された文字列に変換する
  • notesはそのまま表示する。

▼card0.png
https://gyazo.com/820baa2b9aa19d6d5a62f000292b80e2

本サンプルでは、それぞれのカード(様の部分)をタップすると編集モードになるる、という設計とする。その編集モードは、先ずはHTML標準のinputタグを用いて実装すれば、次のような表示となる。

▼card0-edit.png
https://gyazo.com/51143f81d3283087fc2c59a673d16f0b

ここまでのサンプルコードは次のようになる。
ItemCard0.vue を、実際にはItemCard.vueとして動作させる。

https://github.com/hoshimado/qiita-notes/tree/master/qiita-card-bootstrap/src/components/ItemCard0.vue

以下、上述までの表示形式を、BootStrapを用いていい感じにする方法を述べる。

BootStrap(BootStrapVue)を使う準備をする

Vue.js上でBootStrapを利用するには、BootStrapVueを用いるのが簡単。

BootstrapVue provides one of the most comprehensive
implementations of Bootstrap v4 for Vue.js.

Vue CLIのプロジェクトのルートフォルダにて、以下のコマンドでインストールする。

npm install bootstrap-vue    --save

続いて、ルートにあるmain.jsを開いて、次の2行(本サンプルではコメント含めて4行)を追加する。

main.js
import Vue from 'vue'
import App from './App.vue'

// +++ add for bootstrap +++
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// -------------------------

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

以上で、BootStrapをVue CLI上で使う準備は完了。

ref. https://bootstrap-vue.org/docs

項目のタイトル相当を見やすくしたい

先ほどの「card0.png」の画面を見やすくすることを考える。

「起きた」や「寝た」などの表示を上手く装飾するものがないか?
と公式BootStrapVueのComponentのページを見ていく。

https://bootstrap-vue.org/docs/components

このページは、簡単な説明を一覧出来て、それぞれのComponentの説明ページに飛ぶとサンプル表示もあるので、「そのComponentによる装飾がどういうものか?」を掴みやすくて、助かる。

どんなコンポーネントがあるか?を上から順に見ていく中で、
今回のケースなら「バッジ(Badge)」で「起きた」「寝た」などを表示するのがよさそうだ、などのように装飾の仕方を決める。

本サンプルでは、上述のBadgeによる装飾とnotesの値をReadOnlyのForm Textareaで表示するものとする。
この場合は、次のような表示になる。

▼card1.png
https://gyazo.com/12d6d9bb3df6660d55e1b9d349875143

上記を実装するには、コンポーネントItemCard.vueに対して、次のような変更を加える。

  • {{typeStr}}としていた部分を、<b-badge v-bind:variant="typeVariant">{{typeStr}}</b-badge>とする
  • {{notesCurrent}}としていた部分を、<b-form-textarea v-bind:value="notesCurrent" readonly rows="2" max-rows="2"></b-form-textarea>とする
  • 利用するコンポーネントを「import { BBadge, BFormTextarea } from 'bootstrap-vue'」で読み込んで、「components: {}」に指定する

たったこれだけのコード修正で、で上図(card1.png)の様な見やすい表示に変更できる。BootStrapはとても簡単で使いやすい。

なお、編集モード(card0-edit.png)の表示については、notesCurrentの部分は、Form Textareaを用いてreadonly属性を外せばよいだろう。編集モード側のUI変更を含めたサンプルコードは以下。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard1.vue

編集モード側の表示は以下のようになる。

▼card1-edit.png
https://gyazo.com/fe5ba1dc8bbf9e7b4e3e834ad62d0b43

なお、<b-badge>コンポーネントはvariant属性でカラーリングを変更できる。サンプルコードでは、typelist配列の各要素に、variantキーを追加し、それに従ったBadgeカラーを表示する実装にしてある。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ViewListCard.vue

[
    { 
        title: '寝た',
        variant: 'primary'
    }, {
        title: '起きた',
        variant: 'secondary'
    }, {
        title: '服薬',
        variant: 'success'
    }
]

variant属性への指定において、デフォルトで利用可能な値は以下を参照。

https://bootstrap-vue.org/docs/components/badge#contextual-variations

編集モードで選択項目をラジオボタンで、ついでに確定ボタンもいい感じに装飾する

続いて、上図の編集モード(card0-edit.png)における「起きた」「ネタ」をラジオボタンで選べるようにする。HTML標準の<input type="radio">でも良いのだが、BootStrapにForm Radioコンポーネントがあるので、これを使う。

次の公式ガイドに従って、<b-form-group><b-form-radio-group>を用いる。

https://bootstrap-vue.org/docs/components/form-radio#grouped-radios

編集対象は(propsで渡されたtypeをもとに生成した)typeCurrentなので、これをv-model属性で<b-form-radio-group>にバインドする。

<b-form-group label="記録の種別を選んでください">
    <b-form-radio-group
        v-model="typeCurrent"
        :options="typeOptions"
    ></b-form-radio-group>
</b-form-group>

選択肢は、v-bind:options属性(略記して:options属性)で設定する。設定すべき変数のフォーマットは配列で、各要素は次の2つのキーを持つ。

  • 選択肢の文字列としてtext
  • 選択されたときに編集対象(=v-modelでバインドされた変数)へ代入する値としてvalue

したがって、本サンプルでは(propsで渡された)typelistを元にして次のようにtypeOptins配列を生成しておく。

this.typelist.forEach((elem, index)=> {
    this.typeOptions.push({
        text: elem.title,
        value: String(index)
    })
})

ついでなので、「確定」ボタンもBootStrapVueが提供するButtonコンポーネントで装飾する。これは、「<button @click="clickBtnEditFinish">確定</button>」としていたところを、「<b-button @click="clickBtnEditFinish">確定</b-button>」と置き換えるだけで良い。

以上の変更を加えたコンポーネントItemCard.vueのコード全体は以下となる(※importcomponentsへの追加も忘れずに→リンク先のコードを参照)。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard2.vue

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card2-edit.png
https://gyazo.com/4db021ef51d4331edddc5ab860f6d7de

日時の編集ボックスを、いい感じに装飾する

最後に、上図(card2-edit.png)の日付と時刻の入力を良い感じに装飾する。

(※HTML5利用可能環境であれば、素のHTML inputタグが実装している入力支援のpickerを利用可能なので、BootStrap版に置き換えるか否かは好みの問題かもしれない。一応、IEとPC版SafariはHTML5に未対応のため同じ表示にならないが、BootStrap版なら同じ表示が可能、という差はある)

これまでと同様にBootStrapVueのコンポーネント一覧から、日付と時刻のPickerを探す。

https://bootstrap-vue.org/docs/components

Form DatepickerForm Timepickerがあるので、これを使う。

https://bootstrap-vue.org/docs/components/form-datepicker

https://bootstrap-vue.org/docs/components/form-timepicker

使い方は、それぞれを次のように置き換えるだけ。

置き換え前:

<input v-model="dateCurrent" type="date">
<input v-model="timeCurrent" type="time">

置き換え後:

<b-form-datepicker v-model="dateCurrent" class="mb-2"></b-form-datepicker>
<b-form-timepicker v-model="timeCurrent" locale="ja"></b-form-timepicker>

置き換え後のサンプルコードは次のようになる。

https://github.com/hoshimado/sleeplog/blob/master/qiita-card-bootstrap/src/components/ItemCard3.vue

※ここで「class=mb-2」を指定しているが、これはBootStrap v4.5で定義されているclassのこと。BootStrapVueでは、BootStrapで準備されているClassをそのまま利用できる。

上記のコードへの変更によって、編集モードの表示は次のように変わる。

▼card3-1edit.png
https://gyazo.com/11097020fcca048dd5f9ae00f32fd7ef

▼card3-2date.png
https://gyazo.com/e9461f4372ce6acff2410bee18da2235

▼card3-3time.png
https://gyazo.com/f5f32c9de592b855dd2cb498a13ce8aa

なお、「Picker経由だけでなく、時刻を直にテキストとして入力もしたい」という場合は、inputタグを組わせることで実現できる。BootStrapVueでの、その実装例も公式サイトの以下に記載がある。とても親切♪

https://bootstrap-vue.org/docs/components/form-timepicker#button-only-mode

以上ー。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Django REST Framework + Vue.js】APIからデータを取得し表示する

はじめに

 ブログや投稿系のアプリを作成するとを考え、Django REST FrameworkとVue.jsを使って、投稿一覧データを取得し画面に表示させるまでの流れを扱いたいと思います。
 大まかな流れは以下の通りです。
 1. Django REST Frameworkで投稿一覧APIを作成する。
 2. フロントのVue.jsからaxiosによってAPIからデータを取得し画面に表示する。

1. Django REST Frameworkの記述

 まず、Django REST Frameworkをインストールしsettings.pyのINSTALLED_APPSにrest_frameworkの追加をしてください。
 Djangoの設定ディレクトリ名を「config」, アプリケーションディレクトリ名を「apiv1」としています。

models.pyの設定

今回はCategoryモデルとPostモデルと作成しました。

apiv1/models.py
from django.db import models
from django.contrib.auth import get_user_model

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name


class Post(models.Model):
    category = models.ForeignKey(to=Category, on_delete=models.CASCADE)
    author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    content = models.TextField()
    published_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-published_at']

    def __str__(self):
        return self.title

serializers.pyの設定

apiv1/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Category, Post

class UserSerializer(serializers.ModelSerializer):
    """ユーザーシリアライザー"""
    class Meta:
        model = get_user_model()
        fields = '__all__'


class CategorySerializer(serializers.ModelSerializer):
    """カテゴリシリアライザ"""
    class Meta:
        model = Category
        fields = ('id', 'name')


class PostSerializer(serializers.ModelSerializer):
    """投稿シリアライザ"""
    class Meta:
        model = Post
        fields = '__all__'

views.pyの設定

 Viewの指定方法はいくつか種類がありますが、今回は自分が一番わかりやすいと感じたgeneric viewを使いたいと思います。
 generic viewは以下のコードにあるようなListAPIView(一覧取得)の他に、CreateAPIView(作成)やRetrieveAPIView(詳細取得)、UpdateAPIView(更新)などがあります。他にもたくさん種類があり、作成, 取得, 削除, 更新によって使い分けることができます。ListCreateAPIViewという一覧取得と作成を行ってくれるものもあります。

apiv1/views.py
from rest_framework import generics
from django.contrib.auth import get_user_model
from .models import Post, Category
from .serializers import CategorySerializer, PostSerializer

class CategoryListAPIView(generics.ListAPIView):
    """カテゴリモデルの取得(一覧)APIクラス"""
    queryset = Category.objects.all()
    serializer_class = CategorySerializer


class PostListAPIView(generics.ListAPIView):
    """投稿モデルの取得(一覧)APIクラス"""
    queryset = Post.objects.all()
    serializer_class = PostSerializer

urls.pyの設定

config/urls.py(プロジェクトのurls.py)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('apiv1.urls')),
]
apiv1/urls.py
from django.urls import path
from . import views

app_name = 'apiv1'
urlpatterns = [
    path('categories/', views.CategoryListAPIView.as_view()),
    path('posts/', views.PostListAPIView.as_view()),
]

 この時点でスーパーユーザーを作成した後admin管理画面からカテゴリおよび投稿をいくつか作成し、 http://127.0.0.1:8000/api/v1/categories/, http://127.0.0.1:8000/api/v1/posts/ にアクセスするとそれぞれ以下のような表示になると思います。

http://127.0.0.1:8000/api/v1/categories/
スクリーンショット 2020-08-11 1.53.57.png
http://127.0.0.1:8000/api/v1/posts/
スクリーンショット 2020-08-11 1.43.44.png

これらのデータをVue.js側で呼び出し、表示させたいと思います。

2. Vue.jsの記述

 まず最初にaxios, vue-axiosのインストールをしてmain.jsに設定を記述します。

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import axios from 'axios' //追記
import VueAxios from 'vue-axios' //追記

Vue.config.productionTip = false

Vue.use(VueAxios, axios) //追記

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

次にVueRouterを使ってパスを指定します。
今回は http://127.0.0.1:8080/postlist と指定されたら、PostList.vueが表示されるように設定しました。

src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import PostList from '@/components/PostList'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/postlist',
      component: PostList
    },
  ]
})

PostList.vueファイルに記述していきます。

src/components/PostList.vue
<template>
  <div>
    <div v-for="(post, key) in Posts" :key="key">
      <hr>
      <p>カテゴリ: {{ post.category }}</p>
      <p>タイトル: {{ post.title }}</p>
      <p>内容: {{ post.content }}</p>
      <p>投稿日: {{ post.published_at}}</p>
      <hr>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      Posts: []
    };
  },
  mounted() {
    this.axios
      .get("http://127.0.0.1:8000/api/v1/posts/")
      .then(response => (this.Posts = response.data));
  }
};
</script>

http://127.0.0.1:8080/postlist にアクセスすると次の様に表示できました。
スクリーンショット 2020-08-11 2.16.54.png
 今の段階ではカテゴリが数字(id)で表現されています。今回は文字を表示して欲しいので、Djangoのserializers.pyに追記していきます。
 PostSerializerにcategory = CategorySerializer()を追記してカテゴリ名が取得できるようにします。

apiv1/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Category, Post

class UserSerializer(serializers.ModelSerializer):
    """ユーザーシリアライザー"""
    class Meta:
        model = get_user_model()
        fields = '__all__'


class CategorySerializer(serializers.ModelSerializer):
    """カテゴリシリアライザ"""
    class Meta:
        model = Category
        fields = ('id', 'name')


class PostSerializer(serializers.ModelSerializer):
    """投稿シリアライザ"""
    category = CategorySerializer() #追記

    class Meta:
        model = Post
        fields = '__all__'

そのあともう一度 http://127.0.0.1:8000/api/v1/posts/ にアクセスすると、以下のようになると思います。
 CategoryとPostが1対多の関係でありForeignKeyで結び付けられているためcategoryの正体を見ることができました。
スクリーンショット 2020-08-11 2.21.47.png
 
 最後にPostList.vueの<p>カテゴリ: {{ post.category }}</p>の部分を<p>カテゴリ: {{ post.category.name }}</p>と変更し http://127.0.0.1:8080/postlist にアクセスすると、以下の様にカテゴリ名が正しく表示されるようになりました。
スクリーンショット 2020-08-11 2.27.45.png

 以上がDjangoRestFrameworkとVueを用いてAPIからデータを取得、表示する大まかな流れでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む