Chris Brabender
Posted on March 3, 2021
TLDR: It doesn't have to be difficult.
A short history
PWA Studio from Magento has been out and about in the wild for a couple of years now and over time they are slowly adding to the core product.
Originally Venia (the code name for the project) was seen as a reference storefront only but over time has grown in to the base starting point for new projects. This comes with a set of challenges not originally considered by the Magento team.
One of those key challenges is simply modifying or replacing the styling of a component. Until now that has been done in one of 3 ways:
Tree replacement
Taking and replacing the entire tree to get to the component you want to style, and then overriding the entire CSS file. This causes issues because you are taking on ownership of the entire code for all components in the tree, making it more difficult to maintain and upgrade.
Normal replacement module / webpack aliases
This focuses on using webpack to modify the file references in particular components and replacing them with a file in your own project. This approach is relatively solid but can become unwieldly with a long list of aliases to manage when you start
overriding hundreds of CSS files.
The alias approach can also be risky if file names are duplicated elsewhere.
config.resolve.alias = {
...config.resolve.alias,
'./productFullDetail.css': path.resolve('./src/components/ProductFullDetail/productFullDetail.css')
}
Fooman VeniaUiOverrideResolver
A great module from Fooman allowing you to overwrite any file in peregrine/venia-ui easily and following a simple pattern. I personally didn't like the folder structure this introduced to projects and was override only, not extend.
Reference - https://github.com/fooman/venia-ui-override-resolver
So what's different now?
Version 9.0 of PWA Studio introduced some new features and enhancements to the extensibility framework. Targetables gives us the opportunity to modify a React component without the need to override the entire component in our app.
The concept
How do I turn the standard Venia storefront into something custom for my clients?
Here's our starting point:
I wanted to explore how we could use the Targetables, the naming convention for styling attached to components and mergeClasses to simplify the entire process of updating styling.
The naming convention
PWA Studio follows a strict naming convention with regards to CSS files for components. Lets take Button as an example.
The Button component is made up of two files:
- button.js
- button.css
button.js imports button.css and uses that as the defaultClasses with the mergeClasses function from classify.
So what if we were to mimic that file structure in our local project? Following along with the Button example, if I was to create a file src/components/Button/button.css
could I have that picked up automatically?
mergeClasses
mergeClasses, by default, takes the default styling from defaultClasses and merges them with anything passed in via props to the component.
Here we could add an addition set of classes that could be out local styling updates and make it look something like:
const classes = mergeClasses(defaultClasses, localClasses, props.classes);
This would give us the flexibility of local styling on top of the default styling, but also the ability to pass in props styling for specific use cases across the application, which would update our local styling.
Putting it to work
We need two things to make this all work:
- A way to identify any local files that extend default styling
- A way to add them to our library components without overriding
Identifying local styling
globby is a great tool for recursively scanning directories to find files or folders matching specific criteria, so we need to add that to our project.
yarn add globby
Next, we are going to use our local-intercept.js
file as the place we do most of the work here.
This script scans all directories in src/components
and finds any CSS files. It then extracts the component from the folder names and tries to match that to a component in venia-ui, if it matches, we know we are trying to extend styling.
function localIntercept(targets) {
const { Targetables } = require('@magento/pwa-buildpack');
const targetables = Targetables.using(targets);
const magentoPath = 'node_modules/@magento';
const globby = require('globby');
const fs = require('fs');
const path = require('path');
(async () => {
/** Load all CSS files from src/components */
const paths = await globby('src/components', {
expandDirectories: {
extensions: ['css']
}
});
paths.forEach((myPath) => {
const relativePath = myPath.replace('src/components', `${magentoPath}/venia-ui/lib/components`);
const absolutePath = path.resolve(relativePath);
/** Identify if local component maps to venia-ui component */
fs.stat(absolutePath, (err, stat) => {
if (!err && stat && stat.isFile()) {
/**
* This means we have matched a local file to something in venia-ui!
* Find the JS component from our CSS file name
* */
const jsComponent = relativePath.replace('node_modules/', '').replace('.css', '.js');
}
});
});
})();
}
Adding our styling
So now we know what CSS files we are extending, how do we tell our library components to use our styling?
Thats where Targetables come in to play. Taking our script above, we know what the JS Component is so we can just add this after the jsComponent line:
/** Load the relevant venia-ui component */
const eSModule = targetables.reactComponent(jsComponent);
const module = targetables.module(jsComponent);
/** Add import for our custom CSS classes */
eSModule.addImport(`import localClasses from "${myPath}"`);
/** Update the mergeClasses() method to inject our additional custom css */
module.insertAfterSource(
'const classes = mergeClasses(defaultClasses, ',
'localClasses, '
);
The script here loads an esModule and injects our localClasses into the top of the file as an import and then modifies the default mergeClasses from:
const mergeClasses(defaultClasses, props.classes);
to
const mergeClasses(defaultClasses, localClasses, props.classes);
Setting up some custom styles
The screenshot above shows the product detail page, so lets change up some of the styling on that page.
To do that we are going to create a new file in our project:
src/components/ProductFullDetail/productFullDetail.css
Now you can do a yarn watch
and see the changes we are going to do live. As this customisation is applied at build time if you create a NEW file you will need to stop and start your project but if you modify a file you have already created the hot reload functions will work just fine.
Let's add the following to our css file, which will add a border around our Image Carousel:
.imageCarousel {
border: solid 1px black;
}
That's it. That's the blog, thanks for reading. Not really but this should have reloaded and should look a little broken, but this is a good thing.
What we've done here is modified just the imageCarousel class in our custom file and kept all the rest of the styling for the ProductFullDetail page which is great! Exactly what we wanted, but we've lost all our original styling for the imageCarousel.
This is good in some instances where we want to just replace all the styling for a particular class so having this full replacement as an option is great, but if we want to just modify one thing and inherit the rest, we able to use composes from CSS Modules to achieve this. All we need to do is composes imageCarousel from Venia like this:
.imageCarousel {
composes: imageCarousel from '~@magento/venia-ui/lib/components/ProductFullDetail/productFullDetail.css';
border: solid 1px black;
}
Now our page is looking like it should, with our border.
That really is it now though. Thanks for reading! If you have any questions let me know on Twitter @brabs13 or via the #pwa slack channel in Magento Community Engineering.
If you put this into practice please share me a link so I can check out the work.
Posted on March 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.