jaspr icon indicating copy to clipboard operation
jaspr copied to clipboard

Styling in jaspr

Open schultek opened this issue 2 years ago • 4 comments

When looking at #13 I very much liked the concept of a Style class which bundles css properties in a simple way. The advantages of such a class over a raw Map<String, String are really good:

  • strongly typed properties
    • instead of everything being a String, you can have types for numbers, enums, units or custom classes for things like Color
  • IDE support
    • you get hints which values a property can have and how to construct them
    • properties can be documented and viewed from inside the IDE (no googling css properties)
    • e.g. for padding having the EdgeInsets class with separate constructors for .only, .symmetric or .all translates well to the different shorthand versions
  • you only write dart code instead of having to write css
    • this is especially useful for flutter devs or others that are not fluid in css

Proposal / Discussion

When implementing such a class, we need to make sure that it is easy to use and understand. It should be possible to compose, change, and combine styles in an easy way.

Grouping Styles

Having a single Style class with all implemented properties as fields will get way to large very quickly. This negates the positive effect of having IDE support, since you get a very long and convoluted list of properties. I like the way how #13 groups semantically related styles together into sub-classes, like TextStyle, BackgroundStyle or BoxStyle.

In the pr, combining those style groups is done through the MultipleStyle class, which takes a list of BaseStyles. I don't really like this approach since it introduces an additional class (or two with the BaseStyle class) just to allow for settings styles from different semantic groups. Also I think this is not ideal since the user could e.g. put two TextStyles which would not make sense. Also the user does not have help from the IDE in which style groups exist and can be used.


Instead an approach would be to have the Style class accept instances of the grouped sub-classes, something like:

var style = Style(
  text: TextStyle(...),
  background: BackgroundStyle(...),
  box: BoxStyle(...),
);

A disadvantage of this approach is that it is not extensible by the user, while with the MultipleStyle the user could create an own subclass of the BaseStyle class to be passed to the styling list. Also it lacks a way of providing raw styles in order to account for properties that are missing from the existing style classes (could be solved with an additional Map<String, String> raw property).


Another approach could be to use a builder-like pattern to compose styles. Instead or providing style properties through the constructor, there could be methods on the Style class to incrementally compose a style:

var style = Style()
  .text(...)
  .background(...)
  .box(...)
  .raw(...);

Internally this would probably just keep a Map<String, String> instead of keeping instances of separate classes.

This would allow for easy extension by the user through darts extension methods. It works also great with IDE code completion. A disadvantage would be that this would not allow to use styles as a const variable, which might be desirable when you have a set of standard styles you want to use in your app. It's also not very "Fluttery".

Modifying / Composing Styles

With the first approach, in order to modify the style instance the Style class as well as all sub-classes would have to implement a copyWith method. Again this isn't really extensible by the user and double the work to maintain. Merging two styles would also not be easy and requires to merge all sub-classes individually.

The second approach would be modifiable by default, since you could just call e.g. the .text() method again which would override any existing text-styles. It would also be easier to merge two styles by just merging the internal Maps of both.

Both approaches would still be immutable. With "modifying" I mean returning a new instance with adjusted styles, not mutating the existing instance.

A third way

While I like the second approach more because of its advantages, it does not feel very "Fluttery". So I would like to explore a third option combining all the above.

There could be separate sub-classes for semantic style groups, but those are private and hidden behind named constructors of the actual Style class. This would then look like Style.text(...), Style.background(...), Style.raw(...), etc. for the user, which has IDE support for code completion. The user could then still create a custom sub-class extending Style.

Combining styles could be done through a Style.combine(Iterable<Style> styles) constructor, removing the need for a separate class to combine multiple styles. Modifying a style would be done through the same method, by combining the original style with a new style, overriding the existing properties.

Together this would look like:

var backgroundStyle = Style.background(...);

var style = Style.combine([
  backgroundStyle,
  Style.text(...),
  Style.box(...),
  Style.raw({'color': 'blue'}),
  MyCustomStyle(),
]);

A component (e.g. 'DomComponent') would then accept a single Style style property.

schultek avatar Jul 14 '22 08:07 schultek

Hi @schultek, Dawn's maintainer here.

