📑

WixStudioでVeloを使う Step 24 「ライトボックスを使う。リピータで表示しているアイテムを編集する」

2023/10/05に公開

はじめに

前回の記事までに作成したメッセージング機能を更に拡張する。具体的には編集機能。メッセージの編集を実現する。新しくライトボックスとかも使ってみたい。

目標

ライトボックスを使ってメッセージの編集機能を作ってみる。

準備

前回の記事「WixStudioでVeloを使う Step 23 「フィールドタイプ『複数の参照先』がキモい?」」の続き。

ライトボックスを用意する

sendMessage画面にライトボックスを追加する。ライトボックスはページ要素として用意されているの。手順はボタン要素やテキスト要素を追加するときと同じ。
データフックを追加
データフックを追加
ライトボックスを追加すると、画面にライトボックスが表示されたような状態の編集画面になる。
ライトボックスが作成された
ライトボックスが作成された
ライトボックスの表示設定を直しておく。ライトボックスの自動表示をオフに返る。
ライトボックスの自動表示をオフにする
ライトボックスの自動表示をオフにする
ライトボックスを作成したタイミングでは自動表示がオンになっているみたい。突然ライトボックスが開いてしまう。
このライトボックスはメッセージを編集するために使う。テキストボックスと保存ボタンを用意しておく。
ライトボックスはメッセージ編集用に使う
ライトボックスはメッセージ編集用に使う
ライトボックスは、サイトページの一つとして管理されるみたい。
ライトボックスはサイトページの一つ
ライトボックスはサイトページの一つ
ライトボックスの編集を完了するには、サイトページの一覧から他のページを選択するか完了ボタンを押せば良い。
ライトボックスの編集完了
ライトボックスの編集完了
これでライトボックスの用意は終わり。配置した要素のIDを列挙しておく。

要素の種類 要素のID 備考
ライトボックス #lightbox1 メッセージ編集画面
テキストボックス #textbox1
ボタン #button2 保存ボタン
ボタン #button1 ライトボックス右上の×ボタン。初めからあった。

スクリプトを書く

書かなければならないスクリプトは大きく2つ。

  1. ライトボックスを使う処理
  2. ライトボックスに入力したメッセージを保存する処理

ライトボックスを使う処理

