[Vue.js] vuelidateによるvalidation

公式
https://vuelidate.js.org/

これが一番しっくりくるなぁ。。正直Reactより使いやすい。

<!DOCTYPE html>
<html lang="ja">
 <head>
 <meta charset="utf-8">
 <title>title</title>
 <!-- <link rel="stylesheet" href="sample.css"> -->
 <!--[if lt IE 9]>
 <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
 <script src="http://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
 <![endif]-->
 <!-- <script src="sample.js"></script> -->
 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" >
 <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
 <!-- <script src="https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.global.js"></script> -->
 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/vuelidate@0.7.4/dist/validators.min.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/vuelidate@0.7.4/dist/vuelidate.min.js"></script>
 </head>
 
 <body>
<div id="app" class="container mt-5">
    <h2>{{title}}</h2>
    <!-- <pre>{{ $v }}</pre> -->
    <div class="row">
        <div class="col-sm-8">
            <form @submit.prevent="submitForm" action="/hoge">
                <div class="form-group">
                    <label for="name">名前:</label>
                    <input type="name" class="form-control" id="name" v-model="name" @blur="$v.name.$touch()">
                    
                    <div v-if="$v.name.$error">
                        <span v-if="!$v.name.required">名前が入力されていません。</span>
                        <span v-if="!$v.name.alphaNum">英数字で入力してください。</span>
                        <span v-if="!$v.name.minLength">5文字以上で入力してください。</span>
                        <span v-if="!$v.name.maxLength">10文字以下で入力してください。</span>
                        
                    </div>
                </div>
                <div class="form-group">
                    <label for="age">年齢:</label>
                    <input type="age" class="form-control" id="age" v-model="age">
                </div>
                <div class="form-group">
                    <label for="email">メールアドレス:</label>
                    <input type="email" class="form-control" id="email" v-model="email" @blur="$v.name.$touch()">
                    <div v-if="$v.email.$error">
                        <span v-if="!$v.email.required">メールアドレスが入力されていません。</span>
                        <span v-if="!$v.email.email">メールアドレスの形式が正しくありません。</span>
                    </div>
                </div>
                <button  type="submit" class="btn btn-info">送信</button>
                <!-- <button :disabled="$v.$invalid" type="submit" class="btn btn-info">送信</button> -->
            </form>
        </div>
    </div>
</div>
<script>
    Vue.use(window.vuelidate.default);
    const { required, email, maxLength, minLength, alphaNum } = window.validators;

    const app = new Vue({
        el: '#app',
        data: {
            title: '入力フォームバリデーション',
            name: '',
            age: '',
            email: '',
        },
        validations: {
            name: {
                required,
                alphaNum,
                minLength: minLength(4),
                maxLength: maxLength(10),
            },
            email: {
                required,
                email
            }
        },
        methods: {
            submitForm() {
                this.$v.$touch();
                if (this.$v.$invalid) {
                    console.log('バリデーションエラー');
                } else {
                    console.log('submit');
                }
                
            }
        }
    });
 </script>
 </body>
</html>

Vue.jsフォームバリデーションの基礎

 <div>
    <form 
    id="app"
    @submit="checkForm"
    action="https://vuejs.org/"
    method="post"
    >

    <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
            <li v-for="error in errors">{{ error }}</li>
        </ul>
    </p>

    <p>
        <label for="name">Name</label>
        <input 
            id="name"
            v-model="name"
            type="text"
            name="name">
    </p>
    <p>
        <label for="age">Age</label>
        <input 
            id="age"
            v-model="age"
            type="number"
            name="age"
            min="0">
    </p>
    <p>
        <label for="movie">Favorite Movie</label>
        <select 
            id="movie"
            v-model="movie"
            name="movie">
            <option>Star Wars</option>
            <option>Vanilla Sky</option>
            <option>Atomic Blonde</option>
        </select>
    </p>

    <p>
        <input 
            type="submit"
            value="Submit"
        >
    </p>
 </div>
 <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
 <!-- <script>
    console.log(Vue);
  </script> -->
