ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

commit ad85c269493e355dcd7c4edf85512cdbb2581d84
parent af7d7e9c9fbc40579574378d4c5d1f0a8132cd3f
Author: Konstantin Tarkus <koistya@gmail.com>
Date:   Sun,  6 Dec 2015 19:33:12 +0300

Merge pull request #333 from koistya/css-modules

Integrate CSS Modules & isomorphic-style-loader
Diffstat:
MCHANGELOG.md | 2++
Mdocs/react-style-guide.md | 111+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackage.json | 2+-
Msrc/app.js | 1+
Msrc/components/App/App.js | 40++++++++++++++++++++++++++++++++++------
Msrc/components/ContactPage/ContactPage.js | 18+++++++++++-------
Msrc/components/ContactPage/ContactPage.scss | 6+++++-
Msrc/components/ContentPage/ContentPage.js | 14++++++--------
Msrc/components/ContentPage/ContentPage.scss | 6+++++-
Msrc/components/ErrorPage/ErrorPage.js | 14+++++++++-----
Msrc/components/Feedback/Feedback.js | 14+++++++-------
Msrc/components/Feedback/Feedback.scss | 16++++++++--------
Msrc/components/Footer/Footer.js | 39++++++++++++---------------------------
Msrc/components/Footer/Footer.scss | 23++++++++++++-----------
Msrc/components/Header/Header.js | 22+++++++++++-----------
Msrc/components/Header/Header.scss | 16++++++++--------
Msrc/components/Html/Html.js | 4++--
Msrc/components/Link/Link.js | 2+-
Msrc/components/LoginPage/LoginPage.js | 18+++++++++++-------
Msrc/components/LoginPage/LoginPage.scss | 6+++++-
Msrc/components/Navigation/Navigation.js | 22+++++++++++-----------
Msrc/components/Navigation/Navigation.scss | 20++++++++++++--------
Msrc/components/NotFoundPage/NotFoundPage.js | 14+++++++++-----
Msrc/components/RegisterPage/RegisterPage.js | 18+++++++++++-------
Msrc/components/RegisterPage/RegisterPage.scss | 6+++++-
Msrc/components/TextBox/TextBox.js | 12++++++------
Msrc/components/TextBox/TextBox.scss | 4++--
Dsrc/decorators/withContext.js | 43-------------------------------------------
Msrc/decorators/withStyles.js | 68+++++++-------------------------------------------------------------
Msrc/server.js | 2+-
Mtools/webpack.config.js | 16++++++++--------
31 files changed, 278 insertions(+), 321 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased][unreleased] +- Integrate [CSS Modules](https://github.com/css-modules/css-modules) and + [isomorphic-style-loader](https://github.com/kriasoft/isomorphic-style-loader) - Move `DOMUtils.js` to `src/core` folder; remove `src/utils` folder - Replace [cssnext](http://cssnext.io/) with [precss](https://github.com/jonathantneal/precss) - Update build automation scripts to use plain functions diff --git a/docs/react-style-guide.md b/docs/react-style-guide.md @@ -1,10 +1,13 @@ ## React Style Guide -### Folder Structure +### Core Principles - Place each component in a separate folder -- Avoid having shared resources between components (css, images etc.) -- Keep all components' folders in the same parent folder (avoid nesting) +- Avoid having shared resources between components (CSS, images etc.) +- Avoid deeply nested folder structures +- Prefer using class selectors in CSS (see [BEM](https://bem.info/)) +- Avoid nested CSS selectors (see [BEM](https://bem.info/)) +- Keep CSS simple and declarative, avoid loops, mixins etc. ##### File structure per component example: @@ -19,70 +22,60 @@ For more information google for [component-based UI development](https://google.com/search?q=component-based+ui+development). -### CSS Class Names - -Use [BEM](https://bem.info/) approach for naming CSS classes. See also [SUIT CSS](https://suitcss.github.io/) for inspiration. - -```less -// CSS -.ComponentName { } -.ComponentName--modifier { } -.ComponentName-elementName { } -.ComponentName-elementName--modifier { } -``` - ##### CSS styling example: ```jsx // JSX -<nav className="Navigation"> - <ul className="Navigation-items"> - <li className="Navigation-item Navigation-item--selected"> - <a className="Navigation-link" href="/products">Products</a> +<nav className={cx(s.root, this.props.className)}> + <ul className={s.items}> + <li className={cx(s.item, s.selected)}> + <a className={s.link} href="/products">Products</a> </li> - <li className="Navigation-item"> - <a className="Navigation-link" href="/services">Services</a> + <li className={s.item}> + <a className={s.link} href="/services">Services</a> </li> </ul> </nav> ``` -```less +```scss // CSS -@import '../variables.css'; - -.Navigation { - &-items { - margin: 0; - padding: 0; - list-style-type: none; - text-align: center; - } +@import '../variables.scss'; - &-item { - display: inline-block; - vertical-align: top; - } +.root { + width: 300px; +} - &-link { - display: block; - padding: 0 25px; - outline: 0; - border: 0; - color: @default-color; - text-decoration: none; - line-height: 25px; - transition: background-color .3s ease; - - &, - .Navigation-items:hover & { - background: var(--default-bg-color); - } +.items { + margin: 0; + padding: 0; + list-style-type: none; + text-align: center; +} - &--selected, - .Navigation-items:hover &:hover { - background: var(--active-bg-color); - } +.item { + display: inline-block; + vertical-align: top; +} + +.link { + display: block; + padding: 0 25px; + outline: 0; + border: 0; + color: $default-color; + text-decoration: none; + line-height: 25px; + transition: background-color .3s ease; + + &, + .items:hover & { + background: $default-bg-color; + } + + .selected, + .items:hover &:hover { + background: $active-bg-color; } } ``` @@ -96,9 +89,12 @@ Use [BEM](https://bem.info/) approach for naming CSS classes. See also [SUIT CSS ##### React component example: ```js -import './SampleComponent.css'; -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import cx from 'classnames'; +import s from './SampleComponent.css'; +import withStyles from '../../decorators/withStyles'; +@withStyles(s) class SampleComponent extends Component { static propTypes = { ... }; @@ -124,9 +120,12 @@ class SampleComponent extends Component { // ... } + handleClick = (event) => { ... }; + render() { return ( - <div className="SampleComponent"> + <div className={cx(s.root, this.props.className)} onClick={this.handleClick}> + ... </div> ); } diff --git a/package.json b/package.json @@ -42,6 +42,7 @@ "gaze": "^0.5.2", "git-repository": "^0.1.1", "glob": "^6.0.1", + "isomorphic-style-loader": "0.0.5", "jest-cli": "^0.7.1", "jscs": "^2.5.0", "lodash.merge": "^3.3.2", @@ -55,7 +56,6 @@ "react-transform-hmr": "^1.0.1", "redbox-react": "^1.1.1", "replace": "^0.3.0", - "style-loader": "^0.13.0", "url-loader": "^0.5.6", "webpack": "^1.12.9", "webpack-dev-middleware": "^1.4.0", diff --git a/src/app.js b/src/app.js @@ -10,6 +10,7 @@ import { addEventListener, removeEventListener } from './core/DOMUtils'; let cssContainer = document.getElementById('css'); const appContainer = document.getElementById('app'); const context = { + insertCss: styles => styles._insertCss(), onSetTitle: value => document.title = value, onSetMeta: (name, content) => { // Remove and create a new <meta /> tag in order to make it work diff --git a/src/components/App/App.js b/src/components/App/App.js @@ -1,22 +1,50 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import styles from './App.scss'; -import withContext from '../../decorators/withContext'; -import withStyles from '../../decorators/withStyles'; +import React, { Component, PropTypes } from 'react'; +import emptyFunction from 'fbjs/lib/emptyFunction'; +import s from './App.scss'; import Header from '../Header'; import Feedback from '../Feedback'; import Footer from '../Footer'; -@withContext -@withStyles(styles) class App extends Component { static propTypes = { + context: PropTypes.shape({ + insertCss: PropTypes.func, + onSetTitle: PropTypes.func, + onSetMeta: PropTypes.func, + onPageNotFound: PropTypes.func, + }), children: PropTypes.element.isRequired, error: PropTypes.object, }; + static childContextTypes = { + insertCss: PropTypes.func.isRequired, + onSetTitle: PropTypes.func.isRequired, + onSetMeta: PropTypes.func.isRequired, + onPageNotFound: PropTypes.func.isRequired, + }; + + getChildContext() { + const context = this.props.context; + return { + insertCss: context.insertCss || emptyFunction, + onSetTitle: context.onSetTitle || emptyFunction, + onSetMeta: context.onSetMeta || emptyFunction, + onPageNotFound: context.onPageNotFound || emptyFunction, + }; + } + + componentWillMount() { + this.removeCss = this.props.context.insertCss(s); + } + + componentWillUnmount() { + this.removeCss(); + } + render() { return !this.props.error ? ( <div> diff --git a/src/components/ContactPage/ContactPage.js b/src/components/ContactPage/ContactPage.js @@ -1,22 +1,26 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import styles from './ContactPage.scss'; +import React, { Component, PropTypes } from 'react'; +import s from './ContactPage.scss'; import withStyles from '../../decorators/withStyles'; -@withStyles(styles) +const title = 'Contact Us'; + +@withStyles(s) class ContactPage extends Component { static contextTypes = { onSetTitle: PropTypes.func.isRequired, }; - render() { - const title = 'Contact Us'; + componentWillMount() { this.context.onSetTitle(title); + } + + render() { return ( - <div className="ContactPage"> - <div className="ContactPage-container"> + <div className={s.root}> + <div className={s.container}> <h1>{title}</h1> <p>...</p> </div> diff --git a/src/components/ContactPage/ContactPage.scss b/src/components/ContactPage/ContactPage.scss @@ -2,7 +2,11 @@ @import '../variables.scss'; -.ContactPage-container { +.root { + +} + +.container { margin: 0 auto; padding: 0 0 40px; max-width: $max-content-width; diff --git a/src/components/ContentPage/ContentPage.js b/src/components/ContentPage/ContentPage.js @@ -1,10 +1,10 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import styles from './ContentPage.scss'; +import React, { Component, PropTypes } from 'react'; +import s from './ContentPage.scss'; import withStyles from '../../decorators/withStyles'; -@withStyles(styles) +@withStyles(s) class ContentPage extends Component { static propTypes = { @@ -20,11 +20,9 @@ class ContentPage extends Component { render() { this.context.onSetTitle(this.props.title); return ( - <div className="ContentPage"> - <div className="ContentPage-container"> - { - this.props.path === '/' ? null : <h1>{this.props.title}</h1> - } + <div className={s.root}> + <div className={s.container}> + {this.props.path === '/' ? null : <h1>{this.props.title}</h1>} <div dangerouslySetInnerHTML={{__html: this.props.content || ''}} /> </div> </div> diff --git a/src/components/ContentPage/ContentPage.scss b/src/components/ContentPage/ContentPage.scss @@ -2,7 +2,11 @@ @import '../variables.scss'; -.ContentPage-container { +.root { + +} + +.container { margin: 0 auto; padding: 0 0 40px; max-width: $max-content-width; diff --git a/src/components/ErrorPage/ErrorPage.js b/src/components/ErrorPage/ErrorPage.js @@ -1,10 +1,12 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import s from './ErrorPage.scss'; import withStyles from '../../decorators/withStyles'; -import styles from './ErrorPage.scss'; -@withStyles(styles) +const title = 'Error'; + +@withStyles(s) class ErrorPage extends Component { static contextTypes = { @@ -12,9 +14,11 @@ class ErrorPage extends Component { onPageNotFound: PropTypes.func.isRequired, }; - render() { - const title = 'Error'; + componentWillMount() { this.context.onSetTitle(title); + } + + render() { return ( <div> <h1>{title}</h1> diff --git a/src/components/Feedback/Feedback.js b/src/components/Feedback/Feedback.js @@ -1,19 +1,19 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React, { Component } from 'react'; -import styles from './Feedback.scss'; +import s from './Feedback.scss'; import withStyles from '../../decorators/withStyles'; -@withStyles(styles) +@withStyles(s) class Feedback extends Component { render() { return ( - <div className="Feedback"> - <div className="Feedback-container"> - <a className="Feedback-link" href="https://gitter.im/kriasoft/react-starter-kit">Ask a question</a> - <span className="Feedback-spacer">|</span> - <a className="Feedback-link" href="https://github.com/kriasoft/react-starter-kit/issues/new">Report an issue</a> + <div className={s.root}> + <div className={s.container}> + <a className={s.link} href="https://gitter.im/kriasoft/react-starter-kit">Ask a question</a> + <span className={s.spacer}>|</span> + <a className={s.link} href="https://github.com/kriasoft/react-starter-kit/issues/new">Report an issue</a> </div> </div> ); diff --git a/src/components/Feedback/Feedback.scss b/src/components/Feedback/Feedback.scss @@ -2,12 +2,12 @@ @import '../variables.scss'; -.Feedback { +.root { background: #f5f5f5; color: #333; } -.Feedback-container { +.container { margin: 0 auto; padding: 20px 8px; max-width: $max-content-width; @@ -15,19 +15,19 @@ font-size: 1.5em; /* ~24px */ } -.Feedback-link, -.Feedback-link:active, -.Feedback-link:hover, -.Feedback-link:visited { +.link, +.link:active, +.link:hover, +.link:visited { color: #333; text-decoration: none; } -.Feedback-link:hover { +.link:hover { text-decoration: underline; } -.Feedback-spacer { +.spacer { padding-right: 15px; padding-left: 15px; } diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js @@ -1,39 +1,24 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import styles from './Footer.scss'; -import withViewport from '../../decorators/withViewport'; +import React, { Component } from 'react'; +import s from './Footer.scss'; import withStyles from '../../decorators/withStyles'; import Link from '../Link'; -@withViewport -@withStyles(styles) +@withStyles(s) class Footer extends Component { - static propTypes = { - viewport: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }).isRequired, - }; - render() { - // This is just an example how one can render CSS - const { width, height } = this.props.viewport; - this.renderCss(`.Footer-viewport:after {content:' ${width}x${height}';}`); - return ( - <div className="Footer"> - <div className="Footer-container"> - <span className="Footer-text">© Your Company</span> - <span className="Footer-spacer">·</span> - <a className="Footer-link" href="/" onClick={Link.handleClick}>Home</a> - <span className="Footer-spacer">·</span> - <a className="Footer-link" href="/privacy" onClick={Link.handleClick}>Privacy</a> - <span className="Footer-spacer">·</span> - <a className="Footer-link" href="/not-found" onClick={Link.handleClick}>Not Found</a> - <span className="Footer-spacer"> | </span> - <span ref="viewport" className="Footer-viewport Footer-text Footer-text--muted">Viewport:</span> + <div className={s.root}> + <div className={s.container}> + <span className={s.text}>© Your Company</span> + <span className={s.spacer}>·</span> + <a className={s.link} href="/" onClick={Link.handleClick}>Home</a> + <span className={s.spacer}>·</span> + <a className={s.link} href="/privacy" onClick={Link.handleClick}>Privacy</a> + <span className={s.spacer}>·</span> + <a className={s.link} href="/not-found" onClick={Link.handleClick}>Not Found</a> </div> </div> ); diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss @@ -2,43 +2,44 @@ @import '../variables.scss'; -.Footer { +.root { background: #333; color: #fff; } -.Footer-container { +.container { margin: 0 auto; padding: 20px 15px; max-width: $max-content-width; text-align: center; } -.Footer-text { +.text { color: rgba(255, 255, 255, .5); } -.Footer-text--muted { +.textMuted { + composes: text; color: rgba(255, 255, 255, .3); } -.Footer-spacer { +.spacer { color: rgba(255, 255, 255, .3); } -.Footer-text, -.Footer-link { +.text, +.link { padding: 2px 5px; font-size: 1em; } -.Footer-link, -.Footer-link:active, -.Footer-link:visited { +.link, +.link:active, +.link:visited { color: rgba(255, 255, 255, .6); text-decoration: none; } -.Footer-link:hover { +.link:hover { color: rgba(255, 255, 255, 1); } diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js @@ -1,26 +1,26 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React, { Component } from 'react'; -import styles from './Header.scss'; +import s from './Header.scss'; import withStyles from '../../decorators/withStyles'; import Link from '../Link'; import Navigation from '../Navigation'; -@withStyles(styles) +@withStyles(s) class Header extends Component { render() { return ( - <div className="Header"> - <div className="Header-container"> - <a className="Header-brand" href="/" onClick={Link.handleClick}> - <img className="Header-brandImg" src={require('./logo-small.png')} width="38" height="38" alt="React" /> - <span className="Header-brandTxt">Your Company</span> + <div className={s.root}> + <div className={s.container}> + <Navigation className={s.nav} /> + <a className={s.brand} href="/" onClick={Link.handleClick}> + <img src={require('./logo-small.png')} width="38" height="38" alt="React" /> + <span className={s.brandTxt}>Your Company</span> </a> - <Navigation className="Header-nav" /> - <div className="Header-banner"> - <h1 className="Header-bannerTitle">React</h1> - <p className="Header-bannerDesc">Complex web apps made easy</p> + <div className={s.banner}> + <h1 className={s.bannerTitle}>React</h1> + <p className={s.bannerDesc}>Complex web apps made easy</p> </div> </div> </div> diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss @@ -4,37 +4,37 @@ $brand-color: #61dafb; -.Header { +.root { background: #373277; color: #fff; } -.Header-container { +.container { margin: 0 auto; padding: 20px 0; max-width: $max-content-width; } -.Header-brand { +.brand { color: color($brand-color lightness(+10%)); text-decoration: none; font-size: 1.75em; /* ~28px */ } -.Header-brandTxt { +.brandTxt { margin-left: 10px; } -.Header-nav { +.nav { float: right; margin-top: 6px; } -.Header-banner { +.banner { text-align: center; } -.Header-bannerTitle { +.bannerTitle { margin: 0; padding: 10px; font-weight: normal; @@ -42,7 +42,7 @@ $brand-color: #61dafb; line-height: 1em; } -.Header-bannerDesc { +.bannerDesc { padding: 0; color: rgba(255, 255, 255, .5); font-size: 1.25em; diff --git a/src/components/Html/Html.js b/src/components/Html/Html.js @@ -1,7 +1,7 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React, { Component, PropTypes } from 'react'; -import { googleAnalyticsId } from '../../config'; +import config from '../../config'; class Html extends Component { @@ -25,7 +25,7 @@ class Html extends Component { `e=o.createElement(i);r=o.getElementsByTagName(i)[0];` + `e.src='https://www.google-analytics.com/analytics.js';` + `r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));` + - `ga('create','${googleAnalyticsId}','auto');ga('send','pageview');`, + `ga('create','${config.googleAnalyticsId}','auto');ga('send','pageview');`, }); } diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js @@ -1,6 +1,6 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; +import React, { Component, PropTypes } from 'react'; import Location from '../../core/Location'; function isLeftClickEvent(event) { diff --git a/src/components/LoginPage/LoginPage.js b/src/components/LoginPage/LoginPage.js @@ -1,22 +1,26 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import styles from './LoginPage.scss'; +import React, { Component, PropTypes } from 'react'; +import s from './LoginPage.scss'; import withStyles from '../../decorators/withStyles'; -@withStyles(styles) +const title = 'Log In'; + +@withStyles(s) class LoginPage extends Component { static contextTypes = { onSetTitle: PropTypes.func.isRequired, }; - render() { - const title = 'Log In'; + componentWillMount() { this.context.onSetTitle(title); + } + + render() { return ( - <div className="LoginPage"> - <div className="LoginPage-container"> + <div className={s.root}> + <div className={s.container}> <h1>{title}</h1> <p>...</p> </div> diff --git a/src/components/LoginPage/LoginPage.scss b/src/components/LoginPage/LoginPage.scss @@ -2,7 +2,11 @@ @import '../variables.scss'; -.LoginPage-container { +.root { + +} + +.container { margin: 0 auto; padding: 0 0 40px; max-width: $max-content-width; diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js @@ -1,12 +1,12 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; -import classNames from 'classnames'; -import styles from './Navigation.scss'; +import React, { Component, PropTypes } from 'react'; +import cx from 'classnames'; +import s from './Navigation.scss'; import withStyles from '../../decorators/withStyles'; import Link from '../Link'; -@withStyles(styles) +@withStyles(s) class Navigation extends Component { static propTypes = { @@ -15,13 +15,13 @@ class Navigation extends Component { render() { return ( - <div className={classNames(this.props.className, 'Navigation')} role="navigation"> - <a className="Navigation-link" href="/about" onClick={Link.handleClick}>About</a> - <a className="Navigation-link" href="/contact" onClick={Link.handleClick}>Contact</a> - <span className="Navigation-spacer"> | </span> - <a className="Navigation-link" href="/login" onClick={Link.handleClick}>Log in</a> - <span className="Navigation-spacer">or</span> - <a className="Navigation-link Navigation-link--highlight" href="/register" onClick={Link.handleClick}>Sign up</a> + <div className={cx(s.root, this.props.className)} role="navigation"> + <a className={s.link} href="/about" onClick={Link.handleClick}>About</a> + <a className={s.link} href="/contact" onClick={Link.handleClick}>Contact</a> + <span className={s.spacer}> | </span> + <a className={s.link} href="/login" onClick={Link.handleClick}>Log in</a> + <span className={s.spacer}>or</span> + <a className={cx(s.link, s.highlight)} href="/register" onClick={Link.handleClick}>Sign up</a> </div> ); } diff --git a/src/components/Navigation/Navigation.scss b/src/components/Navigation/Navigation.scss @@ -1,23 +1,27 @@ /* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -.Navigation-link { +.root { + +} + +.link { display: inline-block; padding: 3px 8px; text-decoration: none; font-size: 1.125em; /* ~18px */ } -.Navigation-link, -.Navigation-link:active, -.Navigation-link:visited { +.link, +.link:active, +.link:visited { color: rgba(255, 255, 255, .6); } -.Navigation-link:hover { +.link:hover { color: rgba(255, 255, 255, 1); } -.Navigation-link--highlight { +.highlight { margin-right: 8px; margin-left: 8px; border-radius: 3px; @@ -25,10 +29,10 @@ color: #fff; } -.Navigation-link--highlight:hover { +.highlight:hover { background: rgba(0, 0, 0, .3); } -.Navigation-spacer { +.spacer { color: rgba(255, 255, 255, .3); } diff --git a/src/components/NotFoundPage/NotFoundPage.js b/src/components/NotFoundPage/NotFoundPage.js @@ -1,10 +1,12 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import s from './NotFoundPage.scss'; import withStyles from '../../decorators/withStyles'; -import styles from './NotFoundPage.scss'; -@withStyles(styles) +const title = 'Page Not Found'; + +@withStyles(s) class NotFoundPage extends Component { static contextTypes = { @@ -12,10 +14,12 @@ class NotFoundPage extends Component { onPageNotFound: PropTypes.func.isRequired, }; - render() { - const title = 'Page Not Found'; + componentWillMount() { this.context.onSetTitle(title); this.context.onPageNotFound(); + } + + render() { return ( <div> <h1>{title}</h1> diff --git a/src/components/RegisterPage/RegisterPage.js b/src/components/RegisterPage/RegisterPage.js @@ -1,22 +1,26 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import s from './RegisterPage.scss'; import withStyles from '../../decorators/withStyles'; -import styles from './RegisterPage.scss'; -@withStyles(styles) +const title = 'New User Registration'; + +@withStyles(s) class RegisterPage extends Component { static contextTypes = { onSetTitle: PropTypes.func.isRequired, }; - render() { - const title = 'New User Registration'; + componentWillMount() { this.context.onSetTitle(title); + } + + render() { return ( - <div className="RegisterPage"> - <div className="RegisterPage-container"> + <div className={s.root}> + <div className={s.container}> <h1>{title}</h1> <p>...</p> </div> diff --git a/src/components/RegisterPage/RegisterPage.scss b/src/components/RegisterPage/RegisterPage.scss @@ -2,7 +2,11 @@ @import '../variables.scss'; -.RegisterPage-container { +.root { + +} + +.container { margin: 0 auto; padding: 0 0 40px; max-width: $max-content-width; diff --git a/src/components/TextBox/TextBox.js b/src/components/TextBox/TextBox.js @@ -1,10 +1,10 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import s from './TextBox.scss'; import withStyles from '../../decorators/withStyles'; -import styles from './TextBox.scss'; -@withStyles(styles) +@withStyles(s) class TextBox extends Component { static propTypes = { @@ -17,10 +17,10 @@ class TextBox extends Component { render() { return ( - <div className="TextBox"> + <div className={s.root}> {this.props.maxLines > 1 ? - <textarea {...this.props} className="TextBox-input" ref="input" key="input" rows={this.props.maxLines} /> : - <input {...this.props} className="TextBox-input" ref="input" key="input" />} + <textarea {...this.props} className={s.input} ref="input" key="input" rows={this.props.maxLines} /> : + <input {...this.props} className={s.input} ref="input" key="input" />} </div> ); } diff --git a/src/components/TextBox/TextBox.scss b/src/components/TextBox/TextBox.scss @@ -1,4 +1,4 @@ /* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -.TextBox { } -.TextBox-input { } +.root { } +.input { } diff --git a/src/decorators/withContext.js b/src/decorators/withContext.js @@ -1,43 +0,0 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ - -import React, { PropTypes, Component } from 'react'; // eslint-disable-line no-unused-vars -import emptyFunction from 'fbjs/lib/emptyFunction'; - -function withContext(ComposedComponent) { - return class WithContext extends Component { - - static propTypes = { - context: PropTypes.shape({ - onInsertCss: PropTypes.func, - onSetTitle: PropTypes.func, - onSetMeta: PropTypes.func, - onPageNotFound: PropTypes.func, - }), - }; - - static childContextTypes = { - onInsertCss: PropTypes.func.isRequired, - onSetTitle: PropTypes.func.isRequired, - onSetMeta: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, - }; - - getChildContext() { - const context = this.props.context; - return { - onInsertCss: context.onInsertCss || emptyFunction, - onSetTitle: context.onSetTitle || emptyFunction, - onSetMeta: context.onSetMeta || emptyFunction, - onPageNotFound: context.onPageNotFound || emptyFunction, - }; - } - - render() { - const { context, ...other } = this.props; // eslint-disable-line no-unused-vars - return <ComposedComponent {...other} />; - } - - }; -} - -export default withContext; diff --git a/src/decorators/withStyles.js b/src/decorators/withStyles.js @@ -1,78 +1,24 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React, { PropTypes, Component } from 'react'; // eslint-disable-line no-unused-vars -import invariant from 'fbjs/lib/invariant'; -import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; - -let count = 0; - -function withStyles(styles) { - return (ComposedComponent) => class WithStyles extends Component { +import React, { Component, PropTypes } from 'react'; +function withStyles(...styles) { + return (BaseComponent) => class StyledComponent extends Component { static contextTypes = { - onInsertCss: PropTypes.func, + insertCss: PropTypes.func.isRequired, }; - constructor() { - super(); - this.refCount = 0; - ComposedComponent.prototype.renderCss = function render(css) { - let style; - if (canUseDOM) { - style = this.styleId && document.getElementById(this.styleId); - if (style) { - if ('textContent' in style) { - style.textContent = css; - } else { - style.styleSheet.cssText = css; - } - } else { - this.styleId = `dynamic-css-${count++}`; - style = document.createElement('style'); - style.setAttribute('id', this.styleId); - style.setAttribute('type', 'text/css'); - - if ('textContent' in style) { - style.textContent = css; - } else { - style.styleSheet.cssText = css; - } - - document.getElementsByTagName('head')[0].appendChild(style); - this.refCount++; - } - } else { - this.context.onInsertCss(css); - } - }.bind(this); - } - componentWillMount() { - if (canUseDOM) { - invariant(styles.use, `The style-loader must be configured with reference-counted API.`); - styles.use(); - } else { - this.context.onInsertCss(styles.toString()); - } + this.removeCss = this.context.insertCss.apply(undefined, styles); } componentWillUnmount() { - styles.unuse(); - if (this.styleId) { - this.refCount--; - if (this.refCount < 1) { - const style = document.getElementById(this.styleId); - if (style) { - style.parentNode.removeChild(style); - } - } - } + this.removeCss(); } render() { - return <ComposedComponent {...this.props} />; + return <BaseComponent {...this.props} />; } - }; } diff --git a/src/server.js b/src/server.js @@ -32,7 +32,7 @@ server.get('*', async (req, res, next) => { const data = { title: '', description: '', css: '', body: '', entry: assets.app.js }; const css = []; const context = { - onInsertCss: value => css.push(value), + insertCss: styles => css.push(styles._getCss()), onSetTitle: value => data.title = value, onSetMeta: (key, value) => data[key] = value, onPageNotFound: () => statusCode = 404, diff --git a/tools/webpack.config.js b/tools/webpack.config.js @@ -74,6 +74,14 @@ const config = { ], loader: 'babel-loader', }, { + test: /\.scss$/, + loaders: [ + 'isomorphic-style-loader', + 'css-loader?' + (DEBUG ? 'sourceMap&' : 'minimize&') + + 'modules&localIdentName=[name]_[local]_[hash:base64:3]', + 'postcss-loader', + ], + }, { test: /\.json$/, loader: 'json-loader', }, { @@ -85,9 +93,6 @@ const config = { }, { test: /\.(eot|ttf|wav|mp3)$/, loader: 'file-loader', - }, { - test: /\.scss$/, - loader: 'style-loader/useable!css-loader!postcss-loader', }, ], }, @@ -203,9 +208,4 @@ const serverConfig = merge({}, config, { ], }); -// Remove `style-loader` from the server-side bundle configuration -serverConfig.module.loaders - .filter(x => x.loader.startsWith('style-loader/useable!')) - .forEach(x => x.loader = x.loader.substr(21)); - export default [appConfig, serverConfig];