Tutorial: Scoped Styles

This tutorial follows the quick start guide on scoped styles.

Scoped styles are styles specific to a component, and are automatically applied to that component whenever it's used. Scoped styles are great for keeping things consistent and easy to manage, because they make sure a component's style is only applied to that component.

Setup

The tutorial will use the same setup as the first project tutorial. Basically, we create an empty folder, and then use lvweb inside the folder to generate a starting project:

mkdir routes
cd routes
npm i live-elements-web-cli
npx lvweb init
npm i
lvweb serve

To check, we can go to localhost:8080, and we should see the following text:

Welcome to Live Elements

From here we can start building with scoped style components.

Using scoped style components

One of the biggest advantages of components with scoped styles is that they are ready to be used, since they provide a starting default style that makes the component functional. The live-web-view module has a large collection of these components.

For example, ColLayout and Col are part of live-web-view.layout module, and are used to create a column layout. Let's see how they work. We can add them to app/Home.lv:

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import live-web-view.layout

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    Body{
        ColLayout{
            Col{
                H1`Column 1`
            }
            Col{
                H1`Column 2`
            }
        }
    }
}

If we refresh the page at localhost:8080 we will see the newly added text, but won't see the actual layout split into columns. This is because the server is unaware of the additional styles included in our application. In order for the server to know about our styles, we need to define a use property inside the PageView component:

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import live-web-view.layout

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    static any[] use = [
        ColLayout
    ]

    Body{
        ColLayout{
            Col{
                H1`Column 1`
            }
            Col{
                H1`Column 2`
            }
        }
    }
}

The useproperty is a static property (we do this so the server doesn't have to create the component) that lets the server know what components with scoped styles are being used on the page. The server will collect these components together with their styles, and generate a stylesheet that includes styles for all the collected components. Notice we also took out the global stylesheet. The server will link the necessary styles to this component automatically. To see the new changes we need to restart the server:

npx lvweb serve

Refreshing the page, we should see the 2 column layout now working.

Let's add a form. live-web-view.form contains an already styled form component:

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import live-web-view.layout
import live-web-view.form

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    static any[] use = [
        ColLayout,
        FormContainer
    ]

    Body{
        ColLayout{
            Col{
                FormContainer{
                    FormGroup{
                        TextInput{ type='email' placeholder="Email Address" name="email" }
                    FormGroup{
                    }
                        TextInput{ type='password' placeholder="Password" name="password" }
                    }
                    FormGroup{
                        SubmitButton{ T`Submit` }
                    }
                }
            }
            Col{
                H1`Column 2`
            }
        }
    }
}

FormContainer applies a style not only to itself, but also to all of it's children. This is why children like TextInput will have a different style inside FormContainer.

Creating a component with scoped styles

The easiest way to understand how components with scoped styles work is to create actually create one. Let's create a component that displays information about the form we have created. The column layout can be used to show the form in the right column, and information about the form in the left column. We can call our component FormInformation. We'll create it inside a new folder in the root of the project called views.

Let's create the folder in the root of our project:

mkdir views

And then create a file views/FormInformation.lv:

import live-web.dom
import live-elements-web-server.style

component FormInformation < Div{

    classes: [ScopedStyle.className(FormInformation)]

    H1`Login Form`
    P`You can use this form to login. Provide a valid email address and password in order to login to the
    system.`
}

We extend a Div container, then add a heading (H1) and pararaph (P). We also set the class for the component, which is assigned using ScopedStyle.className(FormInformation), a value that ScopedStyle will capture from the server.

FormInformation will also need a css file for it's styles. We can create views/forminformation.css file next to it:

&FormInformation{
    padding: 10px;
    color: #444;
}
&FormInformation h1{
    margin: 0;
    font-weight: bold;
    text-align: left;
    font-family: sans-serif;
}
&FormInformation p{
    margin: 0;
    text-align: left;
    font-family: sans-serif;
}

&FormInformation is a special syntax used by this file in order to reference any FormInformation component. Inside FormInformation we style the heading and paragraph components.

We can also omit &FormInformation from p and h1 since it will be prefixed automatically to all elements in this stylesheet:

&FormInformation{
    padding: 10px;
    color: #444;
}
h1{
    margin: 0;
    font-weight: bold;
    text-align: left;
    font-family: sans-serif;
}
p{
    margin: 0;
    text-align: left;
    font-family: sans-serif;
}

Even though &FormInformation selector has been left out from p and h1, it will be inserted automatically by the server when parsing this file, therefore not conflicting with other p and h1 elements in our page.

We now need to let the server know that FormInformation component will use the css file we created. We can do this through the static use property:

import live-web.dom
import live-elements-web-server.style

component FormInformation < Div{

    static any[] use = [ScopedStyle{ src: './forminformation.css' }]

    classes: [ScopedStyle.className(FormInformation)]

    H1`Login Form`
    P`You can use this form to login. Provide a valid email address and password in order to login to the
    system.`
}

The src property from ScopedStyle sets a relative path to the forminformation.css from the path to FormInformation component.

