Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

view-scoped CSS via JSS #108

Closed
leeoniya opened this issue Dec 4, 2016 · 44 comments
Closed

view-scoped CSS via JSS #108

leeoniya opened this issue Dec 4, 2016 · 44 comments

Comments

@leeoniya
Copy link
Member

leeoniya commented Dec 4, 2016

Now that view closures can return {render: ...} objects, we should evaluate offering the option of returning {css: ...} as part of the mix, possibly accepting a JSS-style [1] stylesheet definition. Each vm is already tagged with a globally unique sequential .id, so the styles can be prefixed to isolate/prevent leakage. Another option is to prefix the defs with the function.name of the the view closure. This means all views created using the same closure would by styled uniformly. In both cases the view's root nodes would need the generated prefixes auto-appended to their className.

JSS pairs well with vdom/hyperscript style libs since it's js-assembled CSS, much like like js-assembled DOM.

This would make for a good isolated and high-ROI project if anyone wants to take charge, since it wouldn't require intricate familiarity with domvm's internals. 98% of it can live decoupled as an addon.

cc @grumpi, @yosbelms, @lawrence-dol

[1] https://github.com/cssinjs/jss

@sabine
Copy link

sabine commented Dec 5, 2016

That's interesting. It looks like it would make maintaining styles of components a good deal easier.

It also looks like it would enable things that are currently very hard to do.

I could see myself using that and don't mind to be the first to try.

What's the first step for that project?

@leeoniya
Copy link
Member Author

leeoniya commented Dec 5, 2016

First let's decide on what makes sense.

I think that defining a stylesheet per view instance is not gonna be good. JSS cannot diff/patch the stylesheets so we don't want to have to regenerate and replace them on each vm.redraw(). That leaves prototype-style JSS styesheets that can be compiled once per view class and domvm will simply handle adding an extra className to view's root elements automatically.

Since each view closure/state is assumed to be isolated to each view instance, compiling and returning {style: <JSS stylesheet>} is not ideal if it's to be shared between all views. From an authoring perspective it feels very intuitive to simply throw everything into the closure, but it belies what's actually going on.

function MyView() {
  var JSS = {};

  return {
    // this is view instance specific since it can use this closure's state
    render: function() {},
    // this would apply to any MyView views and using per-instance closure state will lead to bugs
    style: JSS,
  };
}

This touches on #109 and the fact that we need some form of declared base prototype or class that can hold the JSS definition. One option is to set it statically on MyView:

function MyView() {
  return {
    render: function() {},
  };
}

var JSS = {};

MyView.style = JSS;

While more logical, this authoring style feels unnecessarily imperative.

If we plan to do per-view-class stylesheets (and not per-view-instance), it would make sense to get #109 sorted out first.

EDIT, removed some draft stuff that made it into this comment :)

@sabine
Copy link

sabine commented Dec 5, 2016

I think that defining a stylesheet per view instance [and regenerating that on every vm.redraw] is not gonna be good.

Agreed. Regenerating on vm.redraw() is most certainly overkill.

There is another option: generate a stylesheet on mount and ditch it on unmount. That's what React-JSS does.

React-JSS seems to be using per-component sheets and sharing them if multiple of the same component are mounted.

