jaspr
jaspr copied to clipboard
Styling in jaspr
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 likeColor
- instead of everything being a
- 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 theEdgeInsets
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 BaseStyle
s. 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 TextStyle
s 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 Map
s 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.
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
andBuildContext
.
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
, andTextbox
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).
@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 ;-)
@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.
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! 🤞