japgolly / scalajs-react

Facebook's React on Scala.JS
https://japgolly.github.io/scalajs-react/
Apache License 2.0
1.64k stars 231 forks source link

CSS with Confidence! #77

Closed japgolly closed 9 years ago

japgolly commented 9 years ago

Server code, client JS and HTML are all compiler-verified in Scala, leaving CSS is the last part of my webapp mega-project where I feel unsafe that it always works and is maintainable.

What are the problems with typical CSS? See https://speakerdeck.com/vjeux/react-css-in-js

I'd like to solve this problem in a way such that _any_ questions or concerns that arise in one's project regarding CSS, can be assuaged by the compiler. Examples are:

Free-form thoughts thus far:

inline-style or classname-and-css shouldn't matter. impl detail that shouldn't affect usage.

Styles all compiler verified.

DCE possible by delete-compile-pass/fail.

Styles need be composable.
* Conflicts and/or attribute composition? (prevent? merge? override?)
* For non-commutative composition, order & result should be clear.
* Conditional (platform - eg. IE 10)
* Conditional (property - eg. button is depressed)

CSS serves different purposes. Affects modularity & composition.
* Layout (internal)   - A modifies itself and constituents.
* Layout (relational) - A contains B; B needs css as part of A's layout.
* Decor               - Green and bold and shiiiiit.
* Media-type          - Mobiles and screen resolutions and shiiiiit.
* Defaults            - Whole page defaults to Arial 12.

Problems:
* All Scala-based CSS wouldn't be usable by other non-Scala pages
  (unless classnames or something can be specified and used in generated CSS)
* Internet commenters say pseudo-selectors don't work with inline css.
  * :visited
  * :not(:hover)
* Non-trivial CSS, it would become obsolete right?
  * abbr.timeago[title]
  * td > a
  * article + article
* Using snippets with CSS hacks
  * width: auto\9 !important
* How to affect an entire hierarchy? Why though, and is it important?
* Inline means no changing a common style on-the-fly in the browser and seeing the total effect.
* Frustrates collaboration with designers.
* Recompile per CSS change = slow dev.
* Wouldn't be good to impose style's children logic on callsite.
  Application of a single class can style the subject's children too via "&" in LESS.
japgolly commented 9 years ago

@chandu0101 said:

Recompile per CSS change = slow dev.

There is a trade off here , prod issues isolation is trivial, faster fixes! 1 sec prod >>>> 1 sec dev !

japgolly commented 9 years ago

FYI I really don't think that inline styles (in the literal sense) are the right solution. I think that style application should result in classnames/ids being applied to tags that refer to some generated css.

No matter solution, the actual usage in Scala should be very simple and will probably look the same as inline styling.

japgolly commented 9 years ago

Goals:

Haoyi brought up cascading. It's a serious problem. I said: "I just tested inline vs decl styles and cascading happens in both. That situation will require thought. If it's not solved then the "have no surprises" goal will be mostly lost."

  <div style="color:red">out<div>in</div></div>
  <div class="red"      >out<div>in</div></div>
chandu0101 commented 9 years ago

thanks mate for bringing important concerns to discuss

here are my thoughts (from a inline styles fanatic point of view)

1) more performant for browser rendering and React diffing.

I agree we definitely need serious benchmarks from real world apps. here are some benchmarks for css vs inline From @pspeter3

Mainly these two JsPerfs, http://jsperf.com/class-vs-inline-styles/2 and http://jsperf.com/classes-vs-inline-styles. However http://jsperf.com/inline-style-vs-css-class/9 does seem to imply the opposite. Bad for performance was an overstatement. I'm sure you can also cause bad performance with stylesheets by using poor selectors.

.

2)Pseudo-selectors work.

On solution can be we can create stylable tags!

object StylableAnchorTag {

  trait Style {
    def tagStyle: TagMod = Seq(textDecoration := "none" ,cursor := "pointer")
    def onHover: TagMod = Seq[TagMod]()
  }

  case class State(hover: Boolean = false)

  class Backend(t: BackendScope[Props, State]) {

    def onMouseEnter(e : ReactEventI) = {
      t.modState(_.copy(hover = true))
      if(t.props.onMouseEnter != null) t.props.onMouseEnter(e)
    }

    def onMouseLeave(e : ReactEventI) = {
      t.modState(_.copy(hover = false))
      if(t.props.onMouseLeave != null) t.props.onMouseLeave(e)
    }
  }

