Closed rebo closed 4 years ago
Just a few related projects for inspiration if you don't know them yet:
As you can see in the Quick Peek above, my current solution allows for CSS styles to be declared and used within a seed view function. Here it is again:
fn styled_button() -> Node<Msg> {
let style = use_style(r#"{
color:{{highlight||white}};
background-color:{{primary||#3369ff}};
text-align:center;
position:relative;
}
&:active{
background-color:{{muted||#0049dd}}}
}"#);
button![ style, "Click Me!" ]
}
The use_style()
function takes the CSS string, assigns it a unique identifier injects the CSS under that identifier class name into the html <head>
. In this way a specific component can be linked to a specific style.
This has the advantage of allowing styles to be declared at the same location as the view is constructed. Whilst at the same time using bog standard CSS.
Any CSS can be used here including all kinds of pseudo selectors etc. Currently the & identifier is used in a similar fashion to Sass/Less in order to refer to the component root selector. This allows &:hover
pseudo selectors to work.
You will also see lines such as
color:{{highlight||white}};
This shows the inbuilt theming, if the highlight
color-style is available for the current theme then it is used, otherwise it defaults to white. This aspect is completely optional and theming does not need to be used.
As it stands the unique class names created are effectively hashes and therefore not human readable. However using use_named_style()
will allow a named class to be used.
let style = use_named_styles( "call_to_action_button", r#"{ color: ...}"#);
Currently what I have works in accordance with the Theme Specification which is used by a number of CSS frameworks.
The principle is that a "Theme object" is provided which is them used to style specific properties. For instance a theme objects .colors
field will contain several definitions of colours, such as "primary", "highlight" and "muted" that will be common to all UI on a page. This will then be accessible in anything colour related in the CSS.
For instance,
background-color : {{primary|| lightblue}};
primary
points to the primary colour within the theme objects colour field. If that style is not available lightblue is used instead.
You may be thinking that this theme object would have to be passed around all the view functions and this might be annoying. Not so! Currently the following works.
use_theme( dark_theme , || div![
p![
span![ themed_button() ]
]
])
as long as themed_button()
is called from within a use_theme()
block it will have access to dark_theme
.
It even works across multiple nested functions:
use_theme( dark_theme , || div![
p![
span![ display_form() ]
]
])
...
...
fn display_form -> Node<Msg> {
div![
p![
...
// yes, even here, themed_button will still be able to access `dark_theme`
themed_button()
]
]
}
This means you could surround your entire main view inside a use_theme()
block and every thing can be themed from a single location:
fn main_view(model: &Model) -> Node<Msg> {
let theme = if model.use_dark_theme {dark_theme} else {light_theme};
use_theme(|| ...view nodes here... )
}
The advantage of css in view functions is that it can be generated dynamically. This means that if you are creating a styled form you can provide options for specific colours in the signature:
fn styled_form(data: &Data, form_bg_color: &str) {
let style = use_style(|| format!("background-color: {}", form_bg_color);
...
}
or give the use the option of completely changing the css:
fn styled_form(data: &Data, css: Option<&str>) {
let css = css.unwrap_or( "{ // default_css }");
let style = use_style(|| css ) ;
...
}
Or even more crazy, bind the css argument to an At
attribute on a html node. You then will have an attribute on all your styled nodes that lets you edit and perfect the style in the browser! No more recompilation required.
fn styled_card(data: &Data) {
let css = use_state(||"//default css");
div![bind(At::Custom("data-css"), css), ...]
}
then in your html via a browser (maybe only in debug mode):
<div id="main_card" data-css="...edit there here to update the css of this div">...</div>
Well there is loads more advanced use cases and options but this just gives you a flavour of the styling possibilities.
After a discussion on discord about variants. We have come up with the below api for variants. Thanks to Macocha for the advice.
In styled systems Variants are styled fragments that can be applied to a component. The idea is that a component can be designed then the use of the compent can select from a number of standard style variants.
For instance a button component may have a Danger
style and an Action
style. Instead of writing two different components one can have a single component but give the style choice of which variant.
Anyway here is the rough api:
enum Buttons {
Danger,
CallToAction
}
fn button_with_variants(variant: ButtonVariant ) -> Node<Msg>{
// common styles
let style = use_style(r#"{
padding: 8px;
margin: 4px;
}
"#);
// style variants
match variant {
Buttons::Danger => extend_style(style,
r#"{color: white;
background-color: red;}"#
),
Buttons::CallToAction => extend_style(style,
r#"{color: black;
background-color: green;}"#
)
}
div![style,"Click Me!"]
}
With the addition of "prop" typed styles we now have the makings of a very versatile system. All currently implemented and working as part of Seed hooks.
fn my_button()-> Node<Msg> {
let style = use_css(r#"
border-width: 2px;
color: white;
background-color: red;
"#);
button![style,"Click Me!"]
}
I think I will be dropping the theming support in css text to prevent this morphing into its own template language.
fn my_button_typed()-> Node<Msg> {
let style = use_style(&[
Style::with()
.color("white")
.border_width(2px)
.background_color("red")
.padding("4px")
]);
button![style,"Click Me!"]
}
fn view(_model: &Model) -> Node<Msg> {
use_theme(Theme::dark(), ||
my_button_theme_typed
)
}
fn my_button_theme_typed()-> Node<Msg> {
let style = use_style(&[
Style::with()
.color("white")
.border_width(2px)
.background_color(Th("primary","red")) // named theme alias
.padding(ThIdx(2,"2px")) // index into the second `space` scale
]);
button![style,"Click Me!"]
}
fn my_button_typed_short()-> Node<Msg> {
let style = use_style(&[
Style::with()
.color("white")
.bg_color("red")
.px("24px")
.py("8px")
.m(ThIdx(1,"1px"))
]);
button![style,"Click Me!"]
}
fn my_button_typed_tw()-> Node<Msg> {
let style = use_style(&[
Style::with()
.text_white()
.bg_red_400()
.p_4()
.inline_block()
]);
button![style,"Click Me!"]
}
fn my_button_theme_typed()-> Node<Msg> {
let style = use_style(&[
Style::with()
.color("white")
.background_color("red"),
Style::hover()
.color("red")
.background_color("white")
]);
button![style,"Click Me!"]
}
fn my_button_extend()-> Node<Msg> {
let style = use_css(r#"
border-width: 2px;
color: white;
background-color: red;
"#).extend(
&[Style::with()
.padding("4px")
.margin("10px")]
);
button![style,"Click Me!"]
}
fn view() -> Node<Msg>{
button_variant(Button::Danger)
}
...
...
fn button_variant(variant: Buttons ) -> Node<Msg>{
let style= use_css(r#"{
padding: 8px;
margin: 4px;
outline: none;
border-radius: 4px;
}
{{self}}:focus{
outline:none;
}
"#);
let style = match variant {
Buttons::Danger => style.extend(
r#"{
color:white;
background-color: red;
}
{{self}}:active{
background-color:darkred;
}
"#
),
Buttons::CallToAction => style.extend(
&[ Style::with()
.color("black")
.background-color("lightgreen"),
Style::active()
.background-color("darkgreen"),
])
};
button![style,"Click Me!"]
}
&[S]
"prop":fn view(_model: &Model) -> Node<Msg> {
use_theme(Theme::dark(), ||
button_styled_with_args(&[
Style::with()
.color("white")
.bg_color(Th("primary", "#DB3080"))
.b_width("2px")
.b_color(Th("primary", "#DB3080"))
.radius("3px")
.p("24px")
.m("8px")
.inline_block()
.transition("background-color 300ms, color 300ms"),
Style::hover()
.color("white")
.bg_color(Th("primary_light", "palevioletred"))
])
)
}
...
fn button_styled_with_args(styles: &[Style]) -> Node<Msg> {
let style = use_style(r#"
{
display: inline-block;
text-align: center;
}
"#).extend(styles);
div![style, "Hello"]
}
fn main_view() -> Node<Msg> {
let shared_style = Style::with().color("white")
.bg_color("red")
.px("24px")
.py("8px")
.m(ThIdx(1, "2px"));
div![
element_a_with_style(shared_style),
element_b_with_style(shared_style),
element_c_with_style(shared_style),
element_d_with_style(shared_style),
]
}
**Completely 'inline' ***
fn other_view()->Node<Msg> {
button![
use_style(Style::with().text_white().bg_color_red().px_4().py_2()),
"Click Me"]
}
@rebo this is awesome!!! I'm finally going to be able to create a MaterialUI Seed component lib to replace my production JS app!!!
Glad you like it @thedodd I've been able to tidy it up a far bit after feedback from @MartinKavik so hang tight for an even more clear api.
I will be tidying up my code and uploading soon for people to test with.
Sneak peak of api improvements.
Hopefully it's super clean now:
fn simple_button() -> Node<Msg> {
button![
S.padding("2px").margin("3px").background_color("red"),
"Click Me"
]
}
An example using tailwind style methods and use of variants:
enum Shade {
Light,
Dark,
Normal,
}
fn modern_button(variant: Shade) -> Node<Msg> {
let base_style = S.p_4().m_2()
.bg_red_600().text_gray_200()
.radius("4px").inline_block();
let style = match variant {
Shade::Dark => base_style.bg_red_800().text_gray_400(),
Shade::Light => base_style.bg_red_400().text_gray_100(),
Shade::Normal => base_style,
};
button![style, "Click Me!"]
}
As you can see variants can be created simply by adding new rules to a base style.
Of course CSS style definitions work via css method:
fn simple_css_button() -> Node<Msg> {
let style = S.css(r#"
padding: 8px;
margin: 4px;
outline: none;
border-radius: 4px;
"#);
button![ style, "Click Me"]
}
Closing in favour of #413 .
Quick Peek:
I have been spending quite a bit of time lately working with various styling options within seed with a view to creating re-usable styled components. Currently options have several problems with creating re-usable views. Something which I have been working on with the existing experimental Seed hooks.
Existing options:
style!
tags.(+) - styles can be used inline directly within Seed and within shared components, styles are typed with
St
enum. (-) - lots and lots, primarily they fall outside of usual CSS styling which means pseudo selectors such as:hover
andfocus
do not work. They override class based styling which means composition with anything is a nightmare. Basically unusable for any large projects.(+) Proven old school solution, tags can have semantic classes and therefore style appropriately. (-) Styles cannot be used within components directly or within shared components, an external stylesheet is required. CSS needs to be in an external file which makes distribution and use awkward. Probable style clashes over clashing uses of class names.
(+) Actually pretty good, many of the solutions are super well developed and relatively easy to use. (-) Ties any developed Seed component directly to a specific framework. This greatly reduces the potential user of a Seed re-usable component or int he future a Seed UI system.
Therefore we need a solution that achieves a goal of an effective styling system for components whilst still giving developers as much freedom as possible.
My goals were to re-implement an existing javascript solution in Seed. With the following objectives
For inspiration I looked at https://styled-components.com/ , https://theme-ui.com/, https://emotion.sh/docs/introduction and https://xstyled.dev/.
This video is informative about the general principle for these frameworks.
https://www.youtube.com/watch?v=BkgU_-KGK9w
In my following post I will be explaining my current solution and how it can meet the above objectives.