Bootstrap 5 avec Symfony 5 et Webpack Encore

Introduction

Au moment où j’écris cet article (en décembre 2020), Bootstrap vient de sortir la version v5.0.0-beta1 de son célèbre framework CSS. Comme pour l’utilisation de Tailwind CSS, Symfony avec Webpack Encore nous permet d’utiliser et customiser simplement Bootstrap.

Nous allons découvrir ensemble comment mettre en place simplement Bootstrap, puis nous verrons comment aller plus loin en le personnalisant.

Initialisation du projet Symfony, Webpack Encore et PostCSS

Commençons par créer un nouveau projet Symfony.

symfony new bootstrap5 --full
cd bootstrap5

Puis installons Webpack Encore selon les instructions de la documentation Symfony.

composer require symfony/webpack-encore-bundle
npm install

Renommons le fichier /assets/styles/app.css en app.scss, et modifions le fichier /assets/app.js.

/*
 * Welcome to your app's main JavaScript file!
 *
 * We recommend including the built version of this JavaScript file
 * (and its CSS file) in your base layout (base.html.twig).
 */

// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';

// start the Stimulus application
import './bootstrap';

Dé-commenter .enableSassLoader() dans le fichier webpack.config.js et installons sass-loader.

npm install sass-loader@^9.0.1 node-sass --save-dev

Modifions le fichier /templates/base.html.twig

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{{ encore_entry_link_tags('app') }}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{{ encore_entry_script_tags('app') }}{% endblock %}
    </body>
</html>

Et finalement, lançons une compilation pour nous assurer que tous est OK.

npm run build

Nous allons aussi installer PostCSS suivant la documentation Symfony.

npm install postcss-loader autoprefixer --dev

Créons un fichier postcss.config.js à la racine du projet.

module.exports = {
    plugins: {
        autoprefixer: {}
    }
}

Afin d’en terminer avec cette première étape, faisons un dernier build pour nous assurer que tout fonctionne.

npm run build

Mise en place de Bootstrap 5

Entrons enfin dans le vif du sujet avec la mise en place de Bootstrap 5 !

npm install bootstrap@next

Importons-le JavaScript suivant les consignes de la documentation de Bootstrap 5 en modifiant le fichier /assets/app.js.

/*
 * Welcome to your app's main JavaScript file!
 *
 * We recommend including the built version of this JavaScript file
 * (and its CSS file) in your base layout (base.html.twig).
 */

// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.scss';

// You can specify which plugins you need
import { Tooltip, Toast, Popover } from 'bootstrap';

// start the Stimulus application
import './bootstrap';

Créons un fichier custom.scss dans /assets/styles, puis importons les feuilles de style dans /assets/styles/app.scss.

@import "custom";
@import "~bootstrap/scss/bootstrap";

Nous pouvons lancer un build.

npm run build

Testons Bootstrap 5 sur une page

Bon c’est bien beau toutes ces lignes de commandes, mais testons réellement que notre installation de Bootstrap fonctionne 😉

Créons un contrôleur pour tester et lançons le serveur interne.

symfony console make:controller Home
symfony serve -d

Vérifions que la route fonctionne.

Modifions légèrement le fichier /templates/base.html.twig.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{{ encore_entry_link_tags('app') }}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{{ encore_entry_script_tags('app') }}{% endblock %}
    </body>
</html>

Et ajoutons du code “bootstrap” (issue de la doc) dans le fichier /templates/home/index.html.twig.

{% extends 'base.html.twig' %}

{% block title %}Hello HomeController!
{% endblock %}

