1
/
5

TS+styled-componentsでマージンの取り方を工夫してみた

はじめに
オープンエイトの大津です。 在宅勤務が始まり、かなり日にちが経ちましたね。僕は在宅勤務開始時にPCデスクや椅子がなくコタツ机で仕事を行なっていたのですが、最終的にデスク・椅子ともに買い揃えました。

デスクと椅子がほぼ同時期に届いたため、部屋がダンボール等の荷物で埋め尽くされることになりました!

僕は現在、VBA(VIDEO BRAIN Analytics)のフロントの開発を年明け頃からしています。 このVBAの開発で印象に残っているものとして、マージンに対する取り組みをご紹介したいと思います。


リモートワークを支援するインハウスAI動画編集クラウド「VIDEO BRAIN」 SNS分析・投稿サービス「VIDEO BRAIN Analytics」β版を提供開始コロナ禍において急増するSNSでの企業からの情報発信をサポート | 株式会社オープンエイト
コロナ禍において自宅待機や在宅勤務などの措置で外出が制限される中、SNSの利用が急増しています。米ツイッター社が3月23日に発表した決算報告では、1~3月期は3月23日までの1日当たりの広告を閲覧した利用者数が1億6400万人となり、前年同期から23%増加、2019年10~12月期からは8%増加したとしています。このような状況下で、企業はSNSを通じて価値提供を行い、生活者との繋がりを一層強...
https://open8.com/news/pressrelease/5457/

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;
株式会社オープンエイトでは一緒に働く仲間を募集しています
同じタグの記事
今週のランキング