はじめに
オープンエイトの大津です。 在宅勤務が始まり、かなり日にちが経ちましたね。僕は在宅勤務開始時にPCデスクや椅子がなくコタツ机で仕事を行なっていたのですが、最終的にデスク・椅子ともに買い揃えました。
デスクと椅子がほぼ同時期に届いたため、部屋がダンボール等の荷物で埋め尽くされることになりました!
僕は現在、VBA(VIDEO BRAIN Analytics)のフロントの開発を年明け頃からしています。 このVBAの開発で印象に残っているものとして、マージンに対する取り組みをご紹介したいと思います。
VBAは、SPA(React + TypeScript)で開発されていて、CSSによるスタイリングにはstyled-componentsを利用しています。 今回の話は、styled-componentsを活用してマージンの付け方をルール化したという話になります。そのため、TyepScriptの話はほぼ触れることはないです。すみません。
最後にサンプルコードを載せているので、ぜひ最後までご覧ください!!
マージンの取り組み
マージンに対する取り組みで行なったことは、styled-componentsを利用してpropsからmargin・paddingを指定できる様にした
ことです。
styled-componentsについて調べる中で、ユースケースについて書かれいるこちらの記事がすごく参考になりました。 この中にある「レイアウトの組み方一例」を自分風に作り上げてみました。
styled-componentsの使い方(パッとわかりやすく、色々なパターンを説明することを目指しています) · GitHub
簡単にJSXを記述すると次のようになります。
<LayoutBox margin={{ top: 16 }}> {...} </LayoutBox>
LayoutBoxはstyled-componentsで作成したカスタムコンポーネントで、margin・paddingというpropsを受け取るようにしています。
margin・paddingともに { top?: number, right?: number, bottom?: number, left?: number }
の値を想定しており、受け取ったpropsの数値をもとに各方向へmargin・paddingを取る仕組みとなっています。
このようにすることで、次のメリットが得られるのではないかと考えています。
- JSXの並びをみることでJSX間のマージンが確認することができる
- buttonやpなど特定のJSXにも同様のcssを付与して使用できる
- 拡張の際に意図せずにmargin・paddingが引き継がれない
JSXの並びをみることでJSX間のマージンが確認することができる
こちらが最初にして、一番大きな理由です。
次のようにstyled-componentsでcssを付与すると、ファイルの中身が多くなるにつれて作ったコンポーネントとJSXが離れてしまいmargin・paddingが分かりづらくなってしまいます。 marign・paddingの確認をするたびに画面をスクロールさせるのは大変ですし、目的のコンポーネントを見失うことが多くなりそうだと思います。
const XComponent = styled.div` padding: 8px; // any css `; const YComponent = styled.div` padding: 0px 8px; // any css `; const ZComponent = styled.div` margin-top: 16px; // any css `; <> <XComponent> {...} </Xcomponent> <YComponent /> <ZComponent> </>
これらのコンポーネントをLayoutBoxから拡張して、propsでマージンを取ることでJSXの並びを見ればファイルの中身が多くなろうとも各JSX間で取られているmargin・paddingが分かりやすくなるかと思います。
const XComponent = styled(LayoutBox)` // any css `; const YComponent = styled(LayoutBox)` // any css `; const ZComponent = styled(LayoutBox)` // any css `; <> <XComponent padding={{ top: 8, right: 8, bottom: 8, left: 8 }}> {...} </Xcomponent> <YComponent padding={{ left: 8, right: 8 }} /> <ZComponent margin={{ top: 8 }}> </>
このように各JSX間で取られているmargin・paddingが一目で分かることが僕は好きです。 修正するときは、JSXの並びやmargin・paddingを確認し、その場で数値を変更するだけで良いので楽に対応できると思います。
buttonやpなど特定のJSXにも同様のcss付与して使用できる
こちらはstyled-componentsの仕様を活かしたメリットです。 LayoutBoxをそのまま使い続けるとどうしてもネストが深くなりがちなので、buttonなど他のJSXには次のように付与することができます。
const BaseButton = styled(LayoutBox.withComponent(button))` // any css `; <BsaseButton margin={{ top: 8, bottom: 8 }} />
styled-componentsで作成したコンポーネントはwithComponentメソッドを利用することで、他のJSXに自身の持つcssを与えることができます。
また、styled-componentsで作成したcssはclassName
で付与されるので次のようにclassName
をpropsで受け取るようにしていれば自作したコンポーネントにも利用できます。
type Props = { className?: string; } const CustomComponent: React.FC<Props> = ({ className }) => { return ( <div className={className} /> {...} </div> ) } export default LayoutBox.withComponent(CustomComponent);
こちらの記法は便利だと思っていて、VBAではテキストやボタンに関するコンポーネントをはじめとしたmargin・paddingが必要であろうコンポーネントによく使われています。
拡張の際に意図せずにmargin・paddingが引き継がれない
styled-componentsの拡張は、記述したCSSの内容を引き継げるため、DRYに則ることができます。 この時、意図していないCSSまで引き継いでしまうことは避けたいことだと考えています。
皆気をつけるようにしているとは思いますが、この問題は全く起こらない問題ではないかなと思います。誰かが作ったコンポーネントを拡張し、そのコンポーネントに共通項目として新たなCSSを追加する人もいるかもしれません。CSSを追加した結果、自分が対応する箇所とは別の箇所に意図せず影響を与える可能性があります。
数値が指定されているmargin・paddingを継承すると、レイアウトを大きく崩す恐れがあるため、継承を避けられるのは素直に嬉しいです。
便利なレイアウト用メソッド
LayoutBoxのpropsであるmargin・paddingは{ top?: number, right?: number, bottom?: number, left?: number }
を受け取ります。
記述する時に { left: 8, right: 8 }
など同じ数値を毎回書くのは面倒だという考えのもと次のようなレイアウト用のメソッドを用意しました。
export const lo = { overall: (num: number): BoxLayout => ({ top: num, right: num, bottom: num, left: num }), horizontal: (num: number): Partial<BoxLayout> => ({ right: num, left: num }), vertical: (num: number): Partial<BoxLayout> => ({ top: num, bottom: num }) };
left, rightに同一の値を入れるならば、lo.horizontal(8)
のように利用することで目的のオブジェクトを生成できるようにしました。
この辺りは、好みもあると思いますのでチームで相談して利用すればいいのかなと思います。
おわりに
今回は、margin・paddingをprops指定した話をさせていただきました。 このmargin・paddingの指定方法は個人的にかなり気に入っています。ですが、プロジェクトの最初期に作成したものであるため、まだまだ改良したい部分もあったりします。
例えば、margin・paddingを別々のpropsとしていたが、同一のpropsから指定できるようにして、margin・paddingを指定するために専用のレイアウト用メソッドを作成するなど考えてみたりはしました。けれど、他の開発者からLayoutBox自体のコードが読みづらくなるかなと思いました。
その他、marginを指定しない場合はmargin: 0px 0px 0px 0px
のように全て0としてCSSが付与されてしまっているので、propsで数値を指定しない場合はCSSが付与されないようにしたいですね!
このLayoutBoxではTypeScriptの型に関する使いかたもかなり練習できたので良かったです。
付録
本文中では特に触れられませんでしたが、margin・paddingのpropsに{ top?: number, right?: number, bottom?: number, left?: number }
の配列を渡しても良いように設計しているため、Object.prototype.toStringを用いた型判定のメソッドを自作しています。
type LayoutBoxProps = { top: number; right: number; bottom: number; left: number; }; type Margin = { margin?: Partial<LayoutBoxProps> | Partial<LayoutBoxProps>[]; }; type Padding = { padding?: Partial<LayoutBoxProps> | Partial<LayoutBoxProps>[]; }; const DEFAULT_VALUE: LayoutBoxProps = { top: 0, right: 0, bottom: 0, left: 0 }; // オブジェクトであるか判定するメソッド function isLayoutBoxObj( layout: Partial<LayoutBoxProps> | Partial<LayoutBoxProps>[] ): layout is Partial<LayoutBoxProps> { return isObject(layout); } // 配列であるか判定するメソッド function isLayoutBoxArray( layout: Partial<LayoutBoxProps> | Partial<LayoutBoxProps>[] ): layout is Partial<LayoutBoxProps>[] { return isArray(layout); } function createLayoutBox( layoutBox: Partial<LayoutBoxProps> | Partial<LayoutBoxProps>[] | undefined ): LayoutBoxProps { if (!layoutBox) { return DEFAULT_VALUE; } if (isLayoutBoxObj(layoutBox)) { return { ...DEFAULT_VALUE, ...layoutBox }; } if (isLayoutBoxArray(layoutBox)) { let resultLayout = { ...DEFAULT_VALUE }; layoutBox.forEach((layoutItem: Partial<LayoutBoxProps>) => { resultLayout = { ...resultLayout, ...layoutItem }; }); return resultLayout; } return DEFAULT_VALUE; } const LayoutBox = styled.div<Margin & Padding>` margin: ${({ margin }): string => { const { top, right, bottom, left } = createLayoutBox(margin); return `${top}px ${right}px ${bottom}px ${left}px`; }}; padding: ${({ padding }): string => { const { top, right, bottom, left } = createLayoutBox(padding); return `${top}px ${right}px ${bottom}px ${left}px`; }}; `; //レイアウト用のUtils export const lo = { overall: (num: number): LayoutBoxProps => ({ top: num, right: num, bottom: num, left: num }), horizontal: (num: number): Partial<LayoutBoxProps> => ({ right: num, left: num }), vertical: (num: number): Partial<LayoutBoxProps> => ({ top: num, bottom: num }) }; export default LayoutBox;