Skip to content

Commit ebb9bb0

Browse files
committed
Client: Add LESS to CSS handling options
1 parent f391274 commit ebb9bb0

21 files changed

+265
-66
lines changed

README.md

+33-19
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,17 @@
4646

4747
* Overall simplicity. For any starter project or boilerplate, the probability of having bugs/issues down the track increases along with the amount of code. It is shown by the code size badge and can be checked for any GitHub repository using the link: `https://img.shields.io/github/languages/code-size/<user-name>/<repo-name>`. For Crisp React, the React client and the Express backend each contribute ~50% of the codebase.<br/>The code size of other starter projects was a main motivation to develop this solution. The other projects were enjoyable for learning purposes however the amount of code was percieved to be excessive for use in production.
4848

49-
* CSS Handling. The following three CSS handling approaches can be used:
49+
* CSS Handling. The following four CSS handling approaches can be used:
5050

51-
1. Plain CSS: Simple and performant with the burden to track name collisions created by multiple components using rulesets with similarly named class selectors.
51+
1. Plain CSS: Simple and performant with the burden to track name collisions created by multiple components using rules with similarly named class selectors.
5252

53-
2. CSS Modules: Performant with convenience of name collisions resolved automatically and drawback of possible rule repetition leading to an increase in size of the resulting stylesheet.
53+
2. CSS Modules: Performant with convenience of name collisions resolved automatically and drawback of possible rule repetition leading to an increase in size of the resulting stylesheet. Supports rule reuse via composition.
5454

55-
3. CSS-in-JS: Developer’s convenience with more flexible CSS adjusted if needed during its construction at run-time. The application logic that drives CSS adjustments (for example, driven by the shape of data received) can be sophisticated and challenging to be expressed via CSS created at build time. Overall this approach translates into self-contained and self-adjusting components, development speed and better codebase maintainability (especially when multiple developers or teams are involved). The advantages come at the price of possible rule repetition along with a performance penalty caused by dependency on script bundles download, parsing and execution.
55+
3. LESS: Like Plain CSS but with extended CSS syntax and rich additional functionality.
5656

