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でデータのやり取りだな

【Rust】S3のbucketへ画像アップロード

use dotenv::dotenv;
use std::env;
use tokio::io::AsyncReadExt;
use rusoto_s3::*;
use rusoto_core::*;

#[tokio::main]
async fn main() {
    dotenv();
    let aws_access_key = env::var("AWS_ACCESS_KEY_ID").unwrap();
    let aws_secret_key = env::var("AWS_SECRET_ACCESS_KEY").unwrap();

    std::env::set_var("AWS_ACCESS_KEY_ID", aws_access_key);
    std::env::set_var("AWS_SECRET_ACCESS_KEY", aws_secret_key);

    s3_upload().await;
}

async fn s3_upload() -> Result<(), Box<dyn std::error::Error>> {
    let s3_client = S3Client::new("ap-northeast-1".parse().unwrap());
    let mut file = tokio::fs::File::open("./data/test.png").await?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer).await?;

    let result = s3_client.put_object(PutObjectRequest {
        bucket: String::from("hpscript"),
        key: "test.png".to_string(),
        body: Some(StreamingBody::from(buffer)),
        ..Default::default()
    }).await?;
    // do thing with result
    Ok(())
}

データがうまくアップロードされているのが分かります。なるほどね〜

【Rust】base64で画像を文字列に変換

想像以上に文字列のデータ量が多すぎる…

use std::fs::File;
use std::io::Write;
use std::path::Path;

static FILE_NAME: &'static str = "test";

fn main() {

    let path = "./data/img1.png";
    let base64 = image_base64::to_base64(path);
    println!("{:?}", base64);
    
    let img = image_base64::from_base64(base64);

    let file_type = "png";
    let mut output = File::create(&Path::new(&format!("./data/{}.{}", FILE_NAME, file_type))).unwrap();
    output.write_all(img.as_slice()).unwrap();

}


+mQS0RoL4XWkGRlzADREjATCW58eL2T9dzP754iZx5OBzfBHPz51wNvO7sxlwI3lWnIa/An5fSMLuH42HOzfZiABLKqp+KD7hAPsQB9L4eP1gFPJgwPDTUympfvNM9TWq/Z7iX4pAK4463hDa3MXJ4veh2slWxtqowOOXgnUoyHp4H24dllHc5QPw85K0EzUDsE7MjIY5YWH4WMe2ynw7WP0+Het1k3Xn6ReoDw9mo6EY5sccKOU2GADLZaDiBOZF38EDiSclHU0Imzpp7KCNqs9jUGmtGD3drKFe0HEcAGtKAjBmxnFJHgFg2QqCQFHPz6RzMLwzEMP8kUjwkRhB4EWQ7RGUlUzycFSDHCUF/uqs0bdbrPcs4NlPF3JuH46pOBAFj7DHJ5IAP9+fzYLO9dcreXgl8Kj67drmF5dzfzqfA6D9tiQdNfw7BXHgSJ/7NOjkRxsP53jvT/fcnbr6s6RVmMmzI3Y5ZtNujliCEQVbI5duj16GL36atHJ3yuq96R4Hs7xAFyrdGVi+Jxy/7xdFifhxIHwBpH+5tAmleNhSgpv2n2tbMFwIkiQMfgBUA6fx/gCSy/eG38mPwdf/fSEbb5HPYgdDnX61VJjxK3OHIhfkoKQq4WE2SD1gpdPl3WGoQ4D4/bAwvuIAoFdGa87SA/CycQ0D4GBn43A3U2UlBf4NaK7emUxiqOu2HaFlpdlVUH9eUpocIyphAiT5DQbAchnY88L7xj5pGbcO8jgyGNNNDO3qtoMewrODJp9gXQiBG2UDAXjeSALA1ValS7tlF4CpSAlSHMDwq2tbgHPhnu5GEhjDZXuKBFfQwa3hIPCjiFAStkVbKSsB2ou2bBDd/1j0bA/QDpCDWisQF/ofJJolHwfsTlud4jfXf9m0ZTNt3ScNmzDC0MpQC8Kkzp3aKyO5eaMHkvMO7dr069XFRK//mMH6zhMsF7par1s0JdprxvbY5WhtAnQfFCYA+4HKeJ0AaeTW+Cf2BH/e/Ai7hGv7I3JDFy12tzbRU4fdtzQ/FL/IUOMBH86d8K+45XeOxIDejOfBhXEzP6ZCHrJeIQCfAQD71gDANg0AYJwW5mrq52goEOKPJEbQ9QCwZrcOrfkOlMrt20SJTmBAS+ALGV2mWDAAbp6BKnSE6LjPVzsFVegB3dvXXcvCt4DQMJ4VrCM+dK54QwB4/kjtGgD481AZB2AKw1xR+ngygOHl5dyznwaGeriNMNMBXDUc4wC6ipgDsfEDR6SewDDRg4+fnUrBCNuEDXPWvmfvaGsO6OrbQ40IohpxVKI+H/gV6Rah2i6hcc+PrL575w56mr3GDTNYOt02eu1MZNXwzEJXGLYYotvbRc/3ir7eI3r0GX477CGKtqzfGb8yaeMcoHjIKpew1W7xPrNBZt6d5nHqI1/sbzC/D0D+6urmH89nodSM2jvS65syf1XUAcD4vf53AIZoGPbRgjd/gmHvmUPrYm+4D9aYat5XUH82f28iqz/LezAAltcIeHGAVqEVqqrQ+B96Ra6DpXWjpKsJDH0aqqaIm2GxYBQ
….

うーん、これはちょっと無理ですね。。