DEV Community

Chris Brabender
Chris Brabender

Posted on

Simplifying Targetables in PWA Studio

After the positive feedback on my first Simplifying Styling in PWA Studio article (thanks to everyone who gave me feedback there), I have put together the next iteration of the concept which focuses on Simplifying Targetables in PWA Studio.

I'm not going to go into the details on how to use Targetables, that has been covered in a few places which you can refer to:

What I am going to cover here is the problem with the local-intercept.js file.

What problem?

In our scaffolded PWA Studio project we have a single local-intercept.js file which is our starting point for working with Targets and Targetables.

Having a single file to do all of our work here will quickly make this file unruly and frankly, just enormous. This makes it difficult to identify customisations (for debugging, finding what has been done etc) and ensure we aren't trying to intercept the same component twice.

What is the fix?

Following a similar concept to the Simpliying Styling process, I am proposing a file naming convention and structure to automatically detect and load relevant intercept files for specific modules.

So what does the structure look like? Let's assume we want to intercept the Header component

  • @magento/venia-ui/lib/components/Header/header.js
  • We could create a new file in our local project like this - src/components/Header/header.targetables.js

Again if we wanted to intercept the ProductFullDetail component * @magento/venia-ui/lib/components/ProductFullDetail/productFullDetail.js)

  • We could create a src/components/ProductFullDeatil/productFullDetail.targetables.js file

This way we can more easily identify, categorise and isolate our intercepts. Making it much easier to customise the project.

So assuming we have these files in place, how do we load them? And how do we use them?

Updating local-intercept.js

We need to do a few things in our local-intercept.js file to identify and load these files. So we are going to use globby again to find our targetables files.

We are also going to use babel-plugin-require-context-hook/register to load the files inline and execute a function within those files (this means we can pass the component to our targetables files and run a standardised function to simplify things even more).

  // Access our Targetables object
const { Targetables } = require('@magento/pwa-buildpack');
const targetables = Targetables.using(targets);

// Define the path to @magento packages
const magentoPath = 'node_modules/@magento';

const globby = require('globby');
const path = require('path');

// Context loader allows us to execute functions in the targeted file
const requireContextLoader = require('babel-plugin-require-context-hook/register')();

// Find our .targetables.js files
(async () => {
    const paths = await globby('src/components', {
        expandDirectories: {
        files: ['*.targetables.js']
        }
    });

    paths.forEach((myPath) => {
        const relativePath = myPath.replace('.targetables', '').replace(`src/components`, `${magentoPath}/venia-ui/lib/components`);
        const absolutePath = path.resolve(relativePath);

        fs.stat(absolutePath, (err, stat) => {
        if (!err && stat && stat.isFile()) {
            // Retrieve the react component from our cache (so we can use it more than once if necessary)
            const component = getReactComponent(relativePath.replace('node_modules/', ''));

            /** 
             * Load the targetables file for the component and execute the interceptComponent function
             * We also pass in the component itself so we don't need to load it in the file
            */
            const componentInterceptor = require('./' + myPath);
            componentInterceptor.interceptComponent(component);
        }
        });
    });
})();


// Create a cache of components so our styling and intercepts can use the same object
let componentsCache = [];
function getReactComponent(modulePath) {
    if (componentsCache[modulePath] !== undefined) {
        return componentsCache[modulePath];
    }

    return componentsCache[modulePath] = targetables.reactComponent(modulePath);
}
Enter fullscreen mode Exit fullscreen mode

And that is all we need in our local-intercept.js file in order to load and execute all our *.targetables.js files! We don't need to touch local-intercept.js ever again to execute targetables functionality.

Our *.targetables.js files

Within our *targetables.js files all we need to do is define our interceptComponent function and export it.

EG: src/components/Header/header.targetables.js

Here we are passing in the header.js component from Venia UI with the targetables functions ready to go. So we can simply do component.insertAfterJSX etc in our files.

const interceptComponent = (component) => {
    component.addImport('import MegaMenu from "../../../../../../src/components/MegaMenu"');
    component.insertAfterJSX('Link', '<MegaMenu />');
    component.setJSXProps('Link', {
        'className':'{classes.logoContainer}'
    });
}

exports.interceptComponent = interceptComponent;
Enter fullscreen mode Exit fullscreen mode

And this can be repeated to intercept any other Venia UI component we like:

EG 2: src/components/Navigation/navigation.targetables.js

Here we are just removing the h2 element from the navigation.

const interceptComponent = (component) => {
    // Execute our JSX manipulation
    component.removeJSX('h2');
}

exports.interceptComponent = interceptComponent;
Enter fullscreen mode Exit fullscreen mode

It's a pretty straight forward setup and I hope it will come in handy to help you manage your customisations for your new PWA Studio projects!

Please share and let me know if you have any feedback and/or if you implement this strategy!

Top comments (5)

Collapse
 
jesusiasenzaniro profile image
JesusIasenzaniro

Good morning @chrisbrabender !
How do you set an extension with this method?
I have problems setting the tagList example of the documentation magento.github.io/pwa-studio/tutor..., because the last step use normal intercepts, could you help me out?

Collapse
 
jwbrownie profile image
JWBrownie

Hello Chris, good day to you, I read your article and looked at your code, Did you intentionally provide slightly wrong code? For your readers to fix it? A quick example of the variable:

requireContextLoader

Is declared but is not used anywhere in the code, also the local-intercept.js file should be a function that receives targets, in your code it seems that goes inside the main localIntercept method but there is no where that's being suggested.

I fixed most of it, but I am not sure what to do with requireContextLoader, I'll check that out possible I am getting the import error from the jsx files being loaded because I am not using the requireContextLoader, is that it?

Thank you!

Collapse
 
jwbrownie profile image
JWBrownie

Also, I am getting the following error:

vudrok@xodbox:~/Work/numismatica/enhanced/numismatica-ui$ yarn watch
yarn run v1.22.19
$ webpack-dev-server --progress --color --env.mode development
/home/vudrok/Work/numismatica/enhanced/numismatica-ui/node_modules/@magento/venia-ui/lib/classify.js:1
import React, { Component } from 'react';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at compileFunction (<anonymous>)

Enter fullscreen mode Exit fullscreen mode

This is my full targetables file:

// src/@theme/components/Button/button.targetables.js 
async function interceptComponent(component) {
    const { mergeClasses } = await import('@magento/venia-ui/lib/classify.js');
    const myButtonCSS = require('./button.module.css');

    component.setJSXProps('Button', {
        classes: mergeClasses(defaultClasses, myButtonCSS)
    });
}

module.exports = interceptComponent;

Enter fullscreen mode Exit fullscreen mode
Collapse
 
vasiliib profile image
Burlacu Vasilii

Hi @chrisbrabender ,
Thank you very much for sharing this wonderful solution.
Can you please confirm the changes made into the *.targetables.js files are applied right away while yarn watch is running? Or after changes in the file need to run yarn watch again? If so, is there any solution for this?
Thanks.

Collapse
 
anietog1 profile image
Agustín Nieto

You need to run yarn watch again. I'm yet to find a solution for this, please let me know if you found something!