Per-view-class JSS sheets seem to be closest to what we can already do with CSS preprocessors (except that JSS also provides a guarantee that styles don't get mixed up due to bad selector choices and results in smaller file size overall - at the cost of having to generate styles at runtime).

I'm new to all this CSS-in-JS business, so let me try to summarize:

Per-view-class

  • good: generated styles can be reused between instances of the same view.
  • good: it's seems to me like it will be not too hard to collect all the style sheets of all the view classes and render them into one big (per-browser, cacheable and shared-between-all-routes) CSS file in an isomorphic no-JavaScript setting.

Per-instance

  • good: customization of styles based on run-time-evaluated variables possible. Easy to apply user-specific styles. (To get the effect of letting users customize the color of their own pages, I did let the server generate user-specific styles and place them in a <style> tag in the HTML document served, so it would be cool to have a simple way of solving this class of problem).
  • bad: no straightforward opportunity to cache/reuse generated CSS since generated styles can't be reused between instances of the same view. Server-side rendering means dumping the generated styles into the HTML document (i.e. no caching, no sharing between routes).

From an authoring perspective it feels very intuitive to simply throw everything into the closure, but it belies what's actually going on.

It may be intuitive to author, but if it's returned by the view function, the author is likely to expect that they're defining a stylesheet per-view-instance. I'm with you on this one.

As you might have guessed, I don't mind the "imperative look" of attaching per-view-class styles to a view. :) It gets the job done and it's clear what it does. Sure, there's syntactic sugar (e.g. ES7 decorators) that could be used to make it "look less imperative", but that should be up to the user to do.

@sabine
Copy link

sabine commented Dec 5, 2016

Another thought on runtime-variable-dependent styles:

Using a CSS-in-JS solution, we can define styles for components and we can also define "global styles". If I'm having a use case where I can make my runtime-variable-dependent styles all "global styles", it would be simple to just manually trigger a recompute of the global styles whenever the runtime-variable changes.

To customize the styles of components based on the content of a runtime-variable, I'd pass in the global styles to the component, and then manually add the classes from the global styles to the elements that need them. Example: define the background color of all navigation bars on the page as a global style, then enjoy the straightforward ability to change that color based on user input.

That's just one pattern that would work with per-view-class stylesheets.

And then, there's still the option of using inline styles for runtime-variables whose use is so intricate that their styles can't be flattened into a global stylesheet.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 5, 2016

if you want to hash out some of these semantics concretely, that would be great. we can start with the imperative MyView.style = .

as far as what's required of domvm itself...

mount() [1] should check if there's a vm.view.style that needs to be compiled, and run it through some prefixer using vm.view.name. then compile, cache append it into dom. i guess styles can (should?) be removed on unmount() [2] also, but this is trickier since it requires ensuring no other views of this class exist anywhere else. domvm does have an internal global registry of mounted vms [3], so you can run through these to ensure no more views exist of this class.

redrawSync() [4] will also need to check vm.view.style, and if it's non-null, append or prepend vm.view.name to vm.node's class after render()[5] is called but before reconciliation. you should probably prepend it to vm.node._class which is a static class that's prepended to any dynamic vm.attrs.class.

I'll let you figure out how to handle isomorphic rendering and any required html() additions.

[1] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L183
[2] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L207
[3] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L14
[4] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L227
[5] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L253

@leeoniya
Copy link
Member Author

leeoniya commented Dec 5, 2016

To get up and running:

  1. Have node/NPM & Java installed
  2. Download https://dl.google.com/closure-compiler/compiler-latest.zip
  3. Extract closure-compiler-v20161201.jar into the repo root and rename it to compiler.jar
  4. Run npm install
  5. Run npm run build:full or npm run watch:nano (full list in [1])
  6. Builds, source maps, min builds and README.md are generated in /dist

I should probably make minification optional to make the Java/Closure dependency optional or fall back to UglifyJS...or just transition to TypeScript. Maybe i'll get around to it one day ;)

[1] https://github.com/leeoniya/domvm/blob/2.x-dev/package.json#L11-L22

@sabine
Copy link

sabine commented Dec 5, 2016

Thanks for the detailed pointers, I will look into it and sketch some possible patterns of using CSS-in-JS tomorrow.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 5, 2016

also, it might not be a terrible idea to run the prefixer after compilation. this would allow the system to still work generically on any provided css sting rather than relying on JSS's structure in case we want to allow other css compilers.

i guess this would mean that you can just compile it exterally using whatever preprocessor anyhow and all domvm would need to do is be made aware of the view having a non-null style, handle class prefixing the root node at render, marking the style as compiled = true (or some other method of caching this), and on first mount [of the view class] append it to dom.

one idea is to mutate any MyView.style = <string> -> MyView.style = <dom style element>, then use typeof MyView.style as the switch between a compiled and non-compiled style. The compiled version can simply be detached from the dom, but still live in memory when all views are unmounted. this way it wont need to be re-compiled when the a view with same class re-mounts, it'll just re-insert it to dom.

that's probably the way to go here. minimal coupling.

@sabine
Copy link

sabine commented Dec 6, 2016

Some observations I made about the default behavior of JSS:

  1. Defining
style1 = {
    abc: {
        background: 'red'
    }
}

and

style2 = {
    abc: {
        background: 'green'
    }
}

followed by attaching both style sheets results in something like this:

<style>
.abc-4263465757 {
    background: red;
}
</style>
<style>
.abc-045034767 {
    background: green;
}
</style>

Cool! In different JSS sheets, I can use the same identifier to mean different things and JSS takes care of generating css class names that don't collide. This seems convenient.

  1. Defining
style1 = {
    abc: {
        background: 'red'
    },
    def: {
        background: 'yellow'
    },
}

and

style2 = {
    abc: {
        background: 'green'
    },
    def: {
        background: 'yellow'
    },
}

followed by attaching both style sheets results in something like this:

<style>
.abc-4263465757 {
    background: red;
}
.def-3455768006 {
    background: yellow;
}
</style>
<style>
.abc-045034767 {
    background: green;
}
.def-3455768006 {
    background: yellow;
}
</style>

Note how the resulting css class name for def is identical in both sheets. What's going on? It looks like JSS figured that since the styles are identical it should use the same css class name, or, conversely, if the styles are different, the class names generated by JSS will be different.

For most applications (e.g. those that have a fixed set of styles), this shouldn't matter, since, as long as the correct styles are applied, it doesn't matter whether the generated css class names are distinct between components.

For applications where styles change dynamically, it means that css class names do change when styles are modified (let's say some end user selects orange as their profile color and resultingly the sheets are regenerated). That means that we need to redraw the component after changing its JSS stylesheet so that the component is aware of the new css class names. In a use case with per-view-class JSS stylesheets this means redrawing all instances of the particular view. Doing a redraw in the rare case of modifying a stylesheet seems unproblematic to me.

When letting JSS generate css class names, collisions with external CSS are unlikely but still possible. Example: the other side uses a different css-in-js solution that by chance happens to generate the same class name. This is very unlikely to happen, but prefixing would indeed help with solving the issue quickly when it occurs.

If we prefix, we might just as well not let JSS generate css class names at all. Prefixing takes care of disambiguating class names between components and third party code.


I think, if the addon architecture is flexible enough, there may not even be the need for domvm's code to explicitly talk about styles.

Is there a simple way for addons to use hooks (specifically, mount hooks for the vm)? This may be related to the idea of a View base class that can be extended by addons.

Then, we could do the same as the React-Jss plugin: 1) attach stylesheet on mount, 2) keep a reference counter to remember the number of mounted views of the particular class on the view class itself, 3) detach stylesheet when reference counter reaches zero.

@sabine
Copy link

sabine commented Dec 6, 2016

Update: JSS has a generateClassName option that lets us define how the class names should look like. This is where we can do prefixing.

@thysultan
Copy link

You could use https://github.com/thysultan/stylis.js.
I build this into dio.js, the styles are scoped, not diff'ed and only generated once per component.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

@grumpi thanks for the research,

If we prefix, we might just as well not let JSS generate css class names at all. Prefixing takes care of disambiguating class names between components and third party code.

that's gonna be the way to go. plus i assume we won't have CSS compiler compression cause poor debugging, lack source maps (i could be wrong here). there is no global mount/unmouunt hook but we can add one. the counter sounds like a good idea.

i would prefer to have domvm handle the prefixing via a single regex replace ^\S pass rather than having the compiler do it to keep things less dependent on compiler semantics. there would be a bit of overhead but the decoupling is worth it IMO.

@thysultan

stylis looks good. i think @root is begging for trouble. -moz prefixing seems unnecessary in 2016. there are no un-upgradable mozilla browsers (none are tied to a specific OS version).

i dont think i'd want to bake a css compiler into domvm, but leave some integration spec/api so a third-party one can be hooked up. i'll certainly add stylis to a "pairs well with" section in the docs, similar to xr.js, flyd.js, etc.

@sabine
Copy link

sabine commented Dec 6, 2016

@thysultan That looks interesting and seems to have about 20% the (gzipped, minified) size of JSS (without plugins, so JSS size will be even bigger with vendor prefixing).