Now we can use FormInformation inside the main page just like we did FormContainer:

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import live-web-view.layout
import live-web-view.form
import .views

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    static any[] use = [
        ColLayout,
        FormContainer,
        FormInformation
    ]

    Body{
        ColLayout{
            Col{
                FormContainer{
                    FormGroup{
                        TextInput{ type='email' placeholder='Email Address' name='email' }
                    }
                    FormGroup{
                        TextInput{ type='password' placeholder='Password' name='password' }
                    }
                    FormGroup{
                        SubmitButton{ T`Submit` }
                    }
                }
            }
            Col{
                FormInformation{}
            }
        }
    }
}

Using css processors

ScopedStyle supports custom processors to parse its css file. For example, components in live-web-view package use ScopedStyleswith tailwind css processor in order to use tailwind classes in css with the @apply directive.

ScopedStyle has a property called process which accepts a link to a component that will do the processing.Tailwind processor is for example defined in live-web-view/style/CSSProcessor.lv. So a ScopedStyle using this processor would point the process property to that location:

ScopedStyle{ 
    process: 'live-web-view/style/CSSProcessor.lv'
}

To better understand this, let's use the tailwind processor in FormInformation component. We need to do 2 things. Configure the process property and include the global tailwind stylesheet file. The global tailwind stylesheet configures the main tailwind directives required for tailwind to work:

import live-web.dom
import live-elements-web-server.style

component FormInformation < Div{

    static any[] use = [
        ScopedStyle{ src: './forminformation.css' process: 'live-web-view/style/CSSProcessor.lv' }, 
        ScopedStyle{ src: 'live-web-view/style/global.css' process: 'live-web-view/style/CSSProcessor.lv' } 
    ]

    classes: [ScopedStyle.className(FormInformation)]

    H1`Login Form`
    P`You can use this form to login. Provide a valid email address and password in order to login to the
    system.`
}

Although this stylesheet is included for every component that uses this processor, the server makes sure to remove any duplicates and include it only once.

In forminformation.css file, we can now use the @apply directive:

&FormInformation{
    @apply p-4 text-[#444444];
}
h1{
    @apply m-0 font-bold text-left font-sans;
}
p{
    @apply m-0 text-left font-sans;
}

Nesting component styles

Components with scoped styles can be nested within other components. This allows you to consolidate multiple components into a single one that contains all the others, simplifying the project's structure and making it easier to manage. The main page for example now uses FormContainerColLayout and FormInformation. We can wrap all of these inside a single reusable component.Let's create a wrapper component called DescriptiveForm. The component will inherit from ColLayout. We will put it inside views/DescriptiveForm.lv:

import live-web.dom
import live-web-view.layout
import live-web-view.form
import live-elements-web-server.style

component DescriptiveForm < ColLayout{

    static any[] use = [
        FormContainer,
        FormInformation
    ]

    classes: [ScopedStyle.className(DescriptiveForm)]

    Col{
        FormContainer{
            FormGroup{
                TextInput{ type='email' placeholder='Email Address' name='email' }
            }
            FormGroup{
                TextInput{ type='password' placeholder='Password' name='password' }
            }
            FormGroup{
                SubmitButton{ T`Submit` }
            }
        }
    }
    Col{
        FormInformation{}
    }
}

Since we're inheriting from ColLayout we don't need to include it in the use property. FormDescription and FormInformation however have to be declared.

Now, the main page can simply use the DescriptiveForm, and FormInformation and the rest will be included automatically:

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import .views

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    static any[] use = [
        DescriptiveForm
    ]

    Body{
        DescriptiveForm{}
    }
}

We can also overwrite styles in DescriptiveForm for all its used components. Let's create views/descriptiveform.css and reference it in the use property:

import live-web.dom
import live-web-view.layout
import live-web-view.form
import live-elements-web-server.style

component DescriptiveForm < ColLayout{

    static any[] use = [
        FormContainer,
        FormInformation,
        ScopedStyle{ src: './descriptiveform.css' }
    ]

    // ...
}

In descriptiveform.css, we can select any component used by DescriptiveForm:

&DescriptiveForm{
    padding: 10px;
}

&DescriptiveForm &Col{
    border: 1px solid #ccc;
}

The selectors will automatically be converted by the server to match the components. We can refresh the page to see the final result.

Styling from PageView

The same way we used css component selectors from descriptiveform.css we can also use them from PageView and apply the selectors to any component used by PageView. To use these types of selectors, we need to change the way the stylesheet file is referenced by PageView, and add it to the use property instead.

import live-web.dom
import live-elements-web-server.view
import live-elements-web-server.style
import .views

component Home < PageView{

    head: PageProperties{ title: "Live Elements" }

    static any[] use = [
        DescriptiveForm,
        ScopedStyle{ src: '../styles/style.css' }
    ]

    Body{
        DescriptiveForm{}
    }
}

Now, component selectors will work inside style.css file:

html, body{
  width: 100%;
  height: 100%;
}
&DescriptiveForm{
  height: 100%;
}
&DescriptiveForm &Col{
  height: 100%;
}

We can also remove the Stylesheet from the bundle file, as the server handles this file automatically now.

Conclusion

By using scoped styles inside components, you can create components that can be reused throghout your application, creating a more modular and maintainable code. By isolating the style of each component, you make sure that changes to that component do not inadvertently affect other parts of the application.