この記事が対象としている人
- kintoneのJSカスタマイズについての基本的な知識がある人
- ReactやVueなどのリアクティブなUIライブラリについて基本的な知識がある人
- Vanilla JSで軽いJSカスタマイズを書くこともあるけど、そういうときにReactとかは厳しいし、かといってJQuery使うのも嫌だし、なんかいいライブラリないかな…と思っている人
VanJS
下記の記事で、VanJSというライブラリについて知りました(著者に感謝)。
VanJSは超軽量で外部ライブラリに依存しない、unopinionatedなリアクティブなUIフレームワークで、Vanilla JSとDOMをベースにしています、とのことです(公式ページより)。
公式ページのHello Worldを読むと、JSXを使わずにdiv
、p
といったDOM要素を返す関数ですっきりコンポーネントを書けることが分かります。
// 再利用可能なコンポーネントは単純なVanilla Javascriptの関数として書けます。
// Reactの規約に沿って最初の文字を大文字にしています。
const Hello = () => div(
p("👋Hello"),
ul(
li("🗺️World"),
li(a({href: "https://vanjs.org/"}, "🍦VanJS")),
),
)
van.add(document.body, Hello())
// document.body.appendChild(Hello())と書くこともできます。
下記のコードは、Reactの入門記事でよくありそうなシンプルなカウンターのサンプルです。
const Counter = () => {
const counter = van.state(0)
return span(
"❤️ ", counter, " ",
button({onclick: () => ++counter.val}, "👍"),
button({onclick: () => --counter.val}, "👎"),
)
}
van.add(document.body, Counter())
「へー、宣言的なUIをVanilla JSでこんなにシンプルに書けるんだ。kintoneカスタマイズでも使えるかも」と思った(自分のような)方は、続きを読んでみていただければと思います。
kintoneカスタマイズのサンプル
宣言的に書くうれしさのあるサンプルを考えたら、けっこう長いものになってしまった…
- カスタマイズのベースはアプリテンプレートの「日報アプリ」
- 一覧画面に開始日/終了日の入力フィールドを表示
- 日付が入力した開始日〜終了日のレコードを取得して表示
- 日付入力やレコードの表示にはkintone UI Componentを使用
- ページネーションつき
というカスタマイズを、一覧のカスタマイズビューに実装します。
カスタマイズの準備
- 「日報」アプリを新規作成します。
- カスタマイズ形式の一覧「DateRange」を作成します。
- HTMLに
<div id="root" />
と入力します。
- HTMLに
- JavaScriptカスタマイズに下記を設定します。
- https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.js
- https://unpkg.com/kintone-ui-component/umd/kuc.min.js
-
van-1.1.0.nomodule.min.js
-
https://vanjs.org/start#download-table からダウンロードして設定します。
- ここでは
.nomodule
が付くファイルを使用してください。- kintoneカスタマイズを
type="module"
で動作させることができないためです。- できるようにしてほしいです → Cybozuさん
- kintoneカスタマイズを
- ここでは
-
https://vanjs.org/start#download-table からダウンロードして設定します。
-
list-query.js
- ここで作成するカスタマイズです。
カスタマイズのコード
// list-query.js
const { span, div, a } = van.tags
const LIMIT = 10
kintone.events.on('app.record.index.show', (event) => {
if (event.viewName !== 'DateRange') {
return event
}
const fromDate = van.state('')
const toDate = van.state('')
const fromDatePicker = new Kuc.DatePicker({
label: '開始日',
className: 'horizontal',
onchange: (e) => (fromDate.val = e.detail.value),
})
const toDatePicker = new Kuc.DatePicker({
label: '終了日',
className: 'horizontal',
onchange: (e) => (toDate.val = e.detail.value),
})
van.add(
kintone.app.getHeaderMenuSpaceElement(),
div({ style: `display: flex; gap: 16px; margin-left: 16px` }, fromDatePicker, toDatePicker)
)
const currentRecords = van.state([])
const totalCount = van.state(0)
const offset = van.state(0)
const isLoading = van.state(false)
van.derive(async () => {
try {
isLoading.val = true
const conditions = [fromDate.val && `日付 >= "${fromDate.val}"`, toDate.val && `日付 <= "${toDate.val}"`].filter(
Boolean
)
if (conditions.length === 0) {
currentRecords.val = []
offset.val = 0
totalCount.val = 0
return
}
const query = `${conditions.join(' and ')} limit ${LIMIT} offset ${offset.val}`
const result = await new KintoneRestAPIClient().record.getRecords({
app: kintone.app.getId(),
query,
totalCount: true,
})
currentRecords.val = result.records
totalCount.val = result.totalCount
} finally {
isLoading.val = false
}
})
const tableData = van.derive(() =>
currentRecords.val.length > 0
? currentRecords.val.map((record) => recordToData(record))
: [
{
日付: 'データなし',
ドロップダウン: '',
文字列__複数行_: '',
文字列__複数行__0: '',
},
]
)
const columns = [
{ title: '日付', field: '日付' },
{ title: '部署', field: 'ドロップダウン' },
{ title: '業務内容', field: '文字列__複数行_' },
{ title: '所感、学び', field: '文字列__複数行__0' },
]
const Table = () =>
new Kuc.ReadOnlyTable({
actionButton: false,
columns,
data: tableData.val,
style: { marginLeft: '16px' },
rowsPerPage: LIMIT,
})
const Pagenation = () => {
return totalCount.val > 0
? div(
{ style: `display: flex; gap: 8px;` },
offset.val > 0 ? a({ onclick: () => (offset.val -= LIMIT) }, '<') : '<',
offset.val < totalCount.val - LIMIT ? a({ onclick: () => (offset.val += LIMIT) }, '>') : '>',
span(`${offset.val} - ${Math.min(totalCount.val, offset.val + LIMIT)}(${totalCount.val}件中)`)
)
: ''
}
van.add(document.getElementById('root'), div({ style: 'margin-left: 16px;' }, Pagenation, Table))
const spinner = new Kuc.Spinner()
van.derive(() => {
if (isLoading.val) spinner.open()
else spinner.close()
})
return event
})
const recordToData = (record) => {
return Object.fromEntries(Object.entries(record).map(([code, field]) => [code, field.value]))
}
addStyle(`
[class^="kuc-date-picker"][class$="__group"] {
flex-direction: row;
align-items: center;
}
[class^="kuc-date-picker"][class$="__group__label"] {
padding: 0 8px 0 0;
}
`)
function addStyle(styleText) {
const style = document.createElement('style')
style.textContent = styleText
document.head.appendChild(style)
}
…長いですね。すみません。要点を軽く説明してみます。
const fromDate = van.state('')
const toDate = van.state('')
const fromDatePicker = new Kuc.DatePicker({
label: '開始日',
className: 'horizontal',
onchange: (e) => (fromDate.val = e.detail.value),
})
const toDatePicker = new Kuc.DatePicker({
label: '終了日',
className: 'horizontal',
onchange: (e) => (toDate.val = e.detail.value),
})
van.add(
kintone.app.getHeaderMenuSpaceElement(),
div({ style: `display: flex; gap: 16px; margin-left: 16px` }, fromDatePicker, toDatePicker)
)
ここで、開始日と終了日を格納するStateを用意しています。
fromDatePicker
、toDatePicker
はkintone UI ComponentのDatePickerを利用していますが、VanJSで通常のDOM要素のように扱えています。
DatePickerのonchange
で入力値をStateに格納しています。Stateの読み書きはval
プロパティから行うのがVanJSのお作法です。
const currentRecords = van.state([])
const totalCount = van.state(0)
const offset = van.state(0)
const isLoading = van.state(false)
van.derive(async () => {
try {
isLoading.val = true
const conditions = [fromDate.val && `日付 >= "${fromDate.val}"`, toDate.val && `日付 <= "${toDate.val}"`].filter(
Boolean
)
if (conditions.length === 0) {
currentRecords.val = []
offset.val = 0
totalCount.val = 0
return
}
const query = `${conditions.join(' and ')} limit ${LIMIT} offset ${offset.val}`
const result = await new KintoneRestAPIClient().record.getRecords({
app: kintone.app.getId(),
query,
totalCount: true,
})
currentRecords.val = result.records
totalCount.val = result.totalCount
} finally {
isLoading.val = false
}
})
入力した開始日、終了日からクエリを作成してレコードを取得している部分です。
van.derive
のところは、ReactのuseEffect
を連想した方も多いかと思いますが、関数をStateにBindするには、van.derive
に渡す必要があります。
これで、fromDate
、toDate
が更新されるたびにクエリを作成してレコードを取得しなおし、レコードと件数をStateに格納されます。
あとは、Stateに格納されたレコードの値をkintone UI ComponentのReadOnlyTableを使って表示しています。
van.derive
について補足
自分はvan.derive
の理解に少し時間が掛かったので、ちょっと補足を。
さきほど記載したカウンターのサンプルで、counter
を2倍にしたdoubledCounter
という変数を定義するとします。
const {button, div, span} = van.tags
const Counter = () => {
const counter = van.state(0)
// NG
// const doubledCounter = counter * 2
// GOOD
const doubledCounter = van.derive(() => counter.val * 2)
return div(
span(
"❤️ ", counter, " ",
button({onclick: () => ++counter.val}, "👍"),
button({onclick: () => --counter.val}, "👎"),
),
span(
"❤️ x 2 ", doubledCounter
)
)
}
van.add(document.body, Counter())
Reactだと、コンポーネント関数は再レンダリングのたびに実行されるので、const doubledCounter = counter * 2
と書けるのですが、VanJSではこれはNGです。
以下、公式ドキュメントのvan.derive
の説明を意訳してみました。「派生(derived)state」という概念に留意してください。
`van.derive`は、派生関数fに基づいた「派生(derived)Stateオブジェクト」を生成します。
「派生Stateオブジェクト」の`val`は、常にfの結果と同期しています。つまり、その依存性が変化するたびに、同期的にfが呼び出されて「派生Stateオブジェクト」の`val`が更新されます。
上記のサンプルでは、counter.val
が変化するたびに、
-
() => counter.val * 2
が実行され、派生StateのdoubledCounter
が更新される -
doubledCounter
とbindされているspanが更新される
という動作になります。
まとめ
いまのところ、VanJSの実戦投入例はまだひとつだけなのですが、今後、適用できるユースケースがあれば、積極的に使っていきたいと思っています。