{% block body %}
	<div class="container">
		<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-light">
			<div class="container-fluid">
				<a class="navbar-brand" href="#">Navbar</a>
				<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
					<span class="navbar-toggler-icon"></span>
				</button>
				<div class="collapse navbar-collapse" id="navbarSupportedContent">
					<ul class="navbar-nav me-auto mb-2 mb-lg-0">
						<li class="nav-item">
							<a class="nav-link active" aria-current="page" href="#">Home</a>
						</li>
						<li class="nav-item">
							<a class="nav-link" href="#">Link</a>
						</li>
						<li class="nav-item dropdown">
							<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
								Dropdown
							</a>
							<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
								<li>
									<a class="dropdown-item" href="#">Action</a>
								</li>
								<li>
									<a class="dropdown-item" href="#">Another action</a>
								</li>
								<li><hr class="dropdown-divider"></li>
								<li>
									<a class="dropdown-item" href="#">Something else here</a>
								</li>
							</ul>
						</li>
						<li class="nav-item">
							<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
						</li>
					</ul>
					<form class="d-flex">
						<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
						<button class="btn btn-outline-success" type="submit">Search</button>
					</form>
				</div>
			</div>
		</nav>
		<main>
			<div class="py-5 text-center mt-5">
				<h2>Checkout form</h2>
				<p class="lead">Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.</p>
			</div>

			<div class="row g-3">
				<div class="col-md-5 col-lg-4 order-md-last">
					<h4 class="d-flex justify-content-between align-items-center mb-3">
						<span class="text-muted">Your cart</span>
						<span class="badge bg-secondary rounded-pill">3</span>
					</h4>
					<ul class="list-group mb-3">
						<li class="list-group-item d-flex justify-content-between lh-sm">
							<div>
								<h6 class="my-0">Product name</h6>
								<small class="text-muted">Brief description</small>
							</div>
							<span class="text-muted">$12</span>
						</li>
						<li class="list-group-item d-flex justify-content-between lh-sm">
							<div>
								<h6 class="my-0">Second product</h6>
								<small class="text-muted">Brief description</small>
							</div>
							<span class="text-muted">$8</span>
						</li>
						<li class="list-group-item d-flex justify-content-between lh-sm">
							<div>
								<h6 class="my-0">Third item</h6>
								<small class="text-muted">Brief description</small>
							</div>
							<span class="text-muted">$5</span>
						</li>
						<li class="list-group-item d-flex justify-content-between bg-light">
							<div class="text-success">
								<h6 class="my-0">Promo code</h6>
								<small>EXAMPLECODE</small>
							</div>
							<span class="text-success">−$5</span>
						</li>
						<li class="list-group-item d-flex justify-content-between">
							<span>Total (USD)</span>
							<strong>$20</strong>
						</li>
					</ul>

					<form class="card p-2">
						<div class="input-group">
							<input type="text" class="form-control" placeholder="Promo code">
							<button type="submit" class="btn btn-secondary">Redeem</button>
						</div>
					</form>
				</div>
				<div class="col-md-7 col-lg-8">
					<h4 class="mb-3">Billing address</h4>
					<form class="needs-validation" novalidate>
						<div class="row g-3">
							<div class="col-sm-6">
								<label for="firstName" class="form-label">First name</label>
								<input type="text" class="form-control" id="firstName" placeholder="" value="" required>
								<div class="invalid-feedback">
									Valid first name is required.
								</div>
							</div>

							<div class="col-sm-6">
								<label for="lastName" class="form-label">Last name</label>
								<input type="text" class="form-control" id="lastName" placeholder="" value="" required>
								<div class="invalid-feedback">
									Valid last name is required.
								</div>
							</div>

							<div class="col-12">
								<label for="username" class="form-label">Username</label>
								<div class="input-group">
									<span class="input-group-text">@</span>
									<input type="text" class="form-control" id="username" placeholder="Username" required>
									<div class="invalid-feedback">
										Your username is required.
									</div>
								</div>
							</div>

							<div class="col-12">
								<label for="email" class="form-label">Email
									<span class="text-muted">(Optional)</span>
								</label>
								<input type="email" class="form-control" id="email" placeholder="you@example.com">
								<div class="invalid-feedback">
									Please enter a valid email address for shipping updates.
								</div>
							</div>

							<div class="col-12">
								<label for="address" class="form-label">Address</label>
								<input type="text" class="form-control" id="address" placeholder="1234 Main St" required>
								<div class="invalid-feedback">
									Please enter your shipping address.
								</div>
							</div>

							<div class="col-12">
								<label for="address2" class="form-label">Address 2
									<span class="text-muted">(Optional)</span>
								</label>
								<input type="text" class="form-control" id="address2" placeholder="Apartment or suite">
							</div>

							<div class="col-md-5">
								<label for="country" class="form-label">Country</label>
								<select class="form-select" id="country" required>
									<option value="">Choose...</option>
									<option>United States</option>
								</select>
								<div class="invalid-feedback">
									Please select a valid country.
								</div>
							</div>

							<div class="col-md-4">
								<label for="state" class="form-label">State</label>
								<select class="form-select" id="state" required>
									<option value="">Choose...</option>
									<option>California</option>
								</select>
								<div class="invalid-feedback">
									Please provide a valid state.
								</div>
							</div>

							<div class="col-md-3">
								<label for="zip" class="form-label">Zip</label>
								<input type="text" class="form-control" id="zip" placeholder="" required>
								<div class="invalid-feedback">
									Zip code required.
								</div>
							</div>
						</div>

						<hr class="my-4">

						<div class="form-check">
							<input type="checkbox" class="form-check-input" id="same-address">
							<label class="form-check-label" for="same-address">Shipping address is the same as my billing address</label>
						</div>

						<div class="form-check">
							<input type="checkbox" class="form-check-input" id="save-info">
							<label class="form-check-label" for="save-info">Save this information for next time</label>
						</div>

						<hr class="my-4">

						<h4 class="mb-3">Payment</h4>

						<div class="my-3">
							<div class="form-check">
								<input id="credit" name="paymentMethod" type="radio" class="form-check-input" checked required>
								<label class="form-check-label" for="credit">Credit card</label>
							</div>
							<div class="form-check">
								<input id="debit" name="paymentMethod" type="radio" class="form-check-input" required>
								<label class="form-check-label" for="debit">Debit card</label>
							</div>
							<div class="form-check">
								<input id="paypal" name="paymentMethod" type="radio" class="form-check-input" required>
								<label class="form-check-label" for="paypal">PayPal</label>
							</div>
						</div>

						<div class="row gy-3">
							<div class="col-md-6">
								<label for="cc-name" class="form-label">Name on card</label>
								<input type="text" class="form-control" id="cc-name" placeholder="" required>
								<small class="text-muted">Full name as displayed on card</small>
								<div class="invalid-feedback">
									Name on card is required
								</div>
							</div>

							<div class="col-md-6">
								<label for="cc-number" class="form-label">Credit card number</label>
								<input type="text" class="form-control" id="cc-number" placeholder="" required>
								<div class="invalid-feedback">
									Credit card number is required
								</div>
							</div>

							<div class="col-md-3">
								<label for="cc-expiration" class="form-label">Expiration</label>
								<input type="text" class="form-control" id="cc-expiration" placeholder="" required>
								<div class="invalid-feedback">
									Expiration date required
								</div>
							</div>

							<div class="col-md-3">
								<label for="cc-cvv" class="form-label">CVV</label>
								<input type="text" class="form-control" id="cc-cvv" placeholder="" required>
								<div class="invalid-feedback">
									Security code required
								</div>
							</div>
						</div>

						<hr class="my-4">

						<button class="w-100 btn btn-primary btn-lg" type="submit">Continue to checkout</button>
					</form>
				</div>
			</div>
		</main>

		<footer class="my-5 pt-5 text-muted text-center text-small">
			<p class="mb-1">© 2017–2020 Company Name</p>
			<ul class="list-inline">
				<li class="list-inline-item">
					<a href="#">Privacy</a>
				</li>
				<li class="list-inline-item">
					<a href="#">Terms</a>
				</li>
				<li class="list-inline-item">
					<a href="#">Support</a>
				</li>
			</ul>
		</footer>
	</div>