<script>
 const app = new Vue({
        el: '#app',
        data: {
            errors: [],
            name: null,
            age: null,
            movie: null
        },
        methods: {
            checkForm: function (e) {
                if(this.name && this.age) {
                    return true;
                }

                this.errors = [];

                if(!this.name) {
                    this.errors.push('Name required.');
                }
                if(!this.age) {
                    this.errors.push('Age required.');
                }
                e.preventDefault();
        }
        }
    });
 </script>
 </body>
</html>

うん、これは分かります。

 <div>
    <form 
    id="app"
    @submit="checkForm"
    action="https://vuejs.org/"
    method="post"
    novalidate="true"
    >

    <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
            <li v-for="error in errors">{{ error }}</li>
        </ul>
    </p>

    <p>
        <label for="name">Name</label>
        <input 
            id="name"
            v-model="name"
            type="text"
            name="name">
    </p>
    <p>
        <label for="email">Email</label>
        <input 
            id="email"
            v-model="email"
            type="email"
            name="email"
            >
    </p>
    <p>
        <label for="movie">Favorite Movie</label>
        <select 
            id="movie"
            v-model="movie"
            name="movie">
            <option>Star Wars</option>
            <option>Vanilla Sky</option>
            <option>Atomic Blonde</option>
        </select>
    </p>

    <p>
        <input 
            type="submit"
            value="Submit"
        >
    </p>
 </div>
 <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
 <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
 <!-- <script>
    console.log(Vue);
  </script> -->
<script>
 const app = new Vue({
        el: '#app',
        data: {
            errors: [],
            name: null,
            email: null,
            movie: null
        },
        methods: {
            checkForm: function (e) {

                this.errors = [];

                if(!this.name) {
                    this.errors.push('Name required.');
                }
                if(!this.email) {
                    this.errors.push('Email required.');
                } else if (!this.validEmail(this.email)){
                    this.errors.push('Valid email required.');
                }

                if(!this.errors.length){
                    return true;
                }                
                e.preventDefault();
        },
        validEmail: function(email){
            var re = /^(([^<>()[\]\\.,;:\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 re.test(email);
        }
        }
    });
 </script>
 </body>
</html>

なるほど、わかってきた!

Vue.jsのバリデーション

 <div id="app">
    <div class="form-area__name">
        <p>名前</p>
        <input type="text" name="name" v-model="name">
        <p v-if="isInValidName" class="error">名前は4文字以上で入力してください。</p>
    </div>
    <div class="form-area__email">
        <p>メールアドレス</p>
        <input type="text" name="email" v-model="email">
        <p v-if="isInValidEmail" class="error">メールアドレスの形式で入力してください。</p>
    </div>
    <div class="form-area__tel">
        <p>電話番号</p>
        <input type="tel" name="tel" v-model="tel">
        <p v-if="isInValidTel" class="error">電話番号は8桁で入力してください。</p>
    </div>
 </div>
 
 <script>
    new Vue({
        el: '#app',
        data() {
            return {
                name: 'name',
                email: 'email@email.com',
                tel: '12345678'
            };
        },
        computed: {
            isInValidName() {
                return this.name.length < 4;
            },
            isInValidEmail(){
                const reg = new RegExp(/^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/);
                return !reg.test(this.email);
            },
            isInValidTel() {
                const tel = this.tel;
                const isErr = tel.length < 8 || isNaN(Number(tel));
                return isErr;
            }
        }
    });
 </script>

やりたいことはわかるが、挙動がなんかちゃうな…

【React】react-hook-formのバリデーション

import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const { register, 
  handleSubmit,
  formState: {errors},
  } = useForm();

  const onSubmit = (data) => console.log(data);

  return (
    <div className="App">
      <h1>login</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email', { required: true })} />
          {errors.email && <div>入力が必須の項目です</div>}
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            {...register('password')} type="password" 
          />
        </div>
        <div>
          <button type="submit">Login</button>
        </div>
      </form>
    </div>
  );
}

複数のエラーメッセージ

function App() {
  const { register, 
  handleSubmit,
  formState: {errors},
  } = useForm({
    criteriaMode: 'all',
  });

<label htmlFor="password">パスワード</label>
          <input
            id="password"
            {...register('password', {
              required: {
                value: true,
                message: '入力が必須の項目です。',
              },
              pattern: {
                value: /^[A-Za-z]+$/,
                message: 'アルファベットのみ入力してください。'
              },
              minLength: {
                value: 8,
                message: '8文字以上入力してください。'
              }
            })} type="password" 
          />
          {errors.password?.type === 'required' && (<div>{errors.password.types.required}</div>)}
          {errors.password?.type === 'pattern' && (<div>{errors.password.types.pattern}</div>)}
          {errors.password?.type === 'minLength' && (<div>8文字以上入力してください。</div>)}
        </div>

バリデーションのタイミングは制御できる
reValidateMode: ‘onSubmit’,
mode: ‘onChange’,

なるほど、何となくわかってきました。
さぁ、実装していきますか…

【React】react-hook-formを使ってみる

$ yarn add react-hook-form

react-hook-formを使わずに書いた場合 1

import './App.css';
import { useState } from 'react';

function App() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      email,
      password,
    });
  };

  const handleChangeEmail = (e) => {
    setEmail(e.target.value);
  };
  const handleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  return (
    <div className="App">
      <h1>login</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" value={email} onChange={handleChangeEmail} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            value={password}
            onChange={handleChangePassword}
            type="password"
          />
        </div>
        <div>
          <button type="submit">Login</button>
        </div>
      </form>
    </div>
  );
}