  val component = ReactComponentB[Props]("StylableAnchorTag")
    .initialState(State())
    .backend(new Backend(_))
    .render((P, S, B) => {
      val styles = styleSet1(P.style.tagStyle,P.style.onHover -> S.hover)
      a(styles,
        onMouseEnter ==> B.onMouseEnter,
        onMouseLeave ==> B.onMouseLeave,
        P.other
        )
    })
    .build

  case class Props(onMouseLeave: REventIAny, onMouseEnter: REventIAny, style : Style, other: TagMod *)

  def apply(onMouseLeave: REventIAny = null, onMouseEnter: REventIAny = null ,style : Style = new Style {})(other: TagMod*)
    = component(Props(onMouseEnter,onMouseLeave,style,other))

}

//usage
StylableAnchorTag(style = new Style {override  def onHover = Seq(color := "red")})("Hola"),

3) On-the-fly page-wide style changing possible directly in browser

This boils down to slow dev issue!, again 1 prod sec >>>> 1 dev sec!

4)Can work with external entities such as HTML, designers, etc.

I mostly worked as backend dev so can't talk about this in practical perspective. But we are in a rapid changing ( for better!) industry , developers working hard to cope up with current trend , all other professionals(designers , ... ) must understand this and should give support from their side too , Product is a teamwork afterall ! .

@vjeux could you please share your thoughts on these issues..(especially for 1 and 4 from a practical perspective)

vjeux commented 9 years ago

1) I've never seen style resolution yet being the bottleneck. Lots of people are worried about perf but I've yet to hear anyone said it is bad in practice.

4) We're heard this complaint about React since we announced it. In practice, designers are not stupid :) JSX and styles as JS objects look extremely similar to html and CSS so they are able to contribute without much problem. I've also heard succesful stories with designers helping on clojurescript codebases.

lucidd commented 9 years ago

I like the idea. I wonder what it would be like to have a css preprocessor like less or sass but with the full power and type safety of scala. It would definitely be awesome to use things like go to definition and refactoring across html, css and js without resorting to grepping for strings. As for using pseudo-selectors for inline css, the ones i encountered so far using react were easy to implement by either keeping track of some state (like the :hover example from @chandu0101) or simply implementing their logic like for :first-child.

japgolly commented 9 years ago

Some rough notes/ideas so I don't forget:

gjolund commented 9 years ago

Reading this thread got me really excited, you guys are reaching for the holy grail of web dev. A single language, full stack, with type safety and compiler checks. I'm sure you have already done this, but reaching out to the scalaJS / react communities directly could yield some interesting feedback, especially on the inline css vs classes debate.

japgolly commented 9 years ago

Haoyi said:

What is it that people are asking for that is not provided by less, except for type-safety? Would a type-safe less that could be embedded directly into scala be sufficient?

  • Programmatic generation of styles from Scala/Javascript code
  • Jump-To-Def, Rename, and other refactorings (IntelliJ does it's best, but even it can't do much)
  • Hygienic class names, mangled to prevent clashes
  • Opaque class names, "impossible" to construct using string-stitching
  • Minification of class-names together with the generated HTML using them (only possible because of the above)
  • Dependency analysis and dead-code elimination of your CSS, removing what is not used by the (Scala.js-generated) HTML

LESS or SCSS (which we use at work) is nice but provides none of these things ^_^

japgolly commented 9 years ago

@austinrivas I know! It's an exciting prospect hey! I hope we reach a good solution. Re: your advice, I've reached on the SJS ML here.

japgolly commented 9 years ago

I personally wouldn't like to push pseduo-selector logic into call-site/components, especially adding state. Luckily for my preferences, it wouldn't work as a full solution. :link and :visited can't be determined by JS (right?) so we'll need something else. /cc @lucidd @chandu0101

gjolund commented 9 years ago

@japgolly I've been doing a fair amount of MeteorJS development for the last two years, so I know the benefits of having a unified full stack environment. The difference between what you have in mind and the Meteor ecosystem is that Meteor was trying to introduce javascript everywhere, which compounds the issues inherently present in JS apps.

If we can pull this off it will allow for the same speed of development and code sanity that Meteor offers, while using an enormously more powerful language than javascript.

I tend to agree with you that classes make a lot more sense, especially when you look at the inline hell that some other client js frameworks can encourage ( cough Angular cough ).

japgolly commented 9 years ago

@austinrivas That's pretty cool, so are you saying that their solution is great wrt styles/css, but it's let down only by its choice of language? What would you think they got right and worked well for you?

chandu0101 commented 9 years ago