I like your approach of focusing on the essentials (vendor prefixing and componentizing styles). There's a lot that JSS offers that isn't strictly necessary.

I personally need a way to reuse global style definitions without having to copy-paste them. Like SASS mixins or extending classes (extending classes generates css like .base-class, .derived-class { [styles] }, while mixins generate .derived-class { [base-class styles] [derived-class styles] }. I suppose, I could just write functions that return CSS styles and build my styles string by concatenating these style fragment "mixins". It does feel a tiny bit more cumbersome than merging the styles into a JSON structure, though.

It looks like selector specificity works fine as long as we scope everything with ids. That sounds like stylis would work well in a situation where style sheets aren't shared between instances or rather, where every instance has its own stylesheet so that it can be scoped to the instance's unique id.

@leeoniya Btw, here's what I came up with so far while experimenting with JSS: http://jsbin.com/doquhotecu/1/edit?js,output . This mostly illustrates my earlier points.

i would prefer to have domvm handle the prefixing via a single regex replace ^\S pass rather than having the compiler do it to keep things less dependent on compiler semantics. there would be a bit of overhead but the decoupling is worth it IMO

I'm not sure I agree with this one. People who choose a particular css-in-js solution probably do so because of the compiler semantics. If domvm forces styles through a mandatory prefixing pass that doesn't seem to be decoupling, but rather coupling domvm to the idea of using a css-in-js library by clogging up the code with stuff that is only relevant if people do use a css-in-js library.

I see a place for providing prefixing in the css-in-js addon. I don't see anything wrong with choosing prefixing to be the default and recommended way to deal with disambiguation of css class names. It does make sense to provide a basic prefixing implementation that can run over any generated CSS.

The thing that matters most in my eyes is that "domvm.css-in-js" provides a uniform interface that can be implemented by different css-in-js adapters (e.g. "domvm.css-in-js.jss" or "domvm.css-in-js.stylis"). Some adapters may need the prefixing code, others don't (e.g. jss doesn't need it because prefixing is trivially done by setting the generateClassName option - see jsbin).

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

I'm not sure I agree with this one. People who choose a particular css-in-js solution probably do so because of the compiler semantics. If domvm forces styles through a mandatory prefixing pass that doesn't seem to be decoupling, but rather coupling domvm to the idea of using a css-in-js library by clogging up the code with stuff that is only relevant if people do use a css-in-js library.

Isn't the entire value of defining a MyView.style in that it will get auto-scoped for you, auto-<style> wrapped, auto-inserted on first mount and auto-removed on last unmount. And by "inserted" i dont mean you need to include it in a _raw tag within your templates. IMO there is very little value added over simply exposing global mount & unmount hooks if we start striping out or externalizing all implications from this feature. Clearly you can do everything yourself as your experiment demonstrates.

RE stylis:

I guess my main beef with stylis is that its input is one large stylesheet string. This requires ugly string concat in app-space to dynamically build/define the stylesheet. Tagged template literals would help here, but still lack the full expressiveness of js. It doesnt quite fulfill the spirit of css-in-js as a json-esque structure does; when i think "html in js" i don't want JSX, i want hyperscript. Likewise, css-in-js should be a "hyperscript for css" (hyperstyle TM) - @thysultan if you want to make that, do ping me :)

EDIT: JSS seems like the spiritual equivalent to JSONML, but domvm 2 ditches it for hyperscript. There's no reason not to want the same for css if it can be done non-grotesquely. You can even do stylesheet diffing/patching if you end up hanging on to the vcss, compression/mangling for SSR, auto-px insertion and vendor prefixing, etc....mostly matching the same semantics as hyperscript vdom libs. This would bring full uniformity in a codebase to "everything is js" - it may not be for everyone, but it's how i want to author.

s(".foo, .bar > span", {
  width: 100,
  height: 100
}, [
  s(".baz", {
    color: "red"
  })
])

s("em", {
  width: 100,
  height: 100
})

or to avoid app-space concat of multiple selectors via ,:

([".foo", ".bar > span"], {
    width: 100,
    height: 100
  }, [
    s(".baz", {
      color: "red"
    })
])

this would allow any post-processing plugins to ditch their (probably buggy) parser, custom DSLs and operate on the vcss directly.

@sabine
Copy link

sabine commented Dec 6, 2016

Isn't the entire value of defining a MyView.style in that it will get auto-scoped for you, auto-inserted on first mount and auto-removed on last unmount

You're right, that's the value.

Doing the experiment, I just noticed that if there was an authoritative way for addons to run hooks for all views and to process the views, this might be a useful part of the addon architecture for domvm. My intuitition says that we can get a clean interface using little code just with an addon, as long as the addon has an easy way to register those hooks for the view. :)