export default App;

react-hook-formを使わずに書いた場合 2

import './App.css';
import { useRef } from 'react';
import 

function App() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      email: emailRef.current.value,
      password: passwordRef.current.value,
    });
  };

  // const handleChangeEmail = (e) => {
  //   setEmail(e.target.value);
  // };
  // const handleChangePassword = (e) => {
  //   setPassword(e.target.value);
  // };
  return (
    <div className="App">
      <h1>login</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" name="email" ref={emailRef} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            ref={passwordRef} type="password" 
          />
        </div>
        <div>
          <button type="submit">Login</button>
        </div>
      </form>
    </div>
  );
}

export default App;

### react-hook-formを使った場合

import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => console.log(data);

  return (
    <div className="App">
      <h1>login</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label htmlFor="email">Email</label>
          <input id="email" {...register('email')} />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            {...register('password')} type="password" 
          />
        </div>
        <div>
          <button type="submit">Login</button>
        </div>
      </form>
    </div>
  );
}

export default App;

registerに入っている値

const { name, ref, onChange, onBlur } = register('email');
//略
<input
  id="email"
  name={name}
  onChange={onChange}
  onBlur={onBlur}
  ref={ref}
/>

Reactでカウンターを作る

import React, { Component } from 'react';

const App = () => {
  return <Counter />
}

class Counter extends Component
{
  constructor(props)
  {
    super(props);
    this.state = {
      value : 0
    }
  }

  onIncrement = () => {
    this.setState({ value : this.state.value + 1});
  }

  onDecrement = () => {
    this.setState({ value : this.state.value - 1});
  }

  render()
  {
    return (
      <div>
        <div>
          Count value: {this.state.value}
        </div>
          
        <div>
        <button type="button" onClick={this.onIncrement}>+</button>
        <button type="button" onClick={this.onDecrement}>-</button>
        </div>
      </div>
    );
  }
}
export default App;

なるほど、面白いです^^

Reactで作ったコンポーネントをWebページに埋め込む

まず、Rectで作った簡単なjsファイル

App.js

import React from 'react';
import ReactDOM from "react-dom";

const App = () => {
  
  return (
    <div>
      <h1>hello world { 1 + 2}</h1>
    </div>
  )
  
}

export default App;

ファイルをbuidします。

$ npm run build

すると、buid/static/js にファイルが生成されているので、これを埋め込みます。
ここでは main.50873ce4.jsとなっていました。

