Thursday 5 November 2020

PyBloom coding project part 11: Styling the web pages

We're in the home stretch now. Here in part 11, I go through how I styled the web pages for PyBloom using a modern CSS framework. 

The web pages

In the previous section, we’ve seen how to build the structure of the web app in Flask. After creating the app package, we then need to work on the HTML of the page. Finally, we’ll need to add the CSS that styles the page. To do this, the web page is written in HTML5 and CSS3, but with two scripting languages to simplify: Jinja2 for the HTML rendered by Flask, and Bootstrap CSS. 


I haven’t discussed CSS yet, so let’s take a detour there. I’m a big fan of frameworks and not bothering to recreate (learn again) from first principles. The most comprehensive framework that I’ve come across for CSS is Bootstrap from Twitter. It takes care of making the page responsive, mobile-friendly with predefined styles, without the need to create the CSS from scratch. It’s a scripting language, so there is a need to learn its vocabulary which is invoked in the HTML attributes.


The structure of the HTML makes use of inheritance. There’s a base.html that declares all the common elements, such as declaring how the Bootstrap components are downloaded from the CDN, and also the navigation elements that are common for all pages. Each subsequent page extends this base.html page, to add the actual content and other custom CSS.


Technologies

  • HTML5, CSS3

  • Jinja2

  • Bootstrap CSS

The code

base.html - head 


<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="UTF-8">


So far so familiar. All HTML starts with these tags.


<meta name="viewport" content="width=device-width, initial-scale=1.0">


This is the first tag required by Bootstrap.


“Bootstrap is developed mobile first, a strategy in which we optimize code for mobile devices first and then scale up components as necessary using CSS media queries. To ensure proper rendering and touch zooming for all devices, add the responsive viewport meta tag to your <head>.”


{% if title %}

<title>{{ title }} - PyBloom</title>

{% else %}

<title>Welcome to PyBloom</title>

{% endif %}


Next we have a bit of Jinja2 logic to define the title. All this is saying is: if the variable title is passed into the HTML according to the routes.py then the title is put into the metadata. If not, a default title is used. To be honest, I probably won’t invoke this logic, but it’s there if needed in the future.


<!-- Bootstrap v5 CSS CDN -->

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha2/css/bootstrap.min.css" integrity="sha384-DhY6onE6f3zzKbjUPRc2hOzGAdEf4/Dz+WJwBvEYL/lkkIsI3ihufq9hk9K4lVoK" crossorigin="anonymous">


<!-- Bootstrap JavaScript Bundle with Popper.js -->

<script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha2/js/bootstrap.bundle.min.js" integrity="sha384-BOsAfwzjNJHrJ8cZidOg56tcQWfp6y72vEJ8xQ9w6Quywb24iOsW913URv1IS4GD" crossorigin="anonymous"></script>


These tags are needed in order to download Bootstrap from the CDN, avoiding the need to download potentially out of date components. These first <link> and <script> tags specify the version of Bootstrap CSS and Bootstrap JavaScript to be downloaded from the CDN. I’m using Bootstrap v5 which is in alpha at time of writing. I’ll clearly need to update this to the release version at some point (and hope that nothing breaks in the meantime).


I thought of defining these URLs in my rudimentary CMS, but decided against it because 1) sometimes Bootstrap classes only work with certain versions (e.g. the naming is not backwards compatible across major versions) so it’s better to reference a fixed snapshot; and 2) the complete statement is quite complex, so I can eliminate transposing errors by copying directly from the Bootstrap documentation. 


{% block app_css %}{% endblock %}


This little bit of Jinja2 specifies a block where the custom CSS needed later will go.


<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">

</head>


Every website also displays a little icon in the top left of the browser tab. This last link is a combination of HTML and the very useful Jinja2 function url_for() to tell the browser where to locate this icon file. This function concatenates the absolute file path for the static folder with the filename, and inserts into the tag href attribute. We’ll use it a lot in the HTML, as it avoids hard coding file paths very nicely.


And that’s it for the head, so we close with the </head> tag.


base.html - body


The body HTML is where the Bootstrap scripting goes. The common element across the top of all of my pages will be the navigation bar. Let’s get into it!


<body>

  <div class="container-fluid">


