Framer + RemixでWebサイトをつくる #12 - Codeで表現する - Code Overrides編
引き続き開催しているもくもく会「Kamakura MokMok Hack」のサイトをFramer + Remixでつくっていきます。サイトの要件などに興味ある方は1日目の記事をご覧ください。
次のものはここまでFramerでつくったもののプレビューです。
12日目 - Codeでいろいろ表現する
Framerには「Code Components」と「Code Overrides」というTypeScriptでComponentをつくったり、Smart Componentsにロジックや処理を追加したりすることができます。
ここまでにつくったものだとTopNavigationBarが常に固定になっていて鬱陶しいので、Scroll時に隠れる様に変更したいと思います。
Code Overridesを使う
Code Overridesとは公式の説明によると「レンダリング時に実行されるロジックや処理を持たせたJavaScript関数」とのことです。
Code Overrides are JavaScript functions that are executed the moment you render your prototype
ここではJavaScriptと書いてありますがTypeScriptで記述ができますので静的な型付けもできます。
というわけで早速やってみましょう。
Overrides用のtsxファイルを作成する
まずは今回はTopNavigationBar を選択します。すると右のコンパネの下の方に Overrides
というセクションがあります。この「+」ボタンをクリックします。
初期状態だと File
は Example
が指定されています。自分でファイル作成したい場合は File > New File...
からファイルを新規作成します。Code Overridesのファイルは .tsx
拡張子のファイルが作成できます。
これはOverridesするオブジェクトまたはSmart ComponentsがReact Componentで構成されており、それを受け取って各プロパティ(=props)やStyleを変更し、またそのComponentを返すことでOverride = 上書きが成立します。
というわけで、DynamicScrollBar.tsx
という名前で新規作成したコードは初期でExampleと同じコードが含まれています。また、Framer上にコードエディタが起動します。では、Exampleのコードちょっとみてましょう。
import type { ComponentType } from "react"
import { createStore } from "https://framer.com/m/framer/store.js@^1.0.0"
import { randomColor } from "https://framer.com/m/framer/utils.js@^0.9.0"
// Learn more: https://www.framer.com/docs/guides/overrides/
const useStore = createStore({
background: "#0099FF",
})
export function withRotate(Component): ComponentType {
return (props) => {
return (
<Component
{...props}
animate={{ rotate: 90 }}
transition={{ duration: 2 }}
/>
)
}
}
export function withHover(Component): ComponentType {
return (props) => {
return <Component {...props} whileHover={{ scale: 1.05 }} />
}
}
export function withRandomColor(Component): ComponentType {
return (props) => {
const [store, setStore] = useStore()
return (
<Component
{...props}
animate={{
background: store.background,
}}
onClick={() => {
setStore({ background: randomColor() })
}}
/>
)
}
}
Hoverがわかりやすそうなのでこれを例にコードを見ていきます。
// ComponentType関数型を返す関数
export function withHover(Component): ComponentType {
/*
ComponentType関数型は引数でFramer上で
指定した色々なプロパティ = propsを受け取れます。
*/
return (props) => {
// whileHover propsを渡すComponentとして返す
return <Component {...props} whileHover={{ scale: 1.05 }} />
}
}
Overridesは関数を指定したオブジェクトやSmart Componentsを Component
という引数で受け取ります。受け取ったComponentのpropsをまとめて渡しつつ、whileHover
(hoverしている間)にscale
を1.05倍拡大するスタイルを上書きしています。
これをTopNavigationBar に設定します。設定するにはTopNavigationBar を選択し、右コンパネのOverridesセクションにある Overrides
プロパティでさきほどの withHover関数を指定します。
こうすることでTopNavigationBar にマウスカーソルを乗せると拡大縮小するようになります。
プレビュー
Scrollイベントからスクロールポジションを受ける
Framerは8日目の記事 で書いた通りScroll Componentとその中身になるコンテンツを紐付けてスクロールを実現しています。
Webブラウザなどのアプリケーションならスクロール値は window.pageYOffset
を使いますが、Framerの場合は要素内のスクロール値を取れれば良いので element.currentTarget.scrollTop
で値を取ります。
というわけで以下がそのコードです。
import type { ComponentType } from "react"
import { useEffect, useCallback } from "react"
import { createStore } from "https://framer.com/m/framer/store.js@^1.0.0"
// 状態管理用のStore作成
const useStore = createStore<Store>({
scrollStatus: "STOP",
lastScrollPosition: 0,
})
//Headerの高さ
const headerHeight = 105
// Scroll時に表示・停止中に非表示をする
export function withDynamicScroll(Component): ComponentType {
return (props) => {
const [store] = useStore()
const { scrollStatus } = store
return (
<Component
{...props}
animate={{
y: scrollStatus === "SCROLL" ? headerHeight * -1 : 0,
}}
/>
)
}
}
// Scrollを検知して値を取得しスクロール中か停止中かStoreへ保存する
export function withTrackingScroll(Component): ComponentType {
return (props) => {
const [store, setStore] = useStore()
const scrollHandler = useCallback(
(element: React.UIEvent<HTMLElement>) => {
const { scrollTop } = element.currentTarget
const { lastScrollPosition } = store
let scrollStatus: ScrollStatus = "STOP"
if (headerHeight < scrollTop) {
scrollStatus = "SCROLL"
} else {
scrollStatus = "STOP"
}
if (lastScrollPosition > scrollTop) scrollStatus = "STOP"
setStore({ lastScrollPosition: scrollTop, scrollStatus })
},
[]
)
return <Component {...props} onScroll={scrollHandler} />
}
}
type Store = {
scrollStatus?: ScrollStatus
lastScrollPosition: number
}
type ScrollStatus = "SCROLL" | "STOP"
ざっくりとコードの解説をします。
Storeについて
createStore
は関数間で使用できるFramerが持っているグローバルなStore(State=状態値の集約されたもの)です。プレビューがレンダリングされてから破棄されるまで保持されます。
const useStore = createStore<Store>({
scrollStatus: "STOP",
lastScrollPosition: 0,
})
Framerが持っている useStore
Hooksを使うことでStoreの値と保存するための関数 setStore
を返します。Storeの値はReduxなどと同様に変更を検知してHooksに渡されます。
const [store, setStore] = useStore()
setStoreは一部だけ渡す際は、setStore({...store, 更新したいState})
として新しい値を渡します。
// 例
setStore({ ...store, scrollStatus: "SCROLL" })
Animationについて
<Component
{...props}
animate={{
y: scrollStatus === "SCROLL" ? headerHeight * -1 : 0,
}}
/>
上のコードの様に、FramerのComponentにはframer-motionと同様のアニメーション用のpropsが用意されています。詳しくはframer-motionのAPIとFramerのMotion Component APIを確認してください。
主なイベントpropsは次の通り
props | 説明 |
---|---|
animate | プレビューがレンダリングされた際や値に変更があった際に発火 |
whileHover | マウスカーソルがHoverしている間に発火 |
whileTap | クリックまたはタップしている間に発火 |
今回は scrollStatus
が SCROLL = スクロール中
の時にヘッダを高さ分ほど上に移動させるようにしています。
その他
useEffect
や useCallback
はReact.jsのものなのでReactがわからないって方はぜひこれを機にReactを学びましょう 🥳 { 端折りすぎw
というわけで、今回はCode Overridesについてまとめてみました。このOverridesを使うとデザインツール上で状態管理を導入できます。また、FramerはDyanmic Urlを駆使してpackageのimportを解決しています。勘のいい人はわかると思いますが、実際のプロダクト開発で実装されたReact ComponentをFramerに取り入れることもできなくはないです(工夫は必要ですが)。その辺は次回の「Code Components」を踏まえてどこかでアウトプットできればと思います。
今回のプレビュー
Discussion