CSS trong JavaScript: Công cụ không thể thiếu cho component-based styling

Nhờ vào việc sự dụng inline style, chúng ta có thể lập trình và tận dụng gần như tất cả tiềm năng của Javascript. Đặc biệt là các lợi ích từ CSS pre-processor  (variables, mixins, và functions). Đồng thời, nhiều vấn đề phát sinh từ CSS cũng được giải quyết, như xung đột trong style cũng như namespacing.

Để hiểu thêm về vấn đề của CSS trong JavaScript được giải quyết như thế nào, bạn hãy vào xem React CSS in JS. Còn nếu bạn muốn hiểu thêm về việc cải thiện hiệu năng nhờ vào Aphrodite, bạn có thể đọc tại Inline CSS at Khan Academy: Aphrodite. Ngoài ra, Airbnb’s styleguide cũng là một nguồn khá hữu ích cho những bạn muốn biết về CSS trong Javascript.

Qua bài viết này, tôi sẽ dùng phương thức inline JavaScript style để lập trình nhằm giải quyết một số vấn đề liên quan đến thiết kế cấu trúc mà tôi đã đề cập trong bài viết trước Before you can master design, you must first master the fundamentals.

Một ví dụ điển hình

Hãy bắt đầu với một ví dụ đơn giản: tạo và thiết kế một nút bấm (button)

Thông thường các component và style được để chung vào một file: Button và ButtonStyles. Đó là vì cả 2 đều có chung trong một mục đích: tạo tính thẩm mĩ. tuy nhiên trong bài viết này, tôi sẽ chia code thành nhiều phần khác nhau cho dễ “tiêu hóa”

Dưới đây là button component:

...

function Button(props) {
  return (
    <input
      type="button"
      className={css(styles.button)}
      value={props.text}
    />
  );
}

 

Không có gì cao siêu cả – chỉ là một stateless React component, bởi Aphrodite thật sự phát huy được năng lực của mình khi trong property className . Lúc đó function css sẽ chuyển đổi styles object vào trong CSS. styles vốn được tạo ra nhờ vào function StyleSheet.create({ ... }) thuộc Aphrodite. Bạn có thể thấy kết quả từ StyleSheet.create({ ... }) nhờ vào Aphrodite playground.    

Dưới đây là stylesheet của nút bấm:

...

const gradient = 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)';

const styles = StyleSheet.create({
  button: {
    background: gradient,
    borderRadius: '3px',
    border: 0,
    color: 'white',
    height: '48px',
    textTransform: 'uppercase',
    padding: '0 25px',
    boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .30)',
  },
});

Một trong những lợi thế của Aphrodite đó là sự chuyển đổi trực tiếp cũng như nó khá dễ học, không quá khó. Các property như border-radius sẽ trở thành borderRadius và value sẽ được biến thành string. Pseudo-selectors, media queries, và thông tin về font chữ đều có thể làm được. Ngoài ra, vendor prefixes cũng được tự động thêm vào.

Sau đây là kết quả:

Với ví dụ này, giờ chúng ta hãy suy nghĩ cách dùng Aphrodite để tạo nên một visual design system đơn giản, chỉ tập trung vào 2 yếu cơ bản là: typography và spacing.

Yếu tố cơ bản thứ nhất –  typography

Typography là một trong những ý tố cơ bản nhất của design. Thế nên, điều đầu tiên ta cần làm là xác định typography thông qua hằng số. Và vì không như Sass hoặc Less, hằng số của Aphrodite có thể đặt trong JavaScript hoặc JSON file.

Xác định hằng số của typography

Khi tạo ra hằng số, hãy đặt tên cho các variable theo chức năng của chúng. Thay vì đặt tên cho một font size của bạn là h2, hãy dùng displayLarge để miêu tả vai trò của nó. tương tự, với độ đậm nhạt của font, thay vì dùng 600, thì hãy ghi là semibold để ám chỉ hiệu ứng của nó.

export const fontSize = {
  // heading
  displayLarge: '32px',
  displayMedium: '26px',
  displaySmall: '20px',
  heading: '18px',
  subheading: '16px',

  // body
  body: '17px',
  caption: '15px',
};

export const fontWeight = {
  bold: 700,
  semibold: 600,
  normal: 400,
  light: 200,
};

export const tagMapping = {
  h1: 'displayLarge',
  h2: 'displayMedium',
  h3: 'displaySmall',
  h4: 'heading',
  h5: 'subheading',
};

export const lineHeight = {
  // heading
  displayLarge: '48px',
  displayMedium: '48px',
  displaySmall: '24px',
  heading: '24px',
  subheading: '24px',

  // body
  body: '24px',
  caption: '24px',
};

 

Rất là quan trọng trong việc xác định được chính xác value cho các variable như kích cỡ chữ hay độ đậm nhạt của nó. Đó là bởi vì nó ảnh hưởng đến tính mĩ thuật cũng như vertical rhythm trong thiết kế. Vertical rhythm là một thuật ngữ ám chỉ việc spacing thích hợp.

