linkedin / css-blocks

High performance, maintainable stylesheets.
http://css-blocks.com/
BSD 2-Clause "Simplified" License
6.33k stars 152 forks source link

Feat: Multiple Inheritance #212

Open robert-j-webb opened 5 years ago

robert-j-webb commented 5 years ago

Hi 👋!

I think this project is really interesting and I was wondering, why is it that I can only extend a block once? I'm in a situation where I want to import 2 blocks.

@block-reference buttons from "../../styles/blocks/buttons.block.css";
@block-reference loading from "../../styles/blocks/loading.block.css";

:scope {
  extends: buttons;
  extends: loading;
}
...

Generates Error:

{ Error: [css-blocks] BlockSyntaxError: A block can only be extended once. (tmp/broccoli_css_blocks_analyze-output_path-Co5Mjhle.tmp/src/ui/components/.../stylesheet.css:6:3)

I think the guide may be a bit confusing because I just want to use classes from 2 different files in one file and I really don't know any way of doing that.

Here's my package.json (the relevant parts [I think?]):

    "@css-blocks/ember-cli": "^0.20.0-beta.3",
    "@glimmer/application": "^0.9.1",
artem-v-shamsutdinov commented 5 years ago

Hi,

I'm just a beginner (and have temporarily abandoned css-blocks, after trying to get it to work for 3 days), but since I think the entire dev team is on vacation (or is otherwise unable to answer) I try to put forward a theory:

If you are simply trying to use classes from other blocks (in a given component) then please take a look at:

https://github.com/linkedin/css-blocks#composition-in-templates

Otherwise it just back to the old question of multiple-inheritance vs single inheritance (meaning inheritance of implementation). Last I checked (which was a while back), single inheritance won pretty much in every modern high-use language (for various good reasons, mostly is easier to reason about). Of course those languages also support multiple interface inheritance and that also appears to be true in css-blocks (though I haven't tried):

https://github.com/linkedin/css-blocks#block-implementation

Thanks, :)

robert-j-webb commented 5 years ago

Thanks for the help @russoturisto -- unfortunately that's not really what I'm looking for. I did know about that, but I simply want to have a couple of importable stylesheets that I can use in multiple places when I need them. I think that the point about multiple inheritance versus single inheritance doesn't really apply to CSS because of how it's always flat and much more simply statically analyzed than an equivalent java class for example.

amiller-gh commented 5 years ago

Hey @robert-j-webb, I believe the reason is because multiple inheritance makes everything trickier and we wanted to get v1 of CSS Blocks out!

There are three ways afaik to resolve the "diamond inheritance" naming conflicts that inevitably come up when you discuss multiple inheritance. Either:

  1. Make order matter, effectively providing syntactic sugar over prototypical inheritance concepts (a la Python);
  2. Require a scope resolution syntax for ambiguous member access (a la C++), or;
  3. Require explicit resolution / renaming in the inheriting class definition (a la Eiffel).

(Fun fact, we take approach number three when it comes to property conflicts when composing multiple Block styles in the DOM, where any given element may "inherit" the styles from N number of Block styles.)

But for the case of Block drafting, we also want to accommodate for the audience we're targeting. Most engineers using CSS Blocks will primarily have a JavaScript background and may be fairly unfamiliar with some of the pitfalls of multiple inheritance. Because of this, and because of the time it would take to implement well, it seemed easier at the time to just adopt the same prototypical inheritance model as JavaScript for CSS Blocks v1.

That being said, I can see utility in taking an approach like Python where its linearized multiple inheritance tree is basically just syntactic sugar over prototypical inheritance concepts. I seem to remember we discussed the possibility of layering in multiple inheritance later on if we decided it was prudent. Its easier to add features later than to miss the mark early and re-design them after launch! I'm interested in exploring this again now – I'd like to make sure we take the time to design it well first though.

@chriseppstein thoughts on evaluating MI as a language feature?

chriseppstein commented 5 years ago

In the very near term, we're going to add a block syntax for composing styles. Once we have that, you'd be able to have a block extend one of the several blocks and then implements the others. The styles for the implemented blocks would require some boilerplate code, but the compiler will yell at you if there's any unimplemented styles.

Example:

@block-reference buttons from "../../styles/blocks/buttons.block.css";
@block-reference loading from "../../styles/blocks/loading.block.css";

:scope {
  extends: buttons;
  implements: loading;
  @compose loading:scope;
}

.spinner {
  @compose loading.spinner;
}
.spinner[state|paused] {
  @compose loading.spinner[state|paused];
}

Because our composition is modeled as style inheritance + resolution order, there's very little functional difference between this and multiple inheritance. As @amiller-gh noted, it's really just about making sure the developers understand how naming conflicts are handled. As adam notes, how to resolve conflicts when a name is being merged from two implementations is one tricky aspect.

But the other tricky aspect is if those names have different semantics and you want to have a single block that is both of them. For instance, in this case, if both blocks have an active state on the :scope element, those have very different meanings. For a spinner, it probably means it's spinning and for a button it probably means it's being pressed and the lifecycle of those states isn't linked. If this block is to be both of these things, it probably doesn't make sense to have the active state shared in the block that unifies them. This problem exists for both extends and implements. In the case of this type of naming collision, I think we'll want to introduce a way to rename a style in the sub-block so that if it's invoked by the base context it picks the right name and any style overrides in the sub-block.

Of course, as @russoturisto mentioned above, the design for template composition currently handles this very well. I know it's annoying to use two classes instead of one, but it does keep things simpler. 🤔

robert-j-webb commented 5 years ago

Hmm this is all very interesting - I didn't think I would consider multiple inheritance models when thinking about building CSS :laughing:

All I wanted was the import from SCSS, which I understand as concatenation of source files together. Basically there's this pattern that's very common where there will be a root stylesheet that contains classes that are used universally, as well as keyframes for animations, etc. Now, in the beginning, this might just be one SCSS file, but then you have mixing of different kinds of SCSS and it gets unwieldly, so devs will opt to split it up into many files, like animations.scss, blocks.scss, etc. and then import all of them into a main.scss. Generally people resolved naming collisions that may occur with a prefix like main__ or something.

I don't think I could do that with extends! Excited to see the update.

chriseppstein commented 5 years ago

@robert-j-webb yeah, naming conflicts are the exception, rather than the rule, but not ever having to worry about them is one of css-blocks' best features.