The container model is how Bootstrap (and all CSS) describes the elements within a web page. Think of a container as a box that has a margin, border, padding, and finally the content in the centre. By declaring each container as fluid, we let the browser resize them to fill up the full screen (the viewport). Resizing, re-flowing and maybe re-factoring functionality in response to the size of the device is the essence of responsive design.


<nav class="navbar navbar-expand-md navbar-light bg-light">


The container for the navigation bar is the navbar. In the class attribute we give it a few parameters, and Bootstrap magically does the rest of the formatting.

  • navbar-expand-md: the navbar will expand when the device is mid-sized; this is the breakpoint

  • Navbar-light: light colour scheme for the navbar elements

  • Bg-light: light colour scheme for the navbar background


<span class="navbar-brand">


Bootstrap provides a built-in element type of brand, with a default format that distinguishes it from other navbar elements. By making the brand element in a <span> tag, it just sits there and looks pretty. On many websites, you can click on the brand icon and go to an “about” page. To do this, just put the brand element in an anchor <a> tag with associated href attribute. I’ll use this for the other navbar elements.


  <img src="{{ url_for('static', filename='brand.svg') }}" height="30" class="d-inline-block align-top" alt="PyBloom">

  PyBloom

</span>


We use the Jinja2 url_for() function in order to generate the absolute file path for the brand icon. The Bootstrap attributes of d-inline-block align-top puts the block at the top left. I like this statement as an example of how using these two scripting languages makes generating the HTML and CSS so much more readable.


<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="navbarNavItems" aria-expanded="false">


This toggle button is the key element that enables my responsive design. In smaller screens, smaller than the mid-size breakpoint defined in the parent class above, the screen will not display any of the navbar elements except for the brand and this button. To display the navbar links, you just have to press this button, and the navbar elements are revealed in a column on the left. All the complexity of this design and the interactivity is summarised in these few attributes.

  • class="navbar-toggler": declares to Bootstrap that this button is the one that toggles the navbar in smaller screens

  • data-toggle="collapse": clicking the button collapses the content

  • data-target="navbarNavItems": links to the items in the navbar that will be collapsed/ expanded; the toggled menu bar will look for an element that has ID=navbarNavItems 

  • Aria-expanded="false": sets the current behaviour (i.e. when page is loaded)


  <span class="navbar-toggler-icon"></span>

</button>


Because we need pretty icons everywhere, here’s an icon for the toggle button.


<div class="collapse navbar-collapse" id="navbarNavItems">


This class contains all the remaining navbar elements, and is the subject of the toggle button. When clicking on these elements, you’ll be taken to the web page. Bootstrap handles this subset of elements as a navbar in its own right, inheriting properties from the parent navbar.

  • collapse: sets the behaviour to collapse this navbar on selection

  • navbar-collapse: sets this navbar as the parent breakpoint for the collapse/ expand behaviour

  • id="navbarNavItems": connects this sub-navbar to the toggle button


<div class="navbar-nav mr-auto">


This division groups all the sub-navbar elements together. 

  • navbar-nav: these elements are of the type nav, and will be styled as such

  • mr-auto: the nav items after this group of elements will be pushed to the right side of the viewport, with the padding automatically calculated


<li class="nav-item">

  <a class="nav-link" href="{{ url_for('index') }}">Weather Station</a>

</li>


Here is our first nav item, the actual element that takes us to the subsequent pages. The container for the element, and the content of element (the link) are styled differently, which is why we have two nested items. The container is simply declared to Bootstrap as a nav-item. The link itself is styled in a combination of Jinja2 and Bootstrap.

  • Bootstrap parameters nav-link: declares to Bootstrap as a link object, which won’t look active until the mouse hovers over it

  • Jinja2 script {{ url_for('index') }}: builds the absolute file path to the target page, in this case the index.html page


<li class="nav-item">

  <a class="nav-link" href="{{ url_for('colours') }}">Colour Key</a>

</li>


The second nav item is similar to the first, except we have Jinja2 reference the colours.html file.


<div class="navbar-nav navbar-right">

  <li class="nav-item">

    <a class="nav-link" href="https://blog.mindrocketnow.com">Home</a>

  </li>

</div>


Finally, we have a home link on the right side of the navbar. The construction is very similar to the previous nav items, except it’s a shameless plug for my blog.


<!-- This is where the page content will go -->

{% block app_content %}{% endblock %}


<!-- Space for custom JS -->

{% block app_js %}{% endblock %}


We end the base.html by provisioning a couple of slots to be extended by page-specific HTML and CSS content.