(My intuition may be off, and if addons aren't supposed to be able to "transform" views by registering hooks for them, it seems easier to do the integration in domvm itself, rather than in an addon.)

Clearly you can do everything yourself as your experiment demonstrates.

True. The point of the experiment so far has been to see how things look like if done manually in the naive way so I have something to improve on. (Please consider that I hadn't even heard of css-in-js before this thread brought it up.)

I don't want to do things manually because repeating this on every view would be a major pain. There clearly needs to be something that encapsulates the annoying/repetitive parts and that shows a nice, minimal interface.

One thing I very much agree with is that it's not desirable to leave everyone to do their own css-in-js implementations when there is so much that can be shared.

Maybe css-in-js is to the core domvm like the router is to core domvm. It's something that many people in the domvm ecosystem may want to use. It does make sense for domvm to ship with something that gives out-of-the-box support for one css-in-js solution that is favored by domvm (either that or shipping with a minimal css-in-js solution, just a tiny bit larger in scope than the current version of stylis).

It's probably a preference thing... when I pointed out how I had to manually add the onclick handlers for clicked links in 2.x-dev, I was delighted that I now have this fine-grained control over how clicking links inside my app behaves. You were quick to point out that you might want to add some explicit router support in domvm core. :)

Feel free to laugh. In the end, I'll trust your judgement, because I'm far better off building on what you're making here than doing my own thing. I can see how having a few tiny pieces inside domvm core to properly support different things like routers, css-in-js, and whatever else comes up may be better than having an addon architecture that could result in people writing addons that interfere with each other.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

I'm not opposed to providing global mount/unmount hooks for addons to use, in addition to exposing the global views vm registry - there could be a lot of vm <-> vm comm that this can facilitate, though things can get pretty hairy if you're not careful - side-effects, one-way flow and all that. The css-in-js could definitely be implemented as an addon. I'll have to check the performance implications of checking for custom hook presence on every view mount.

I'm actually quite tempted to test out the hyperstyle idea by reusing domvm's vtree nodes with a new STYLE node type. The overall code increase could be < 1k, perhaps significantly. This would be decoupled and not handle View class prefixing, which as i've mentioned i'd like to simply operate on already-compiled css which allows people not to use any complex form of css-in-js solution. They can even load the css from an on-page style tag and assign the textContent to MyView.style for prefixing.

Feel free to laugh.

This is no laughing matter! 😆

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

@grumpi actually those global hooks are already exposed by way of the ViewModel and its prototype being exposed [1]. Numerous addons already use this to add functionality.

You should be able to do domvm.ViewModel.prototype.hooks = {mount: ..., unmount: ...};

The only minor issue with this is that if vm-instance hooks are also defined by a view, they will replace vm.hooks [2] rather than shallow-clone & extend. but i'll add a fix for this.

[1] https://github.com/leeoniya/domvm/blob/2.x-dev/src/builds/pico.js#L18
[2] https://github.com/leeoniya/domvm/blob/2.x-dev/src/view/ViewModel.js#L125

@sabine
Copy link

sabine commented Dec 6, 2016

I'm actually quite tempted to test out the hyperstyle idea by reusing domvm's vtree nodes with a new STYLE node type. The overall code increase could be < 1k, perhaps significantly

That sounds like a great idea to me. I was just about to suggest something like that, but you beat me to it and your plan sounds a lot more thought out. I certainly don't want to write css strings, and the json structure of jss seems workable, but far from optimal. I've been doing okay with SASS, but it would be really nice for maintainability to place styles right with the components that use them.

operate on already-compiled css

I agree that this is a good idea. It's also simple to implement.

So there's ideas for two separate modules:

  • hyperscript css compiler that compiles to styles string
  • lightweight css-in-js addon for domvm that attaches and detaches styles to/from the document head when components are mounted/unmounted, prefixes classes in the provided styles string and transforms templates by replacing occurrences of class names in the style sheet by the prefixed class names.

Sounds like a plan. When jss is out of the picture, the cognitive dissonance resulting from repeating things that jss already does is gone. 😆

actually those global hooks are already exposed by way of the ViewModel and its prototype being exposed

That's cool. I wasn't aware of that. :) Ok, good. Will see what happens tomorrow.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

I've been doing okay with SASS, but it would be really nice for maintainability to place styles right with the components that use them.

just to clarify, i didn't mean you'll be defining style inside the render() template, as this would still be subject to mutation trans-redraw, so would represent view-instance rather than view-class definitions. i was just thinking out loud about the internal implementation of having it live as something like var style = domvm.style(<hyperstyle>), and then assigning that compiled output (which would be plain css) to MyView.style. This acheives both the css compilation as an addon and .style support in the core or at least as an addon in nano+ builds (which would be ultra light weight).

@lawrence-dol
Copy link
Collaborator

Not sure, entirely, of the practical upshot of all this, though I love the idea of styles bound with component all in JS; so in my experiments having a separate CSS file has been a thorn.

It is, though, highly beneficial to preserve the ability of externally loaded stylesheets to override/customize the styles of any particular component, ideally without resorting to using !important or artificially creating "more specific" rules.

@sabine
Copy link

sabine commented Dec 6, 2016

i didn't mean you'll be defining style inside the render() template

No worries. That's clear. I'll be attaching styles to document head and detaching them from there again. Even for view-instance sheets, if I'm going to support them at all. There's no reason to treat them different from per-class sheets. I'll first try and see how my use case works out with per-class sheets.

having it live as something like var style = domvm.style(), and then assigning that compiled output (which would be plain css) to MyView.style

That's the straightforward way of gluing them together I was thinking of as well. It just feels a little bit odd to be stringifying and then parsing again (to prefix). But I think that's better than the alternative of supporting css vnodes as input to the css-in-js addon. Edit: Hm... nah, I'd rather work on vnodes than parse, that's for sure, now that I've seen the chaotic mess css parsing is. 😆

@lawrence-dol I think we can do the same thing jss does: give you the ability to specify a comment in your HTML doc where the styles should go. Then you can place any stylesheets that override after this position.

@lawrence-dol
Copy link
Collaborator

Then you can place any stylesheets that override after this position.

Hmmm. I don't think that will meet the need. This would be totally after the fact customization with zero modifications to the component code.

Such as my current project which comes out of the box aesthetically pleasing, but basic, and which the customer can trivially supply any number of CSS files loaded from their area of the web site containing targeted overriding styles. Therefore, it's important that the two style sheets, mine and theirs, are at the same level in the cascade, or that theirs' take precedence. And this can't be done with user stylesheets because they don't control their customers' computers, so from the end-user's point of view, both at just stylesheets coming from the web server.

@sabine
Copy link

sabine commented Dec 6, 2016

Therefore, it's important that the two style sheets, mine and theirs, are at the same level in the cascade, or that theirs' take precedence.

Okay, I just looked up cascade. So, the problem is essentially that style tags in the head element are higher priority than stylesheets included with link tags, is that correct?

So the only way to override styles that are defined in style tags in the head of the document is to place more style tags below, but this is horribly clunky and not what your customers need (they'd rather place another link tag to override styles), correct?

Edit: Do you think using a script instead of a link to override component styles could work? The most trivial thing we could do is just concatenate the new definitions to the component's existing styles.

@lawrence-dol
Copy link
Collaborator

they'd rather place another link tag to override styles

In general, yes, that's more elegant and flexible.

In this specific case, it's not a preference but necessary, because the only way to include their styles, unknown at the time of deployment, is to optionally reference them in header links, using filenames provided in the URL. Sucking the content of the customer's files into the document between two style tags will not be possible.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 6, 2016

where to stick the generated styles is a good question. i think since all domvm styles will be scoped (presumabley they will not have side-effects) we can prepend them as the very first styles in <head>. anything the user had during initial render will then come afterwards and override the defaults if needed.

it feels a bit backwards to have them exist before global resets like normalize.css, border-box, bootstrap, etc, but it should work okay in practice. i dont know if this arrangement is harder on the browser's css processing, but we can test this.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 7, 2016

@grumpi

Hm... nah, I'd rather work on vnodes than parse, that's for sure, now that I've seen the chaotic mess css parsing is.

Yeah, after some testing, the regex/replace prefixing falls apart pretty quickly and then you're headed for css-parsing hell. I guess it would make sense to expose a hookup for users to define some basics for prefixing to be handled externally. Here's my proposal:

// hyperstyle node generator
var s = domvm.defineStyle;

// default compiler that supports domvm.defineStyle; this can be
// swapped by the user if another DSL & processor is used
domvm.compileStyle = function(style, viewName) {
  // run some loop to prefix the vcss with .viewName and barf out a stylesheet string
   return stylesheet;
};

function MyView() {...}

// set the style to the css processor's native DSL. in this case a hyperstyle vtree or fragment
MyView.style = [
  s()
];

@leeoniya
Copy link
Member Author

leeoniya commented Dec 7, 2016

hooks patch landed: b2ccddb

@sabine
Copy link

sabine commented Dec 7, 2016

I found this: https://github.com/jotform/css.js/blob/master/css.js It seems to be doing what's needed from a css parser.

I think there's orthogonal requirements and also orthogonal deployment situations:

1: deployment is in a pure JS project that you have control over
2: deployment is in a traditional environment (e.g. CMS, jQuery, external libraries) that you don't have much control over

A: styles don't change during runtime
B: styles do change during runtime

possible ways of authoring CSS in JS:

  • X) precompile all styles attached to all components into one big sheet, deploy in the traditional way (as a CSS stylesheet, without including the JavaScript required to process components' CSS and without including the components' style definitions). Works for 1A and 2A. (Having a build step that collects and builds all the CSS styles is also helpful in the server-side-rendering case as long as styles are static.)
  • Y) compile styles at runtime and place rendered styles in a style tag in the head of the document. Works for 1A and 1B.