{% endblock %}

Et vérifions ! Cool, Bootstrap est en place ! Vous pouvez vous arrêter la si vous le souhaitez !

Personnalisons (un peu) Bootstrap !

Bon maintenant que Bootstrap fonctionne, essayons d’aller un tout petit peu plus loin en personnalisant les couleurs. On ne va pas aller très loin dans notre personalisation, car la doc est suffisamment explicite.

Nous allons simplement modifier le fichier /assets/styles/customs.scss en sur-chargeant certaines valeurs du fichier node_modules/bootstrap/scss/_variables.scss.

$primary:       #FFBE0B;
$secondary:     #FB5607;
$success:       #3A86FF;
$info:          #FF006E;
$light:         #8338EC;

Nous lançons un build.

npm run build

Et vérifions que le changement est visible en rechargeant notre page (et désolé pour mon choix de couleurs totalement radom 😉 ).

Conclusions et dépôt GitLab

Nous avons vu comment mettre en place facilement et simplement Bootstrap dans un projet Symfony à l’aide de Webpack Encore, et avec l’utilisation de Saas, nous avons vu qu’il peut être également simple de personnaliser Bootstrap.

Vous avez les clés pour aller plus loin dans votre utilisation de Bootstrap, amusez-vous bien !

Vous trouverez les sources dans ce dépôt GitLab.

6 réflexions sur “Bootstrap 5 avec Symfony 5 et Webpack Encore”

  1. Ping : Mettre en production une application Symfony 5 avec Heroku – YoanDev

  2. Ping : Un Workflow de pro avec Symfony 5 ! – YoanDev

  3. Hello Yoan

    Après l’installation de node-saas et lors du build, j’obtiens le message suivant
    Error: Node Sass version 5.0.0 is incompatible with ^4.0.0.

    Afin de le corriger j’ai du faire :
    npm uninstall node-sass
    npm install node-sass@4.14.1

    Une différence de version entre l’écriture de l’article et maintenant?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.