index.html


The hard work of styling each page is done in the base.html document. For each of the subsequent pages, we simply have to extend it with a small amount of additional HTML.


{% extends "base.html" %}


This Jinja2 sippet does exactly that, without needing to know the file path.


{% block app_content %}


This header tells Jinja2 where to insert the subsequent code to make up the full page code.


<h1>Latest data</h1>

<div>

  <h2>Last 24 hours</h2>

  <img src="{{ url_for('static', filename=content['lastday']) }}" class="img-fluid" alt="Temperature over last day">


<h2>Last week</h2>

<img src="{{ url_for('static', filename=content['lastweek']) }}" class="img-fluid" alt="Temperature over last week">


As before, we use a combination of Jinja2 and Bootstrap to define and style the image elements. The Jinja2 snippet references the file that was declared in the rudimentary CMS (content.py) so that the image source points to the right graph. Bootstrap styles this as a fluid image.


  </div>

{% endblock %}


After closing the division, we also need to close the block.


Putting it together


base.html


<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">


    <!-- A little Jinja2 logic to choose page title -->

    {% if title %}

    <title>{{ title }} - PyBloom</title>

    {% else %}

    <title>Welcome to PyBloom</title>

    {% endif %}


    <!-- Bootstrap v5 CSS CDN -->

    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha2/css/bootstrap.min.css" integrity="sha384-DhY6onE6f3zzKbjUPRc2hOzGAdEf4/Dz+WJwBvEYL/lkkIsI3ihufq9hk9K4lVoK" crossorigin="anonymous">


    <!-- Space for Custom CSS -->

    {% block app_css %}{% endblock %}


    <!-- Location of favicon -->

    <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">

  </head>


  <body>

    <!-- Navbar Container -->

    <div class="container-fluid">

      <!-- Navbar Header [contains both toggle button and navbar brand] -->

      <nav class="navbar navbar-expand-md navbar-light bg-light">


        <!-- Navbar Brand [image + title, no link] -->

        <span class="navbar-brand">

          <img src="{{ url_for('static', filename='brand.svg') }}" height="30" class="d-inline-block align-top" alt="PyBloom">

          PyBloom

        </span>


        <!-- Toggle Button [handles opening navbar components on mobile screens] -->

        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="navbarNavItems" aria-expanded"false">

          <span class="navbar-toggler-icon"></span>

        </button>


        <!-- Navbar Collapse [contains all other navbar components] -->

        <div class="collapse navbar-collapse" id="navbarNavItems">

          <!-- Navbar Menu -->

          <div class="navbar-nav mr-auto">

            <li class="nav-item">

              <a class="nav-link" href="{{ url_for('index') }}">Weather Station</a>

            </li>

            <li class="nav-item">

              <a class="nav-link" href="{{ url_for('colours') }}">Colour Key</a>

            </li>

          </div>

          <!-- a plug for my blog! -->

          <div class="navbar-nav navbar-right">

            <li class="nav-item">

              <a class="nav-link"  href="https://blog.mindrocketnow.com">Home</a>

            </li>

          </div>

        </div>

      </nav>

    </div>


    <!-- This is where the page content will go -->

    {% block app_content %}{% endblock %}


    <!-- Bootstrap JavaScript Bundle with Popper.js -->

    <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha2/js/bootstrap.bundle.min.js" integrity="sha384-BOsAfwzjNJHrJ8cZidOg56tcQWfp6y72vEJ8xQ9w6Quywb24iOsW913URv1IS4GD" crossorigin="anonymous"></script>



    <!-- Space for custom JS -->

    {% block app_js %}{% endblock %}

  </body>



index.html


{% extends "base.html" %}


{% block app_content %}

  <h1>Latest data</h1>

  <div>

    <h2>Last 24 hours</h2>

    <img src="{{ url_for('static', filename=content['lastday']) }}" class="img-fluid" alt="Responsive image">

    <h2>Last week</h2>

    <img src="{{ url_for('static', filename=content['lastweek']) }}" class="img-fluid" alt="Responsive image">

  </div>

{% endblock %}



This is the bulk of the web page work done. I have one more web page to create, and I'll describe how in part 12. Also visit https://github.com/Schmoiger/pybloom for the full story.

No comments:

Post a Comment

It's always great to hear what you think. Please leave a comment, and start a conversation!