possible ways of overriding CSS with user-styles:

  • in X) just use link tags in head of document
  • in Y) link tags don't work, styles need to be overridden in another style tag or by overwriting the sheet of the component in JavaScript.

@sabine
Copy link

sabine commented Dec 7, 2016

How about something like this: https://jsbin.com/punuxotoki/1/edit?html,output ?

I went ahead and implemented an example of a Stylesheet class with automatic prefixing on css strings using the jotform parser. I think this only serves as an example. Let's do better than that. Authoring CSS strings in JS is worse than just writing plain CSS. 😆

The idea of the Sheet class is to provide a common baseline of 1) attaching/detaching style strings to/from the DOM, 2) keeping ref count, 3) caching the compiled CSS, and 4) automatically applying required transformations to the vnodes. For a css compiler, we implement

  • the compile method - needs to create a css style string and store it in compiledStyles.
  • the transformVm method to modify the view template before mounting. In the example: attach the class name to the root element. I'm not sure this is what we need. It just happens to by chance work in this example. I think you have a better idea how to correctly have addons apply transformations to the vm's vnodes.

@lawrence-dol
Copy link
Collaborator

Would it be sensible to have a named placeholder style tag. Any DOMVM styles go in that?

@leeoniya
Copy link
Member Author

leeoniya commented Dec 7, 2016

I'm a bit busy ATM, but i'll review all this later today.

I wanted to bring up a point that calls into question this entire topic and see what you guys think.

View-scoped css cannot prevent styles from leaking into sub-views. This is of course the folly of "C" in CSS. But is it worth providing a solution that does only half the job by preventing leakage out?

@sabine
Copy link

sabine commented Dec 7, 2016

@lawrence-dol

Would it be sensible to have a named placeholder style tag. Any DOMVM styles go in that?

In this first sketch, I did the same thing jss does, i.e.

  • giving the ability to place a comment <!--cssinjs--> into the DOM as a placeholder for where all the styles tags of the different sheets go, and
  • attaching and detaching styles for the individual components on mount/unmount.

Adding a data-attribute or an id to the style tag that identifies the component which the styles are for is totally feasible. I added this in http://jsbin.com/bazuzodowo/edit?html,output

With "a named placeholder style tag", do you mean one style tag that will hold all the styles for all the components, or do you mean one tag for each component? Please clarify. Both is possible, but the former could mean that we'd just append the component's styles to that style tag when it's mounted for the first time.

@leeoniya

View-scoped css cannot prevent styles from leaking into sub-views

I'm aware of the problem with view-scoping. I think prefixing the actual class names is a more precise solution. E.g. turn

.breadcrumbs {} into .viewClass-breadcrumbs rather than .viewClass .breadcrumbs. However, this requires processing the generated vnodes to prefix the className in all relevant places. Is that feasible?

Styles that are inherited may still leak into sub-components, but the subcomponent can do a reset on its main node. (Whereas in the case of descendant-scoping(is that a good name?), the subview's own styles might lose a specificity battle against the parent component's styles.)

@lawrence-dol
Copy link
Collaborator

In my opinion, the 'C' in CSS stands for a vulgar term referring to unwilling intercourse with a group of assailants. But we are stuck with it because our industry can't, as a group, design it's way out of a paper bag.

They almost partly fixed this with the introduction of <style scoped> and then someone realized that this would make too many developers' lives easier and unceremoniously kiboshed it, throwing the baby out with the bathwater instead of recognizing that the suggestion reveals a gaping problem with the current state of the art (art is used loosely here).

As a whole, we still haven't collectively recognized that the "specificity" rules aren't rules, but more guidelines for the partly-insane and savants among us. Add to this that the cascade refers to seepage of unintended side-effects that make even the most well meaning effort devolve into gibberish with 6 weeks of having been written.