I personally wouldn't like to push pseduo-selector logic into call-site/components, especially adding state. Luckily for my preferences, it wouldn't work as a full solution. :link and :visited can't be determined by JS (right?) so we'll need something else. /cc @lucidd @chandu0101

I never used visited/link pseudo classes , but there should be a way to do in js (may be a little more work - Simple means not easy - Rich Hickey)

I tend to agree with you that classes make a lot more sense, especially when you look at the inline hell that some other client js frameworks can encourage ( cough Angular cough ).

I think "inline hell" phrase is borrowed from html inline style days , in react inline styles are a bit different we do have stylenames/classnames in code ,but in runtime we only see style attr's for a particular tag/element ( i would argue that this is a lot better(correct way!) than having many class names with overridden styles)

keep up the flow guys , It doesn't matter who wins(inline/css) ,we'll have a better system at the end of the day!

ochrons commented 9 years ago

I'd like to add "responsiveness" as one design goal. Currently it's being implemented as clunky @media rules, which have quite a lot of limitations. For touch controlled devices you'd want to have more changes than just basic grid layout, for example larger buttons etc. With programmatic styles the application would have much more control.

lucidd commented 9 years ago

@chandu0101 @japgolly seems like it was possible at one point (at least jquery could do is) but has since been disabled in some browsers because of security implications (source). So getting those to work with inline styles would probably impossible if the security stuff does its job correctly.

SRGOM commented 9 years ago

Long discussion, and I skimmed so not sure if this has been mentioned but any chance of type safety of values? e.g., say color accepts only Color.red, or Color.fromRGBA, or "auto", or "inherit". Also, if we're going the lihaoyi/scalatags way, I'd like the ability to add custom css attributes easily.

vjeux commented 9 years ago

For color, in React Native I thought about enforcing types there but I couldn't find a good reason to do that. Colors being misused as strings are not a common source of error so it didn't make sense to impose a different way of writing it just for the sake of it. This analysis may be different in the context of scalajs though

SRGOM commented 9 years ago