I also considered strongly typing styles in Dawn.

The problem is, it needs a huge amount of time and effort to implement the current CSS spec in a strongly typed Dart syntax. Here are some situations that a strongly-typed CSS causes some setbacks:

  • Take into account all the CSS properties that have multiple syntaxes and rules at the same time (background, grid-template, etc.).
  • In a strongly-typed CSS, variables would have no place. They can be useful in theming. But this problem can somehow be solved through InheritedWidget and BuildContext.

The next issue is keeping up with the continuously evolving HTML & CSS specs and adding them to our implementation.

So my decision for Dawn in general was to:

  • Use Dart instead of JavaScript
  • Provide some basic and necessary widgets such as Text, Container, Image, Video, Audio, Input, and Textbox instead of HTML
  • Keep CSS styling as it is but as a parameter for the basic widgets.

At last, I think it's better to make a VSCode extension to analyze and highlight Style({...}) keys and values using VSCode's CSS Language Service (an extension like React and Vue's styled components that analyzes tagged string literals).

Hawmex avatar Jul 14 '22 19:07 Hawmex

@schultek Yes I agree with you combining and grouping styles with MultipleStyle is not very nice. But I didn't know any better way during the implementation of it.. :-)

I like your third way idea with combining styles by using only one Style class. I try implement it in my PR when I will have some time ;-)

mjablecnik avatar Jul 14 '22 20:07 mjablecnik

@Hawmex Thanks for the feedback, really appreciate it.

I too thought about the effort that it takes to both initially implement it and maintain it over time. It is impossible to achieve close to 100% support for the complete css spec.

That's why it is important to me that such an implementation would allow the user to extend / fix it with their own styles and also always allow to fallback to raw css styles. I think this way we can have the advantages of a Style class in dart for the most common css properties while avoiding maintenance hell.

@mjablecnik I'm also happy to work on this, then you can focus on the components + bootstrap part. I have also some more stuff regarding styles I want to try out anyways that I didn't put into the proposal for now.

schultek avatar Jul 14 '22 20:07 schultek

Thought I'd share some info on styling for the community and perhaps this could later be added as a wiki page, currently my project is setup to use the islands template but there's no styles.css file in the web/ directory and so the styles need to be added when defining Document.islands(...).

What we want to produce is the same webpage as what is shown for in JasprPad: https://jasprpad.schultek.de/?sample=bulma

All of the files and components needed for Bulma can be found here: https://github.com/schultek/jaspr/tree/main/packages/jaspr_pad/samples/bulma

You will see that the styles.css file is included there but this doesn't work when running the islands template project structure. The contents of that file is shown below.

@import "https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css";

html, body {
  display: flex;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  height: 100%;
  font-family: sans-serif;
}

body {
  padding: 16px;
}

This is how you can have the same within your Dart code

// root component used to set up the document in 'islands' mode
class MyDocument extends StatelessComponent {
  const MyDocument({Key? key}) : super(key: key);

  @override
  Iterable<Component> build(BuildContext context) sync* {
    // scaffolds the main document (<html>, <head>, <body>)
    // and selects the 'islands' mode
    // - only specified **Island** components will be compiled as part of the javascript bundle and
    //   hydrated on the client
    // - all client-side code will be auto-generated inside the /web directory
    yield Document.islands(
      title: 'Islands',
      styles: [
        StyleRule.import(
          'https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css',
        ),
        StyleRule.import(
          'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css',
        ),
        StyleRule(
          selector: const Selector.list([
            Selector.tag('html'),
            Selector.tag('body'),
          ]),
          styles: Styles.raw({
            'display': 'flex',
            'flex-flow': 'column',
            'justify-content': 'center',
            'align-items': 'center',
            'height': '100%',
            'font-family': 'sans-serif',
          }),
        ),
        StyleRule(
          selector: const Selector.list([
            Selector.tag('body'),
          ]),
          styles: Styles.raw({
            'padding': '16px',
          }),
        ),
      ],
      // renders the [AppBulma] component inside the <body>
      body: AppBulma(),
    );
  }
}

Hope this helps! 🤞

MarkOSullivan94 avatar Sep 14 '22 03:09 MarkOSullivan94