Để tìm hiểu thêm về vertical rhythm thì bạn hãy vào đọc bài viết sau: Why is Vertical Rhythm an Important Typography Practice?

Việc chọn độ cao cho dòng chữ cũng như kích thước của front đều có cách thực hiện riêng của nó. Chúng ta có thể sử dụng tỉ lệ trong số học để đưa ra các set cặp kích thước thích hợp và có tiềm năng. Khoảng một tuần trước, tôi đã có một bài viết rất chi tiết về vấn đề trên: Typography can make or break your design: a process for choosing type. Để chọn được front với kích cỡ vừa ý, bạn nên sử dụng Modular Scale. Với việc xác định độ cao cho dòng chữ thì vertical rhythm calculator là một lựa chọn tuyệt vời.

Xác định những thành phần làm nên một Headline

Sau khi đã chọn ra typography mà bạn thích nhất, bước tiếp theo sẽ là tạo nên một component để sử dụng chúng. Mục đích của component này là nhằm giữa sự nhất quán trong thiết kế cũng như cách áp dụng cho heading xuyên suốt codebase.

import React, { PropTypes } from 'react';
import { StyleSheet, css } from 'aphrodite/no-important';
import { tagMapping, fontSize, fontWeight, lineHeight } from '../styles/base/typography';

function Heading(props) {
  const { children, tag: Tag } = props;
  return <Tag className={css(styles[tagMapping[Tag]])}>{children}</Tag>;
}

export default Heading;

export const styles = StyleSheet.create({
  displayLarge: {
    fontSize: fontSize.displayLarge,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displayLarge,
  },
  displayMedium: {
    fontSize: fontSize.displayMedium,
    fontWeight: fontWeight.normal,
    lineHeight: lineHeight.displayLarge,
  },
  displaySmall: {
    fontSize: fontSize.displaySmall,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displaySmall,
  },
  heading: {
    fontSize: fontSize.heading,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.heading,
  },
  subheading: {
    fontSize: fontSize.subheading,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.subheading,
  },
});

 

Có thể thấy, component `Heading` là một stateless function với chức năng chấp nhận tag như là một phần của nó cũng như chuyển đổi tag đó với style tương thích. Điều này thực hiện được là nhờ vào việc chúng ta đã xác định tag mapping từ trước đó.

...
export const tagMapping = {
  h1: 'displayLarge',
  h2: 'displayMedium',
  h3: 'displaySmall',
  h4: 'heading',
  h5: 'subheading',
};

Ở phần cuối của component file, chúng ta sẽ xác định mục đích của styles object. Đây là lúc mà typography phát huy tác dụng của nó.

export const styles = StyleSheet.create({
  displayLarge: {
    fontSize: fontSize.displayLarge,
    fontWeight: fontWeight.bold,
    lineHeight: lineHeight.displayLarge,
  },
  
  ...
});

Như vậy, Heading component của ta sẽ trông như dưới đây

function Parent() {
  return (
    <Heading tag="h2">Hello World</Heading>
  );
}

Với phương pháp trên, ta sẽ hạn chế được sự xuất hiện variability trong hệ thống type. Nhờ đó mà tránh được cái bẫy của hàng trăm kích cỡ chữ khác nhau bởi ta đã tiêu chuẩn hóa heading cho cả data base. Hơn nữa, Heading component cũng có thể áp dụng được vào Text component dành cho body copy.

Yếu tố cơ bản thứ hai –  Spacing

Khi lựa chọn về spacing giữa các yếu tố, chúng ta có thể dựa vào số học. Nhờ vào spacingFactor , ta có thể tạo ra các set khoảng cách cho spacing đựa vào một điểm chung. Phương pháp trên bảo đảm tính nhất quán và logic trong spacing.

const spacingFactor = 8;
export const spacing = {
  space0: `${spacingFactor / 2}px`,  // 4
  space1: `${spacingFactor}px`,      // 8
  space2: `${spacingFactor * 2}px`,  // 16
  space3: `${spacingFactor * 3}px`,  // 24
  space4: `${spacingFactor * 4}px`,  // 32
  space5: `${spacingFactor * 5}px`,  // 40
  space6: `${spacingFactor * 6}px`,  // 48

  space8: `${spacingFactor * 8}px`,  // 64
  space9: `${spacingFactor * 9}px`,  // 72
  space13: `${spacingFactor * 13}px`, // 104
};

Có thể thấy, ví dụ ở trên đều sử dụng một tỉ lệ chung, từ dòng 1 đến 13. Tuy nhiên, bạn cứ việc mà thử nghiệm với các tỉ lệ khác nhau. Bởi thiết kế luôn đòi hỏi việc sử dụng các kích cỡ khác nhau dựa trên mục đích, người xem, và thiết bị mà họ nhắm tới. Sau đây là một ví dụ với khoảng cách spacing dựa trên tỉ lệ vàng với spacingFactor của 8.