@vjeux: You're right, its not something that fails often and I was only giving an example, my actual point being some sort of safety for valid values, e.g. sometimes in hurry, in place of doing clear:both, I do float:both. Also, I'd wager that most of us are somewhat experienced users and are the CSS/HTML tail. Our experiences might not be representative. Also, (I've never used an IDE for HTML, so can't say for sure), but if an HTML IDE provided you completion for CSS values, then for a Scala expert but HTML beginner, writing regular HTML/CSS using IDE would be a better choice (a choice he'll soon regret though :) ). I understand your statement, and obviously we need to prioritize things because there can't be a magical solution that fits every need, but I just want to put across my view

lihaoyi commented 9 years ago

writing regular HTML/CSS using IDE would be a better choice (a choice he'll soon regret though :) )

I don't think you've written HTML in Scala for an extended period of time. Anybody who has will tell you it's completely amazing =P

The safety and tooling you get for it far outstrips anything you have writing CSS directly, and the flexibility is far beyond any templating system you've ever used before that (even React.js's). I don't see why writing CSS in Scala would be any worse.

SRGOM commented 9 years ago

@lihaoyi I think I messed up what I was trying to say. I was comparing against the imaginary future in which you have this tool in a production state without completion for values as a conscious design decision. I was going to say that the tool's current goals themselves make it better than the scala ide with html completion. I am an old guy who uses vim, but I can completely understand the power an IDE can give so no arguments with you there. Btw, Scalatags is really cool, wish it had some compile time method for typechecking for attrs. (

japgolly commented 9 years ago

Happened upon similar discussion on twitter: https://twitter.com/DavidKPiano/status/569546791269609472

Cécile Muller @ ‏wildpeaks 4h4 hours ago @ Vjeux @ DavidKPiano one downside is mediaqueries don't work in inline styles, so renderToStaticMarkup would have to generate a stylesheet

David Khourshid ‏@ DavidKPiano 4h4 hours ago @ wildpeaks @ Vjeux Also: pseudoelements, pseudoselectors, combinators and relationships, destroying the "cascade", etc.

. ‏@ yoshuawuyts 4h4 hours ago @ davidkpiano @ mbrochh @ reactjsnews @ sasscss To me this is the only thing that feels like a CSS replacement — http://gridstylesheets.org/

Colin Megill ‏@ colinmegill 2h2 hours ago @ DavidKPiano @ Vjeux We're releasing something in about 10 days that will blow your freaking socks off. Stay tuned.

SRGOM commented 9 years ago

I happened to be thinking about this today and here's a very rough idea of what I had in mind- this is based on the requirement of "where else in JS and CSS is this used".

//Shared dir: User.scala 
package shared

import scalatags.Text.all._

object User{
  val userProfile = "user-profile"
  val userBio     = "user-bio"
  val userName    = "user-name"
  val userScore   = "user-score" 

  def display( 
    name: String,
    age: Int,
    score: Int
  ) = {
    div( cls := userProfile )( 
      div( cls := userBio )( 
        span( cls := userName )( name ),
        span( s" ($age) " )
      )
      span( cls := userScore )( score  )
    )
  }
}

//CSS Framework
case class CssContext( selector: String ){
   def pushChild( c: CssContext ) // css selector of the form ".class1 .class2"
   def pushImmediateChild( c: CssContext ) // css selector of the form ".class1>.class2"
   def pushSibling( c: CssContext ) // css selector of the form ".class1+.class2"

   def style( s:  StyleElements* ) 
}

//CSS Project

val browsers = Seq( "moz", "ms", "webkit" )
val borderRadiusCustomProperties = browsers mapValues( b => CssCustomProperty[ ( SizeElem, SizeElem ) ]( s"$b-border-radius" ))

val reusableStyleElementGroup = StyleGroup.style(
  CssProperty.fontSize      := 9 rem, //space between 9 and rem intentional, using post fix operators
  CssProperty.textTransform := CssValues.TextTransform.Underline //basically some       
  //classification is needed so that text-transform can not be set to CssValues.FontWeight.Bold.
   //This creates a problem with experimental features though. 
   //If  chrome adds text-transform: disappear, then how do you write that? 
)

CssClassContext( shared.User.userProfile ). 
  pushChild( CssClassContext( shared.User.userName ) ).
  style( 
    CssProperty.fontFamily  := Seq( "Calibri", "serif" ),
    CssProperty.fontWeight  := CssValues.INHERIT
  ).
  //the definition for this style 
  //for Seqs has not been show above, 
 //but easy to do using implicit class 
  style( 
    reusableStyleElementGroup
  ).
  style(
    CssProperty.marginTopRightBottomLeft := ( 1 px, 2 px, 3px, 4px )
  )

CssClassContext( shared.User.age ).
  pushImmediateChild( shared.User.age ).
  style(
    reusableStyleElementGroup
  ).
  style(
    CssProperty.marginTopHorizontalBottom := ( 1 px, 2 px, 3px )
  )

CssClassContext( shared.User.userProfile ).
  style(
    CssProperty.marginVerticalHorizontal := ( 1 px, 2 px )
  )

Here's what it should generate:

.user-profile .user-name{ 
  font-family    : Calibri, serif;
  font-weight    : inherit;
  font-size      : 9rem;
  text-transform : underline;
  margin         : 1px 2px 3px 4px;
}

.user-profile>.user-age{
  -moz-border-radius: ( 2em, 2em )
  -ms-border-radius: ( 2em, 2em )
  -webkit-border-radius: ( 2em, 2em )
  margin: 1px 2px 3px;
}

.user-profile{
  margin: 1px 2px;
}

Problems: Css Properties are not first class citizens. You write much more than regular CSS. There's also an issue of experimental features if one's trying to keep values typesafe (see comment above). Then there's also the problem of how to treat shorthand properties. (Naming basically I think). Overall not very clean, but I figured I'll share anyway.

SRGOM commented 9 years ago

Funnily, I completely unintentionally wrote textTransform in place of textDecoration, which I guess justifies the original idea even more

japgolly commented 9 years ago

I've only quickly skimmed this, it may be of interest (and relevance). Will read properly later.

https://en.bem.info/articles/bem-for-small-projects/ http://www.smashingmagazine.com/2012/04/16/a-new-front-end-methodology-bem/

chandu0101 commented 9 years ago

Radium: a powerful complementary tool to #reactjs inline style (MQ, states..) http://projects.formidablelabs.com/radium/ #css

japgolly commented 9 years ago

I've created a new project for this upcoming solution: ScalaCSS

I have analysed what we've all discussed and produced a first draft of the requirements of everything I intend this solution to do. Please check it out, review it, and let me know if you love it, hate it, what you think. Certain things have been rejected (like CSS attr value validation - sorry @SRGOM maybe later), and certain proposed implementation details have been ruled out (for which the constraints should explain the reasoning).

Please join me there and on its Gitter chat to continue discussion.