So we continue to pursue the heights of madness, ever courting danger on the brink of despair, in an everlasting effort, doomed to ultimate failure, to write sane, maintainable styles to control the visuals of an application, using tools designed to produce documents. BECAUSE THERE'S NOTHING ELSE.

So yes, we should wrestle with this until we can do something better, or realize that doing the same thing using a different syntax is folly.

(rant over)

@lawrence-dol
Copy link
Collaborator

do you mean one style tag that will hold all the styles for all the components, or do you mean one tag for each component?

I had in mind the former, but the latter would be, if feasible and efficient, better.

Styles that are inherited may still leak into sub-components

This seems to be an intractable problem unless and until there's some mechanism to denote an element scoping boundary whereby CSS logically does a "revert to browser defaults here". But if all styles for a component are prefixed, a reasonable level of isolation should be possible.

@sabine
Copy link

sabine commented Dec 7, 2016

Oh, but CSS is such a terrific language, as long as you can keep the number of assailants (the sum of third party sources, third party users, and coworkers) at bay and practice self-defense skills. 😉

the latter would be, if feasible and efficient, better

At what point of time would you need to see the placeholder tag? Is mount time of the component early enough, or does it need to be on document ready, when the component has loaded, or at a different time? What would you be doing with the placeholder tags? Would you want to kill them to eliminate a component's styles? Let your users replace the content with something they like better?

I do think putting individual placeholders and filling them on mount (and whenever the style sheet explicitly changes during runtime) is both feasible and possible. It currently seems easier and more straightforward to me than putting one placeholder for all the styles.

I don't know how high the cost would be in terms of performance. We'd have to look at how many components there are. If the number of components gets high, inserting all their placeholders using something like innerHTML could probably make things fast enough.

I suppose a div placeholder in the body, like,

<body>
  <div id="domvm-component-styles"><!--cssinjs--></div>
[...]
</body>

wouldn't work?

Still, I think that it may be worth exploring the route of

  • using js to author and compile css and
  • using js to prefix class names (both in the generated sheet and in the view template),
  • not using js to actually compile and insert sheets at runtime (instead, just compile to css that gets minified and served in the traditional way). I mean.... in many use cases it is pretty wacky to compile static information at runtime just to save a little bandwith (by not needing to vendor-prefix the source styles).

if all styles for a component are prefixed, a reasonable level of isolation should be possible