<body>
    <h1>hello world!</h1>
    <div id="root"></div>
    <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
    <script src="main.js" crossorigin></script>
</body>

なるほど、フッターにインラインで記述するのではなく、Node JSで開発して、それをminifyしてコンポーネントとして埋め込むのね。
何となくReactの使い方は理解しました。

Reactの基礎 [環境構築]

$ npm -v
10.9.2
$ node -v
v22.13.1
$ yarn –version
bash: yarn: command not found

$ sudo npm install –global yarn
$ yarn –version
1.22.22

$ yarn create react-app helloworld
$ cd helloworld
$ ls
node_modules package.json public README.md src yarn.lock

$ yarn start

src/App.js

import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

修正する

import React from 'react';

const App = () => {
  return (
    <div>
      <h1>hello world</h1>
    </div>
  )
}

export default App;

ちょっと思い出してきました。

【Rust】axumでファイルのアップロードを受け取る

HTML側

<form action="/upload" method="post" enctype="multipart/form-data">
    <div>
        <p>name</p>
        <input type="text" name="name"><br><br>
        <input type="file" name="testfile" onchange="previewFile(this);">
    </div>
    <div>
        <input type="submit" value="送信する">
    </div>
</form>
<p>プレビュー</p>
<img id="preview">

<script>
function previewFile(file) {
    var fileData = new FileReader();
    fileData.onload = (function() {
        document.getElementById('preview').setAttribute("style","width:100px;height:100px");
        document.getElementById('preview').src = fileData.result;
        
    });
    fileData.readAsDataURL(file.files[0]);
}
</script>

axum側
axum = { version=”0.8.1″, features = [“multipart”] }
tokio = { version = “1.25”, features = [“full”] }

use tower_http::services::{ServeDir, ServeFile};
use axum::{
    extract::Multipart,
    extract::DefaultBodyLimit,
    routing::post,
    routing::get,
    Router,
};
use tokio::fs::File;
use tokio::io::AsyncWriteExt;


#[tokio::main]
async fn main() {

    let serve_dir = ServeDir::new("static").not_found_service(ServeFile::new("static"));

    let app = Router::new()
        .route("/", get(handle_index))
        .route("/upload", post(handle_upload))
        .layer(DefaultBodyLimit::max(1024 * 1024 * 1024))
        .nest_service("/static", serve_dir.clone())
        .fallback_service(serve_dir);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn handle_upload(mut multipart: Multipart)-> axum::response::Html<String> {

    while let Some(field) = multipart.next_field().await.unwrap(){
        let param_name = field.name().unwrap().to_string();
        match param_name.as_str() {
            "name" => {
                let name = field.text().await.unwrap();
                println!("tags: {}", name);
            }
            "testfile" => {
                let file_name = match field.file_name() {
                    Some(name) => name.to_owned(),
                    None => panic!("file_name is None"),
                };
                match field.bytes().await {
                    Ok(data) => {
                        println!("Length of `{}` is {} bytes", param_name, data.len());
                        let mut file = File::create(format!("./tmp/{}", file_name)).await.unwrap();
                        file.write_all(&data).await.unwrap();
                    }
                    Err(e) => {
                        eprintln!("Error reading `{}`: {}", param_name, e);
                        // return;
                    }
                }

            }
            _ => {
                println!("unknown param_name: {}", param_name);
            }
        }
    }

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("test.html", &context);
    axum::response::Html(output.unwrap())
}

なるほど、なかなか面白い。

【javascript】アップロードファイルのプレビュー

<form action="/post" method="post" enctype="multipart/from-data">
    <div>
        <input type="file" name="test" onchange="previewFile(this);">
    </div>
    <div>
        <input type="submit" value="送信する">
    </div>
</form>
<p>プレビュー</p>
<img id="preview" width="100px" height="100px">

<script>
function previewFile(file) {
    var fileData = new FileReader();
    fileData.onload = (function() {
        document.getElementById('preview').src = fileData.result;
    });
    fileData.readAsDataURL(file.files[0]);
}
</script>

次はaxumでデータのやり取りだな