処理はページコードとして書く。書き始める前に確認事項。

  1. ライトボックスを開く際の操作をどうするか。
    今回はリピータのアイテム(メッセージ)をダブルクリックした際に編集用のライトボックスを表示することにする。
  2. ライトボックスを開く処理は、sendMessage画面のページコード(sendMessage.##.js)に記述する。開いた後の処理はライトボックスのページコード(Center.##.js)に記述する。
    • sendMessage.##.jsには、メッセージをダブルクリックした際にライトボックスを開く処理
    • Center.##.jsには、ライトボックスのテキストボックスやボタンの定義。

まずはsendMessage.##.jsにスクリプトを書く。

sendMessage.##.js(抜粋)
import wixData from 'wix-data';
import {sendMessage,getMyMessages,deleteMessage} from 'backend/Messages'
import { currentMember } from 'wix-members-frontend'
import { openLightbox } from 'wix-window-frontend';
$w.onReady( async function () {
  const me = await currentMember.getMember()
  initData()
  $w('#repeater1').onItemReady( ($item, itemData, index) => {
    onDblClick( event =>{
      wixWindowFrontend.openLightbox('Center',itemData)
    })
    /*以下、省略*/

まず、ライトボックスを開くにはwix-window-frontend.openLightbox()を使う(import { openLightbox } from 'wix-window-frontend';)。ライトボックスを開くタイミングはメッセージをダブルクリックした時としているので$item('#box3').onDblClick( event =>{...})と記述。いつもはクリックイベント(onClick(event=>{..}))を使うけど、今回はダブルクリックイベントなので記述がちょっと違う(onDblClick( event =>{...}))。実際にライトボックスを開く処理はopenLightbox('Center',itemData)。引数は二つ。一つ目はライトボックスの指定。二つ目はライトボックスに渡すデータ。ライトボックスはメッセージの編集用なのでメッセージアイテムを渡しておく。ここで謎なのが一つ目の引数に与えられている'Center'という文字列。これはライトボックスの名前。ライトボックスの名前は「Center」
ライトボックスの名前は「Center」
注意したいことが二つ。一つ目はライトボックスのIDを指定するわけじゃないという点。二つ目はライトボックスの名前は予めわかりやすいものにしておいた方が良いということ(...ライトボックスに限らずボタンとかも)。ライトボックスの名前はページコードのファイル名としても利用される。

https://www.wix.com/velo/reference/wix-window-frontend/openlightbox
https://www.wix.com/velo/reference/$w/box/ondblclick

これでライトボックス自体は開くのでページを確認する。

メッセージをダブルクリックするとライトボックスが開く
メッセージをダブルクリックするとライトボックスが開く

ライトボックスが開くと、意外とライトボックス内部のレイアウトが崩れてたりするのでついでに調整しておく。話が逸れるけど、WixStudioではスタックが使えるのでガンガン使う。結構、お気に入り。だいたい、スタックとマージンとパディングで片付ける。
スタックがお気に入り
スタックがお気に入り

話を戻す。
ライトボックスが開いたらテキストボックスや保存ボタン(と×ボタン)が表示される。これらの要素を使うためのスクリプトを書く。

Center.###.js
import { lightbox } from 'wix-window-frontend';
$w.onReady(function () {
  const context = lightbox.getContext()
  $w('#textBox1').value = context.body
  $w('#button2').onClick( event => {
    context.body = $w('#textBox1').value
    lightbox.close(context)
  })
});

まずimport { lightbox } from 'wix-window-frontend';でライトボックスを利用する為の準備をする。ライトボックスを開く際に渡されたデータを受けとったり、ライトボックスを閉じたりするためにwix-window-frontend.lightboxが必要。const context = lightbox.getContext()でライトボックスを開く際に渡されたデータを受けとる。具体的にはsendMessage.##.jswixWindowFrontend.openLightbox('Center',itemData)で、二つ目の引数がlightbox.getContext()で受けとれる。だからconst contextにはメッセージのアイテムが格納された状態になっている。
$w('#textBox1').value = context.bodyはメッセージの本文を予めテキストボックスに表示させておくための処理。
$w('#button2').onClick( event => {...})は、保存ボタンのクリックイベント。保存ボタンが押されたら二つのことをする。一つはライトボックスを閉じる(lightbox.close(context))処理。.close()の引数にcontextを渡している。引数に渡した値は呼び出し元(.openLightbox())で受けとることが出来る。ただし、ライトボックスを閉じる前にcontext.body = $w('#textBox1').valueを処理する。メッセージアイテムに対して、メッセージの本文(.body)をテキストボックスの内容で書き換えておく。

ライトボックスを使う処理はこれで終わり。

ライトボックスに入力したメッセージを保存する処理

ライトボックスの保存ボタンを押すとライトボックスが閉じられる。そして、本文が書き換えられたメッセージアイテムを取得することが出来る。受けとったメッセージアイテムをMessagesコレクションに保存すれば良い。
保存する処理を予めbackend/Messages.jswに関数saveMessage(message)として定義しておく。

backend/Messages.jsw
export async function saveMessage(message){
  return wixData.save('Messages',message)
}

上記記述をbackend/Messages.jswに定義を追記しておけば良い。この記述では特に新しい事は出てこない。重要なのはwix-data.save()
https://www.wix.com/velo/reference/wix-data/save

次にページスクリプト(sendMessage.##.js)の修正。

sendMessage.##.js
import wixData from 'wix-data';
import {sendMessage,getMyMessages,deleteMessage,saveMessage} from 'backend/Messages'
import { currentMember } from 'wix-members-frontend'
import { openLightbox } from 'wix-window-frontend';
$w.onReady( async function () {
  const me = await currentMember.getMember()
  initData()
  $w('#repeater1').onItemReady(($item, itemData, index) => {
    $item('#box3').onDblClick(event => {
      openLightbox('Center', itemData)
        .then(message => {
          if (message) {
            saveMessage(message)
              .then(result => {
                initData()
                $w('#text4').text = 'メッセージが更新されました'
              })
              .catch(error => {
                $w('#text4').text = 'メッセージが更新されませんでした'
              })
          }
        })
    })
    /*省略*/

定義した関数saveMessage()sendMessage.##.jsで呼び出すのでimport {sendMessage,getMyMessages,deleteMessage,saveMessage} from 'backend/Messages'を記述。sendMessage()の戻り値はwix-data.save()の結果をそのまま返すことにした(Promise)。メッセージの保存が出来ればメッセージが更新された旨を表示する。失敗すればその旨を表示する。保存後にはinitData()を呼び出す。関数initData()にも手を加える。

sendMessage.##.jsのinitData()
async function initData(){
    const messages = await getMyMessages()
    $w('#repeater1').data = []             // <--- 追加
    $w('#repeater1').data = messages

}

追加したのは1行($w('#repeater1').data = [])。実はこの1行に苦しんだ。initData()を呼び出すとMessagesコレクションからデータを取得しリピータにセットしてくれる。しかし、注意点がある。リピータにデータをセットする際にはオブジェクトの配列としてセットする必要がある。さらに配列要素のオブジェクトは一意の_idプロパティを持っていることが条件になる。Messagesコレクションから取得したメッセージはこの条件を満たすので問題無い。が、リピータのデータを差し替えた際に問題が発生する。リピータにデータをセットするとonItemReady()が呼び出されることになっている。しかし、呼び出される条件がある。それはオブジェクトの _idの値が新しい こと。どうやらリピータはデータがセットされる度に全てのアイテムを作り直すわけではないみたい。リピータのデータを差し替える場合、差し替えられる直前のデータと新しいデータの中身(_id)を比較し、新しく増えたものはonItemReady()を呼び出す。逆に消えたものはonItemRemoved()を呼び出す直前から存在するアイテムに対しては何も行わない。そのため、単純にコレクションからデータを取り直してリピータにセットしただけではダメ。少々乱暴な方法になるけど$w('#repeater1').data = []で一度アイテムを消してしまうと上手くいく。本当はsplice()等を使って丁寧に該当要素だけを取り除くほうが良いのかも。onItemRemoved()onItemReady()が呼び出される回数も減るし。

以上でスクリプトの編集も終わり。

動作確認

動作を確認する。二人の会員を用意した。左が会員1、右が会員2。

二人のメッセージ
二人のメッセージ
メッセージをダブルクリックし、会員1がメッセージを書き換える。一番上のメッセージ。
会員1がメッセージを書き換える
会員1がメッセージを書き換える
書き換えれば会員1のメッセージ一覧に反映される。会員2はブラウザを更新すれば反映される。
メッセージが書き換えられた
メッセージが書き換えられた

https://n5creation.wixstudio.io/zenn-0024

まとめ

すっごく苦労した。実はやりたいことは単純だったし、すぐに終わると思ってた。ただ、リピータが思っていた以上に賢かった。リピータのデータはセットすれば全てのアイテムが書き換えられるとばかり思っていたがそんな事はしない。データ量が増えれば非効率だし変更があったアイテムだけを更新するべき。リピータはそうできている。初めから気づいていれば苦労しなかったと思う。忘れないようにする。ライトボックスの実装は簡単。これは何も苦労しなかった。普通にJavascriptでライトボックスを作ろうとすれば面倒も多いけど、WixStudioで組み込むのは秒で終わる。Veloで扱うのも簡単だった。新しく憶えることはimport wixWindowFrontend from 'wix-window-frontend'wixWindowFrontend.openLightbox()wixWindowFrontend.lightboxの二つが今回は登場した。

今回作成したメッセージの編集機能。実はまだ問題を抱えてる。自身が送ったメッセージを相手も編集できてしまうという点。最低最悪の問題。この問題はMessagesコレクションの権限設定(の内容)が原因で発生している。具体的にはサイト会員はコンテンツの更新が可能という権限。次回はこれを解消する?

参考

sendMessage.##.js
import wixData from 'wix-data';
import { sendMessage, getMyMessages, deleteMessage, saveMessage } from 'backend/Messages'
import { currentMember } from 'wix-members-frontend'
import { openLightbox } from 'wix-window-frontend';

$w.onReady(async function () {
  const me = await currentMember.getMember()
  initData()
  $w('#repeater1').onItemRemoved(item => {
    console.log(item)
  })
  $w('#repeater1').onItemReady(($item, itemData, index) => {
    $item('#box3').onDblClick(event => {
      openLightbox('Center', itemData)
        .then(message => {
          if (message) {
            saveMessage(message)
              .then(result => {
                initData()
                $w('#text4').text = 'メッセージが保存されました'
              })
              .catch(error => {
                $w('#text4').text = 'メッセージが保存されませんでした'
              })
          }
        })
    })
    if (itemData.fromMember._id == me._id) {
      $item('#box3').customClassList.add('from-me')
    } else {
      $item('#box3').customClassList.add('to-me')
    }
    $item('#text7').text = itemData.body
    $item('#text6').text = me._id == itemData.fromMember._id ? null : `From:${itemData.fromMember.nickname}`
    $item('#text5').text = me._id == itemData.toMember._id ? null : `To:${itemData.toMember.nickname}`
    $item('#button2').onClick(event => {
      deleteMessage(itemData._id)
        .then(result => {
          initData()
          $w('#text4').text = 'メッセージが削除されました'
        })
        .catch(error => {
          $w('#text4').text = 'メッセージが削除されませんでした'
        })
    })
  })
  wixData.query('Members/PublicData').find()
    .then(results => {
      const selectItems = results.items.map(item => {
        return {
          'label': item.nickname,
          'value': item._id
        }
      })
      $w('#dropdown1').options = selectItems
    })
  $w('#button1').onClick(async evnet => {
    const messageObj = {}
    messageObj.toMember = $w('#dropdown1').value
    messageObj.body = $w('#textBox1').value
    await sendMessage(messageObj)
      .then(isSuccess => {
        if (isSuccess) {
          $w('#dropdown1').value = null
          $w('#textBox1').value = null
          $w('#text4').text = 'メッセージが送信されました。'
        } else {
          $w('#text4').text = 'メッセージが送信できませんでした。'
        }
      })
    initData()
  })
});
async function initData() {
  const messages = await getMyMessages()
  $w('#repeater1').data = []
  $w('#repeater1').data = messages
}
backend/Messages.jsw
import wixData from 'wix-data';
import { currentMember } from 'wix-members-backend';
export async function sendMessage(messageObj) {
  const me = await currentMember.getMember()
    .then(member => member)
    .catch(error => {
      console.log(error)
      return false
    })
  if (!me) return false
  messageObj.fromMember = me
  return wixData.save('Messages', messageObj)
    .then(result => true)
    .catch(error => {
      console.log('failed:', messageObj)
      return false
    })
}
export async function getMyMessages() {
  const me = await currentMember.getMember()
  return wixData.query('Messages').eq('fromMember', me._id).include('fromMember').include('toMember')
    .or(
      wixData.query('Messages').eq('toMember', me._id).include('fromMember').include('toMember')
    )
    .not(
      wixData.query('Messages').hasSome('deletedMembers', me._id)
    )
    .descending('_createdDate')
    .find().then(results => {
      console.log(results.items)
      return results.items
    }).catch(error => [])
}
export async function deleteMessage(messageId) {
  const me = await currentMember.getMember()
  return wixData.insertReference('Messages', 'deletedMembers', messageId, me._id)
}
export async function saveMessage(message) {
  return wixData.save('Messages', message)
}
Center.##.js
import { lightbox } from 'wix-window-frontend';
$w.onReady(function () {
  const context = lightbox.getContext()
  $w('#textBox1').value = context.body
  $w('#button2').onClick( event => {
    context.body = $w('#textBox1').value
    lightbox.close(context)
  })
});

つづき

https://zenn.dev/niibori/articles/veloonwixstudio-beginner-0025

Discussion