The jss people seem to think so too (by default they're creating those wacky unique class names by attaching numbers, but they can prefix, too). I think so too.

@sabine
Copy link

sabine commented Dec 7, 2016

Ahh, no, there was another way to leak into subview styles.

Parent styles:

.abc li {...}

Child has a li.def tag that is styled with rule

.def {...}

And loses against parent's more specific rule.

-> coding convention: never use descendant selector. Better: use child selector. Again better: just give the darned thing its own class, if possible. Edit: I was jumping to conclusions too quickly again. Actually, the coding convention would be "never use descendant selector without a class selector (generic selectors, like attribute selectors and tag selectors are bad descendants)".

Edit: In some cases, descendant selectors are fine. I.e. when writing a view that doesn't have a subview that styles could leak into. The other question is... is isolation between view and subview always desirable?

@leeoniya
Copy link
Member Author

leeoniya commented Dec 8, 2016

@grumpi

The stylesheet impl looks pretty good. May want limit the usage of this and prototype, replacing some stuff with closures. This may help compression a lot.

BTW, multiple X.prototype.* = defs can be shortened to:

function X() {}

X.prototype = {
  constructor: X,
  foo: function() {},
  bar: function() {},
}

E.g. turn .breadcrumbs {} into .viewClass-breadcrumbs rather than .viewClass .breadcrumbs. However, this requires processing the generated vnodes to prefix the className in all relevant places. Is that feasible?

Probably not. We should avoid trying to do too much.

Re: placeholders, I'm not sure this is a good idea. Prepending all prefixed styles to <head> should be sufficient. Fewer things for users to remember to do.

Re: multiple vs single <style> nodes, let's not do anything fancy until we need to. 1 per component is less complex to implement, plus modification of a single stylesheet may cause a re-parse/re-eval of the whole thing, so could be slower.

SSR is still gonna be whacky if we're prepending styles to <head>. There's a good chance that the entire <html> document is not generated by domvm, so we need a way to provide some mock or callback that can collect these generated stylesheets so they can be concat'd and prefixed into the output's <head> and also marked as "attached" in the barfed js, then the hydration would need to pick up those <style> elements. We'll need to figure out a way to do this which isnt super awkward.

Drafted this a bit earlier...

I guess if we're stuck with using CSS, we'll just hope that authors use something sane to avoid clobbering sub-view styles.

Fully agree that descendant selectors are generally toxic and explicit classes are ideal (lately i like BEM [1][2][3]). However, child selectors force you to maintain a rigid stucture, which becomes a nuisance and a source of bugs during any form of refactoring. Keep in mind that adding classes to everything is gonna eat into your vdom performance (initial render moreso than redraw), it's a trade-off like anything else.

I think the general idea of runtime-generated css is not gonna be a common case, so let's put hyperstyle aside for now and focus on simple auto-prefixing of user-provided stylesheets, which means we'll need a fast parser.

CSSTree and mensch look like the fastest css parsers [4][5] excuding Styletron [7]. I'm able to get a minified mensch down to 7.55k (min). CSSTree is ~0.7k larger since its build process is not great, some regex duplication, etc. My intuition is that any parser that's significantly smaller will be deficient in some surprising way. I'd be interested to test [5] the performance claims of the one @grumpi mentioned [6] since it's about ~1k smaller than mensch.

BEM:

[1] http://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/
[2] http://getbem.com/introduction/
[3] https://en.bem.info/methodology/quick-start/

Perf:

[4] https://github.com/hellofresh/css-in-js-perf-tests
[5] https://github.com/postcss/benchmark
[6] https://github.com/jotform/css.js

Styletron:

This thing seems like the ferarri of CSS with a resulting size & perf that's delicious - but impossible to debug...yet.

[7] https://ryantsao.com/blog/virtual-css-with-styletron

@sabine
Copy link

sabine commented Dec 8, 2016

SSR is still gonna be whacky if we're prepending styles to . There's a good chance that the entire document is not generated by domvm, so we need a way to provide some mock or callback that can collect these generated stylesheets so they can be concat'd and prefixed into the output

If we want to barf styles in the head in SSR, we need to collect all the active sheets. That's true. All that's needed is to go over the vm registry to extract them all. The css-in-js addon should provide a function to do that. Don't think we need callbacks or anything fancy.

the hydration would need to pick up those <style> elements

When a Stylesheet attaches for the first time, we can check if a style element for it already exists and claim it, if it does exist. As long as the style elements are outside of the part of the DOM that is controlled by domvm, this naive approach should work just fine. Nothing to rehydrate on domvm's side.

I think the general idea of runtime-generated css is not gonna be a common case, so let's put hyperstyle aside for now

Ahh, but the point of hyperstyle isn't runtime-generated css. It's having a sane language to write CSS in.

focus on simple auto-prefixing of user-provided stylesheets, which means we'll need a fast parser.

If we're not in the business of runtime-generated css, prefixing can be an external build step instead of "compiling" at runtime. So we could use any parser we want to. :)

If a website consists completely of static content, the idea of making a Single-Page-Application written in JavaScript rendered by the client's browser is pretty nuts, do you agree?

By the way, to get prefixed class-names, we don't even need to walk over the vnodes to adjust them. We could also do something like this:

function View(vm) {
    var cn = vm.view.styles.className;
    return function() {
        return el("div", [
            el('h2', "View"),
            el('div.'+vm.view.styles.className('abc'), "Hello, this is my stylesheet:"),
            el('pre.'+vm.view.styles.className('def'), vm.view.styles && vm.view.styles.styles),
        ])]);

(This is the same that jss does: providing a way to look up the generated class names.)

Styletron: This thing seems like the ferarri of CSS with a resulting size & perf that's delicious - but impossible to debug...yet.

The comparison with a ferrari is pretty good. The lack of back row seats (in this case, the complete lack of child/descendant selectors means that hover styles need to be done in JavaScript) can be rather uncomfortable. Other than that, I like their idea of breaking up things into small composable pieces. Edit: Actually, something like Styletron could work really nicely for my use case - if there was a way to do hover styles and animations with child or descendant selectors.


Maybe we should look at the things we want to achieve. Looking at the hyped things others are doing is fun but also leads us away from a possibly-existing simple solution to what we really need.

I...

  1. need to write CSS in a way that lets me avoid repetition (i.e. I need functionality similar to SASS mixins/extends to add styles that are supposed to repeat through the whole app)
  2. want to avoid accidental style clashes and would like to make specificity battles as unlikely as I can. Edit: to be precise: I never want to write !important again.
  3. do care about runtime-generated css, but the largest part of my css would be static and could, in principle, be compiled and served the traditional way. I don't care much about the "adding and removing styles to/from the head thing dynamically"-thing either way. In my eyes, dynamic style tags are useful for exactly the runtime-generated css but not in a general case.
  4. would like to keep my styles close to my components (maybe in the same folder) - and have the ability to automatically check for unused styles.

Most of what I think I need has to do with improving the usability of maintaining CSS, not with performance, and not with doing things a "modern" and "cool" CSS-in-JS way just because we could.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 8, 2016

I dunno, this whole thing is starting to seem like a [worse] solution to a mostly-solved problem, for little added benefit. If you have even a moderately complex system, let's face it, your time is more valuable than the investment into adding some lightweight build process that includes SASS. To prefix in SASS is so easy a 5-year-old can do it. If you also adhere to BEM, then you're in even better shape.

The scoping & repetition issues are now gone. Point 1, Point 2

Point 3 needs some clarification:

"In my eyes, dynamic style tags are useful for exactly the runtime-generated css but not in a general case."

In what specific non-contrived cases would this justify adding extra API surface.

Point 4.

would like to keep my styles close to my components (maybe in the same folder)

sure, keep them wherever you want.

and have the ability to automatically check for unused styles.

if you mean at per-view-class level, we can just expose the view registry and your ssr backend can figure out what's mounted and only include those stylesheets. though this seems pointless since new things can mount later on the client.


there could be some case for using hyperstyle to write CSS, which gains you some wins by simplifying the compilation step by avoiding parsing (so much so that you can do it in real-time) and not use heavy alternatives like SASS to precompile. But the rub is, SASS/LESS/CSS has mature IDE tooling, which i think brings much more value than the simpler syntax of HTML and limited set of attributes that you can keep in your head, that's why hyperscript works in the first place. If HTML had as many attributes and values as CSS does hyperscript would fall over from a usability perspective without some form of hinting/IDE.

i'm gonna need some solid common-case problems that this entire thread solves to justify inclusion into the lib (added API surface, code maintenance, tests, docs). it clearly can be done as an external plugin, perhaps it should stay this way.

i'm against anointing some specific CSS parser and sucking it into the lib to pair with this feature. it's true that a flyd stream adapter is included in the source, but view reactivity has much more client-side value (and is by definition dynamic) than some isolated need for client-side generated styles.

@sabine
Copy link

sabine commented Dec 8, 2016

I dunno, this whole thing is starting to seem like a [worse] solution to a mostly-solved problem, for little added benefit.

Kind of. The question is what exact problem is left that runtime-inserted/compiled style tags are solving? 😆 That's why I figured we better back off and consider what we actually want.

your time is more valuable than the investment into adding some lightweight build process that includes SASS. To prefix in SASS is so easy a 5-year-old can do it. If you also adhere to BEM, then you're in even better shape.

Can't argue with that.

"In my eyes, dynamic style tags are useful for exactly the runtime-generated css but not in a general case."

In what specific non-contrived cases would this justify adding extra API surface?

I had intended this to represent a point for domvm not providing any support for runtime-generated css. Sorry about the confusion.

People who need runtime-generated css can just use one of the existing css-in-js solutions (they all come with their own code for inserting/removing style tags) or roll their own (I mean, I currently effectively do my run-time generated css by computing some values, inserting them in the right places in a stylesheet string and slapping that in a style tag).

have the ability to automatically check for unused styles

I was more thinking along the lines of what https://github.com/purifycss/purifycss does (I didn't know that purifycss existed before I just stumbled upon it while searching for existing solutions a moment ago). Just something that lets me know when I'm never using a css class. But since someone already solved that, I suppose I'm covered.


I think now that a workflow that fits my needs probably looks like this:

  • write static sheets in SASS and place them in the same folders as the components using them, prefix all the class names in SASS the same way I prefix them in my components.
  • write a small runtime-generated sheet in the smallest css-in-js library I find. That seems a little more convenient than 1) making another sheet in SASS and 2) copying my compiled styles into a javascript string and 3) manually modifying that string to get all the computations done. (Maintaining this one has been a nightmare.) This sheet would be global, so there's no need to mess around with views at all.
  • use purifycss to keep things tidy