Golden Ratio (1:1.618)
8.0 x (1.618 ^ 0) = 8.000
8.0 x (1.618 ^ 1) = 12.94
8.0 x (1.618 ^ 2) = 20.94
8.0 x (1.618 ^ 3) = 33.89
8.0 x (1.618 ^ 4) = 54.82
8.0 x (1.618 ^ 5) = 88.71

Dưới đây thể hiện spacing scale sẽ như thế nào trong code. Tôi cũng thêm vào helper function nhằm xử lí việc tính toán cũng như làm tròn kết quả.

const spacingFactor = 8;
export const spacing = {
  space0: `${computeGoldenRatio(spacingFactor, 0)}px`,  // 8
  space1: `${computeGoldenRatio(spacingFactor, 1)}px`,  // 13
  space2: `${computeGoldenRatio(spacingFactor, 2)}px`,  // 21
  space3: `${computeGoldenRatio(spacingFactor, 3)}px`,  // 34
  space4: `${computeGoldenRatio(spacingFactor, 4)}px`,  // 55
  space5: `${computeGoldenRatio(spacingFactor, 5)}px`,  // 89
};

function computeGoldenRatio(spacingFactor, exp) {
  return Math.round(spacingFactor * Math.pow(1.618, exp));
}

Sau khi đã xác định được spacing, chúng ta có thể áp dụng chúng vào thiết kế của mình.

Hãy thử thêm marginBottom vào Button component

import { spacing } from '../styles/base/spacing';

...

const styles = StyleSheet.create({
  button: {
    marginBottom: spacing.space4, // adding margin using spacing constant
    ...
  },
});

Có thể thấy rằng nó sẽ luôn hoạt động trong mọi trường hợp. Tuy nhiên, nếu ta muốn thay đổi marginBottom property của Button tùy vào vị trí của nó thì phải làm sao?

Một cách để thực hiện là ta chèn lên style cũ một style mới từ component parent. Tuy vậy, nó có thể khá rắc rối thế nên một phương thức khác tối ưu hơn là tạo ra Spacing component để quản lí những thay đổi trong spacing của các yếu tố trong database.

import React, { PropTypes } from 'react';
import { spacing } from '../../base/spacing';

function getSpacingSize(size) {
  return `space${size}`;
}

function Spacing(props) {
  return (
    <div style={{ marginBottom: spacing[getSpacingSize(props.size)] }}>
      {props.children}
    </div>
  );
}

export default Spacing;

Nhờ đó mà ta không phải lo về tỉ lệ cũng như setting của spacing giữa các component với nhau. Nói cách khác, các component “con” sẽ trở thành các thành phần độc lập và không yêu cầu ta phải biết về vị trí đặt nó ở đầu nhằm tương thính với các yếu tố khác.

Nguyên nhân là do các component như buttons, inputs, và cards luôn cần có sự khác nhau về kích cỡ cũng như spacing. Bởi button trong một bài viết sẽ có kích thước khác với button nằm ở navigation bar. Như vậy, nếu một component luôn cần có sự nhất quán trong các kích thước khác nhau thì là lẽ hiển nhiên khi ta chỉ cần tập trung quản lí chúng thay vì là các component.

Bạn hẳn cũng nhận ra, trong ví dụ trên chỉ sử dụng marginBottom. Đó là vì nếu ta xác định tất cả margin theo một hướng nhất định sẽ giúp tránh việc bị crash do không tương thích giữa các margin cũng như việc thiết kế được thuận tiện hơn. Bạn có thể tìm hiểu thêm thông qua bài viết của Harry Robert: Single-direction margin declarations.

Thêm nữa, bạn đã có thể sử dụng spacing mà bạn đã xác định trước đó như là padding.

import React, { PropTypes } from 'react';
import { StyleSheet, css } from 'aphrodite/no-important';
import { spacing } from '../../styles/base/spacing';

function Card(props) {
  return (
    <div className={css(styles.card)}>
      {props.children}
    </div>
  );
}

export default Card;

export const styles = StyleSheet.create({
  card: {
    padding: spacing.space4}, // using spacing constants as padding
    
    background: 'rgba(255, 255, 255, 1.0)',
    boxShadow: '0 3px 17px 2px rgba(0, 0, 0, .05)',
    borderRadius: '3px',
  },
});

Nhờ đó mà thiết kế của bạn sẽ trở nên nhất quán hơn.

Sau đây là thành quả của chúng ta

Giờ thì bạn đã hiểu thêm về CSS trong Javascript rồi đấy, hãy thử vọc và thí nghiệm đi. Áp dụng inline JavaScript styles vào project tiếp theo của bạn là một ý tưởng không tồi. Tôi tin rằng bạn sẽ cảm thấy khá tuyệt nếu có thể tự mình giải quyết được những vấn đề liên quan về styling và thẩm mĩ.

Nguồn: blog.topdev.vn via Medium

Tuyển dụng lập trình viên lương cao trên TopDev