57-
The solution allows to use each approach as a sole CSS handling technique or combine it with any or both of the remaining two approaches - with no configuration effort. More details are available under the [CSS Handling](#css-handling) heading.
57+
4. CSS-in-JS: Developer’s convenience with more flexible CSS adjusted if needed during its construction at run-time. The application logic that drives CSS adjustments (for example, driven by the shape of data received) can be sophisticated and challenging to be expressed via CSS created at build time. Overall this approach translates into self-contained and self-adjusting components, development speed and better codebase maintainability (especially when multiple developers or teams are involved). The advantages come at the price of possible rule repetition along with a performance penalty caused by dependency on script bundles download, parsing and execution. There are other, more subtle drawbacks mentioned later.
58+
59+
The solution allows to use each approach as a sole CSS handling technique or combine it with any or all of the remaining three approaches - with no configuration effort. More details are available under the [CSS Handling](#css-handling) heading.
5860

5961
* API. The backend communicates with a cloud service on behalf of clients and makes data available via an API endpoint. It's consumed by the clients. The Name Lookup API is used as a sample:
6062
![API Screenshot](docs/screenshots/api.png)
@@ -92,6 +94,10 @@ It can be conveniently executed from the Cloud Shell session opened during the d
9294
- [Backend Usage Scenarios](#backend-usage-scenarios)
9395
- [SSR](#ssr)
9496
- [CSS Handling](#css-handling)
97+
- [Plain CSS](#plain-css)
98+
- [CSS Modules](#css-modules)
99+
- [LESS](#less)
100+
- [CSS-in-JS](#css-in-js)
95101
- [Containerisation](#containerisation)
96102
- [Using Docker](#using-docker)
97103
- [Using Heroku](#using-heroku)
@@ -367,28 +373,36 @@ SSR is enabled for production builds. In order to turn it off rename the `postbu
367373
### Turning On and Off on the SPA Level
368374
By default SSR is enabled for the [`first`](https://github.com/winwiz1/crisp-react/blob/master/client/src/entrypoints/first.tsx) SPA and disabled for the [`second`](https://github.com/winwiz1/crisp-react/blob/master/client/src/entrypoints/second.tsx) SPA. To toggle this setting follow the instructions provided in the respective file comments.
369375
## CSS Handling
370-
1. ### Plain CSS
371-
To take this approach create a file with the `.css` extension and a name that doesn’t end with `-style`, for example `abc.css`. Place it anywhere under the [`src/`](https://github.com/winwiz1/crisp-react/blob/master/client/src) subdirectory, for instance under `src/css/` or next to your component under `src/components/`.
376+
### Plain CSS
377+
To take this approach create a file with the `.css` extension and a name that doesn’t end with `.module.css`. Place it anywhere under the [`src/`](https://github.com/winwiz1/crisp-react/blob/master/client/src) subdirectory, for instance under `src/css/` or next to your component under `src/components/`.
372378

373-
Multiple files can be created. At the build time all [imported](https://github.com/winwiz1/crisp-react/blob/master/client/src/components/ComponentB.tsx#L9) `.css` files will be combined into a single stylesheet with class selectors left intact. The stylesheet, created under `client/dist/`, will be downloaded and cached by a browser.
379+
Multiple files can be created. At the build time all [imported](https://github.com/winwiz1/crisp-react/blob/master/client/src/entrypoints/first.tsx#L26) `.css` files will be combined into a single stylesheet with class selectors left intact. The stylesheet, created under `client/dist/`, will be downloaded and cached by a browser.
374380

375-
The solution uses this approach for two purposes:
376-
- To put frequently used CSS rules on the global scope and share it among components to avoid duplication caused by each component having its own similar rule.
381+
The solution uses this approach for two purposes:
382+
- To put frequently used CSS rules on the global scope and share it among components to avoid duplication caused by each component having its own similar rule.
377383
Suppose you are creating an accessible webapp so each component has lots of `<span>` elements with various screen reader prompts and the same class name: `class=’sr-only’`. The relevant CSS rule in a plain CSS file like [this](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/app.css) one can be easily shared.
378384

379-
- To modify styling of an existing or third party library component that expects predefined class selectors. For example, like in [that](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/react-day-picker.css) file.
380-
2. ### CSS Modules.
381-
The only difference from plain CSS files is the file name - it must end with `-style`. For example, [`base-component-style.css`](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/base-component-style.css). At the build time all such files will be combined into the single stylesheet mentioned above, but with class selectors mangled to ensure uniqueness.
385+
- To modify styling of an existing or third party library component that expects predefined class selectors. For example, like in [that](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/react-day-picker.css) file.
386+
### CSS Modules
387+
The filename of a module must end with `.module.css`. At the build time all such files will be combined into the single stylesheet mentioned above with class selectors mangled to ensure uniqueness.
388+
389+
To embed class selectors into JSX code the mangled names are required. These names are not available until a build is completed. Therefore [this](https://github.com/winwiz1/crisp-react/blob/master/client/src/components/BaseComponent.tsx#L16-L20) helper object is used to map unmangled class selectors (e.g. `.left_component`) into the mangled ones (returned by `cssStyle.left`).
390+
391+
In order to improve loading performance, the solution [uses](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/base-component.module.css) this approach for the CSS that determines the overall layout of a page (or a major component) while leaving more subtle/detailed and numerous CSS rules for the CSS-in-JS library.
392+
393+
> Letting CSS-in-JS handle a rule that determines page's layout could introduce unwanted CLS (Cumulative Layout Shift). The CLS will likely be delayed due to the typical CSS-in-JS delay and visible to users as a sudden and unpleasant jerk movement.
394+
395+
### LESS
396+
LESS is like plain CSS but on steroids. Lots of extra [features](https://lesscss.org/features/) are available. The solution [uses](https://github.com/winwiz1/crisp-react/blob/master/client/src/css/app.less) one such feature: 'parent selector' denoted by ampersand.
382397

383-
To embed class selectors into JSX code the mangled names are required. These names are not available until a build is completed. Therefore [this](https://github.com/winwiz1/crisp-react/blob/master/client/src/components/BaseComponent.tsx#L16-L20) helper object is used to map unmangled class selectors (e.g. `.left_component`) into the mangled ones (returned by `cssStyle.left`).
398+
Multiple `.less` files can be created. At the build time all [imported](https://github.com/winwiz1/crisp-react/blob/master/client/src/entrypoints/first.tsx#L27) `.less` files will be combined into the single stylesheet along with the rules produced by plain CSS files and CSS modules.
384399

385-
In order to improve loading performance the solution uses this approach for the CSS that determines the overall layout of a page (or a major component) while leaving more subtle/detailed and numerous CSS rules for the CSS-in-JS library.
386-
3. ### CSS-in-JS
387-
The `@emotion/react` package of the Emotion library is used. To take this approach follow the [documentation](https://emotion.sh/docs/css-prop#object-styles) and search for the `css({` pattern in `.tsx` files located under `src/components/`.
400+
### CSS-in-JS
401+
The `@emotion/react` package of the Emotion library is used. To take this approach follow the [documentation](https://emotion.sh/docs/css-prop#object-styles) and search for the `css({` pattern in `.tsx` files located under `src/components/`.
388402

389-
The class selectors are generated at run-time in browser's memory and combined into a separate stylesheet. The stylesheet is then programmatically inserted into the `<head>` element of the DOM tree.
403+
The class selectors are generated at run-time in browser's memory and combined into a separate stylesheet. The stylesheet is then programmatically inserted into the `<head>` element of the DOM tree.
390404

391-
The insertion is delayed by script bundles processing and execution. Another drawback of this approach is loss of JSX code portability. Also the Emotion library is not Atomic (see the [review](https://github.com/andreipfeiffer/css-in-js#11-atomic-css)) so rule duplication is an issue.
405+
The insertion is delayed by script bundles processing and execution. Other drawbacks of this approach include possible CLS and loss of JSX code portability. Also the Emotion library is not Atomic (see the [review](https://github.com/andreipfeiffer/css-in-js#11-atomic-css)) so rule duplication is an issue.
392406
## Containerisation
393407
Assuming the deployment demo in the [Project Highlights](#project-highlights) section has been completed, a container has already been built in the cloud and deployed to Google Cloud Run. In this section we will build the container locally and expect it to run in two other deployments (in the local environment facilitated by Docker and the cloud one provided by Heroku) without any further adjustments.
394408
### Using Docker

client/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
"jest": "^27.0.6",
7777
"jest-css-modules": "^2.1.0",
7878
"jsdom": "^17.0.0",
79+
"less": "^4.1.1",
80+
"less-loader": "^10.0.1",
7981
"mini-css-extract-plugin": "^2.2.0",
8082
"mkdirp": "^1.0.4",
8183
"null-loader": "^4.0.1",

client/src/components/BaseComponent.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* other.
1212
**/
1313
import * as React from "react";
14-
import styles from "../css/base-component-style.css";
14+
import styles from "../css/base-component.module.css";
1515

1616
const cssStyle: Record<string, string> = {
1717
container: styles["component_container"],

client/src/components/ComponentB.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as React from "react";
66
import { Header, Container, Menu } from "semantic-ui-react";
77
import { BaseComponent } from "./BaseComponent";
88
import * as SPAs from "../../config/spa.config";
9-
import "../css/app.css"; // import plain CSS file once in any source file.
109

1110
const Description: React.FC = _props => {
1211
return (
@@ -43,7 +42,7 @@ const Description: React.FC = _props => {
4342
const Navigation: React.FC = _props => {
4443
return (
4544
<nav>
46-
<Menu vertical compact borderless>
45+
<Menu vertical compact borderless className="nav_menu">
4746
<Menu.Item>
4847
<Menu.Header>Go back to</Menu.Header>
4948
<Menu.Menu>

client/src/components/Navigation.tsx

+4-15
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,24 @@
33
* responsible for rendering the menu.
44
*/
55
/** @jsx jsx */
6-
import { jsx, css } from "@emotion/react";
6+
import { jsx } from "@emotion/react";
77
import * as React from "react";
88
import { NavLink } from "react-router-dom";
99
import { Menu } from "semantic-ui-react";
1010
import * as SPAs from "../../config/spa.config";
11-
import styles from "../css/navigation-style.css";
11+
import styles from "../css/navigation.module.css";
1212

1313
const cssStyle: Record<string, string> = {
1414
menu: styles["menu"],
1515
};
1616

17-
const cssMenu = css({
18-
"div.header": {
19-
fontSize: "1.1em !important"
20-
},
21-
"& a": {
22-
fontSize: "0.9em !important",
23-
paddingTop: "1em !important",
24-
paddingBottom: "1em !important",
25-
},
26-
});
27-
2817
export const Navigation: React.FC = _props => {
2918
return (
3019
<nav css={cssStyle.menu}>
31-
<Menu vertical css={cssMenu}>
20+
<Menu vertical className="nav_menu">
3221
<Menu.Item>
3322
<Menu.Header>First SPA</Menu.Header>
34-
<Menu.Menu css={cssMenu}>
23+
<Menu.Menu>
3524
<Menu.Item header as={NavLink} exact to="/" children="Overview" />
3625
<Menu.Item header as={NavLink} to="/a" children="ComponentA" />
3726
<Menu.Item header as={NavLink} to="/lighthouse" children="Lighthouse" />

client/src/components/Overview.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import {
1717
import { Navigation } from "./Navigation";
1818
import { BaseComponent } from "./BaseComponent";
1919
import { getAnchorCSS } from "../css/common-styles";
20+
import styles from "../css/overview.module.css";
21+
22+
const cssStyle: Record<string, string> = {
23+
msg: styles["msg"],
24+
};
2025

2126
const cssIcon = css({
2227
float: "left",
@@ -30,7 +35,7 @@ const cssMessage = css(
3035
const Description: React.FC = _props => {
3136
return (
3237
<Container text textAlign="justified">
33-
<Message css={cssMessage}>
38+
<Message css={cssMessage} className={cssStyle.msg}>
3439
<Icon css={cssIcon}
3540
name="info circle"
3641
color="blue"

client/src/css/app.css

+6
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ that remains unmangled.
99
height: 1px;
1010
overflow: hidden;
1111
}
12+
13+
.welcome {
14+
text-align: center;
15+
margin-top: 2em;
16+
margin-bottom: 3em;
17+
}

client/src/css/app.less

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.nav_menu {
2+
& div.header {
3+
font-size: 1.1em !important;
4+
}
5+
& a.item {
6+
font-size: 0.9em !important;
7+
padding-top: 1em !important;
8+
padding-bottom: 1em !important;
9+
}
10+
}

client/src/css/common-styles.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
type CssInJs = Record<string,Record<string,string|number>>;
22

3+
/*
4+
This function helps to avoid code repetition in .tsx files but
5+
it won't eliminate rule duplication in CSS stylesheet unless an
6+
Atomic CSS-in-JS library is used (see README).
7+
8+
It's a typical CSS-in-JS drawback and in this project it was
9+
deemed to be an acceptable price to pay for CSS-in-JS advantages.
10+
*/
311
export function getAnchorCSS(): CssInJs {
412
return {
513
"& a": {

client/src/css/css-modules.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare module "*.css";
2+
declare module "*.less";

client/src/css/navigation-style.css

-3
This file was deleted.

client/src/css/navigation.module.css

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
This file will be treated as CSS Module so simple
3+
names (without dashes or underscores) can be used
4+
in class selectors. No name collisions with third
5+
party libraries or other components.
6+
*/
7+
.menu {
8+
margin-right: 1em;
9+
}

client/src/css/overview.module.css

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
This file will be treated as CSS Module so simple
3+
names (without dashes or underscores) can be used
4+
in class selectors. No name collisions with third
5+
party libraries or other components.
6+
*/
7+
.msg {
8+
margin-top: 0 !important;
9+
}

client/src/css/react-day-picker.css

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ following two steps would be required:
1010
@import "react-day-picker/lib/style.css";
1111
*/
1212

13-
/* Optionally use this CSS to adjust style:
13+
14+
15+
/* Optionally use this CSS to adjust style:
16+
1417
.DayPicker-Weekday {
1518
color: dimgrey !important;
1619
}

client/src/css/style.d.ts

-1
This file was deleted.

client/src/entrypoints/first.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ import { ErrorBoundary } from "../components/ErrorBoundary";
2323
import { renderToString } from "react-dom/server"; // used for SSR
2424
import * as SPAs from "../../config/spa.config";
2525
import { isServer, getHistory } from "../utils/postprocess/misc";
26+
import "../css/app.css";
27+
import "../css/app.less";
2628

2729
const First: React.FC = _props => {
2830
return (
2931
<>
3032
<Router history={getHistory()}>
3133
<ErrorBoundary>
3234
<Helmet title={SPAs.appTitle} />
33-
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}>
35+
<div className="welcome">
3436
<h2>Welcome to {SPAs.appTitle}</h2>
3537
</div>
3638
<Switch>

client/src/entrypoints/second.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { ErrorBoundary } from "../components/ErrorBoundary";
1919
// import { renderToString } from "react-dom/server";
2020
import * as SPAs from "../../config/spa.config";
2121
import { isServer } from "../utils/postprocess/misc";
22+
import "../css/app.css"; // import plain CSS file once in any source file.
23+
import "../css/app.less"; // import LESS file once in any source file.
2224

2325
const Second: React.FC = _props => {
2426
return (
2527
<>
2628
<ErrorBoundary>
2729
<Helmet title={SPAs.appTitle} />
28-
<div style={{ textAlign: "center", marginTop: "2rem", marginBottom: "3rem" }}>
30+
<div className="welcome">
2931
<h2>Welcome to {SPAs.appTitle}</h2>
3032
</div>
3133
<ComponentB />

0 commit comments

Comments
 (0)