🎠

Framer + RemixでWebサイトをつくる #12 - Codeで表現する - Code Overrides編

2021/12/25に公開

引き続き開催しているもくもく会「Kamakura MokMok Hack」のサイトをFramer + Remixでつくっていきます。サイトの要件などに興味ある方は1日目の記事をご覧ください。

次のものはここまでFramerでつくったもののプレビューです。
https://framer.com/share/kamakura-dev-change-component--645V68V9BSByvDYoHLJp/Ih6d4c8rJ

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 というセクションがあります。この「+」ボタンをクリックします。

初期状態だと FileExample が指定されています。自分でファイル作成したい場合は File > New File... からファイルを新規作成します。Code Overridesのファイルは .tsx 拡張子のファイルが作成できます。

これはOverridesするオブジェクトまたはSmart ComponentsがReact Componentで構成されており、それを受け取って各プロパティ(=props)やStyleを変更し、またそのComponentを返すことでOverride = 上書きが成立します。

というわけで、DynamicScrollBar.tsx という名前で新規作成したコードは初期でExampleと同じコードが含まれています。また、Framer上にコードエディタが起動します。では、Exampleのコードちょっとみてましょう。

DynamicScrollBar.tsx
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 にマウスカーソルを乗せると拡大縮小するようになります。

プレビュー

https://framer.com/share/kamakura-dev-override-hover--bd6MRQz2AWagv5iKUuSQ/Ih6d4c8rJ


Scrollイベントからスクロールポジションを受ける

Framerは8日目の記事 で書いた通りScroll Componentとその中身になるコンテンツを紐付けてスクロールを実現しています。
Webブラウザなどのアプリケーションならスクロール値は window.pageYOffsetを使いますが、Framerの場合は要素内のスクロール値を取れれば良いので element.currentTarget.scrollTop で値を取ります。

というわけで以下がそのコードです。

DynamicAppBar.tsx
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を確認してください。

https://www.framer.com/motion/
https://www.framer.com/docs/component/

主なイベントpropsは次の通り

props 説明
animate プレビューがレンダリングされた際や値に変更があった際に発火
whileHover マウスカーソルがHoverしている間に発火
whileTap クリックまたはタップしている間に発火

今回は scrollStatusSCROLL = スクロール中 の時にヘッダを高さ分ほど上に移動させるようにしています。

その他

useEffectuseCallback はReact.jsのものなのでReactがわからないって方はぜひこれを機にReactを学びましょう 🥳 { 端折りすぎw



というわけで、今回はCode Overridesについてまとめてみました。このOverridesを使うとデザインツール上で状態管理を導入できます。また、FramerはDyanmic Urlを駆使してpackageのimportを解決しています。勘のいい人はわかると思いますが、実際のプロダクト開発で実装されたReact ComponentをFramerに取り入れることもできなくはないです(工夫は必要ですが)。その辺は次回の「Code Components」を踏まえてどこかでアウトプットできればと思います。

今回のプレビュー

https://framer.com/share/kamakura-dev-code-overrides--vnC8UVzkLoMK0t9yiXJa/Ih6d4c8rJ


Discussion