there could be some case for using hyperstyle to write CSS, which gains you some wins by simplifying the compilation step by avoiding parsing (so much so that you can do it in real-time) and not use heavy alternatives like SASS to precompile

I think that describes it well. Hyperstyle could be an alternative to SASS and allow easily manipulating styles in JavaScript. It would make runtime-generated styles more convenient.

I think you're right it's probably not worth to invest the effort into creating and maintaining it; there's so many people making these small css-preprocessors and css-in-js libraries these days. I'm pretty sure there must be something in the haystack that will do. :)

i'm gonna need some solid common-case problems that this entire thread would solve to justify inclusion into the lib (added API surface, code maintenance, tests, docs). it clearly can be done as an external plugin, perhaps it should stay this way. i'm definitely against anointing some CSS parser and sucking it into the lib to pair with this feature.

I've been saying it should be an external plugin from the start. 👍 So, totally agree with that.

@leeoniya
Copy link
Member Author

leeoniya commented Dec 8, 2016

let's close this then. thanks for your input, guys 👍

@leeoniya leeoniya closed this as completed Dec 8, 2016
@sabine
Copy link

sabine commented Dec 11, 2016

Ok, I finally understood how a sane way to use css-in-js goes:

Development:

  1. refer to css class names of your views indirectly. (e.g. el("div."+getStyle("contact-list"), [...])).
  2. author css in js in whatever format the css-in-js lib you're using requires (e.g. {"contact-list": { background: 'green' }}).

Build:

  1. make your css-in-js lib barf out the CSS for your styles (e.g. ".a { background: green; }", or ".MyView-contact-list { background: green; }") to a plain old CSS file.
  2. If necessary, create the mapping of style identifiers to css class names (e.g. styles = {"contact-list": "a"}) that is used by your views.
  3. postprocess CSS as necessary (e.g. using https://github.com/postcss/postcss).

How to efficiently do styles that need to be customized at runtime:

Use placeholders in your generated CSS. E.g.

.user-profile {
    background: $user-background-color;
    border: 1px solid black;
}

Postprocess the CSS to separate exactly those declarations which contain placeholders.

With https://github.com/mrnocreativity/postcss-critical-split, one way to do this is:

  1. You need to mark all the declarations with placeholders with a comment. I do that by prettyprinting my CSS, then running a simple regex that adds the comment to every line that contains a $ sign (you could use a different placeholder-syntax, btw).
  2. process with postcss-critical-split.

Result:

dynamic.css:
.user-profile {
    background: $user-background-color;
}

fixed.css:
.user-profile {
    border: 1px solid black;
}

Insert runtime-computed values into dynamic.css in a fashion like https://github.com/ezakto/CSSTemplate.js (i.e. "render" your CSS template by finding/replacing placeholders with their computed values to a style tag).


Random comments:

  • In my eyes, the "insane" way to use css-in-js is to do everything in the client - as shown in many css-in-js examples. I can think only of few cases that would possibly warrant to incur the cost of a) making the client build bigger (by adding the css-in-js lib) and b) reducing performance (I suspect this only matters on mobile devices but haven't done benchmarks) by doing more JavaScript computations on the client.
  • I think almost no one needs the full power of runtime-generated styles (which is what you could achieve with a css-in-js solution running on the client). Most people (me including) only want to customize their sheets a little (e.g. like making the color scheme customizable), and that's where automatically extracting a CSS template seems a wonderful solution.
  • I'm going to try my luck with the ferrari (styletron). Reason: It looks like I can reduce the amount of CSS to around 25-40% of the size I would have if I wrote it using SASS while getting better automation (e.g. automatic generation of CSS templates for dynamic styles). It might be that the lack of descendant selectors really isn't a problem. And if there really is a case where it is, I can add that style without running it through styletron. Will report how it goes.

@leeoniya
Copy link
Member Author

Ok, I finally understood how a sane way to use css-in-js goes:

[7 paragraphs of detailed instructions]

lol thanks, i needed that today :D

let me know how styletron works out, specifically the debugging experience.

@sabine
Copy link

sabine commented Dec 11, 2016

So far, I can just say that the "flat" way of authoring CSS (no descendants) seems to have more upsides than downsides.

Compared to writing SASS, there is much less I need to keep in my head: no need to think about what to @extend and what to @include, I can slap all the styles in the style declaration and styletron sorts things out. With SASS, I was constantly thinking about how I can reuse things reasonably in order to keep the generated CSS small. With styletron it just automatically happens to become as small as possible.

I wrote a little wrapper so I can write styles this way:

            s("my-special-button", [
                mx(mixins.button),
                    
                p(":hover", [
                    d("border-color", "pink"),
                ]),
                m(media.phone, [
                    d("padding", "1rem"),
                ]),
            ]),

I've mostly been porting over existing styles. Still have to port the parts of compass-vertical-rhythm that I use, and a bunch of other things.

Can't say much about debugging yet.

I do set the style identifier as class name on the node in development, next to the styletron-generated classes, so I can easily see where in my code I have to look to find the style definition I might need to change. I suppose I could also put this into a data-attribute or somewhere else, but, since my style identifiers are extremely unlikely to clash with anything, I just put them there.

I also run the generated CSS through https://github.com/stylelint/stylelint, but I haven't found an option to make it check that the generated CSS is truly valid. It does help with catching some things, though.

Just did a quick search for a validator, and it looks to me like the simplest way to get good warnings is to run the generated CSS through the w3c validator (https://github.com/gchudnov/w3c-css).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants