DEV Community

TommyLi
TommyLi

Posted on • Updated on

Building My First Chrome Extension - Reddit Filter

Oh hello.

Welcome to my first blog. Last week I learned how to build my first chrome extension to solve a problem I had, and I decided that writing a blog to document my process would be a good way for me to remind myself how my program work in case I need to build something similar in the future. If you are interested in learning how to build an extension, just for fun or to solve a problem, I hope this blog can give you a better idea of how to do that.

To summarize my extension, it is a post-filter that removes any Reddit posts containing keywords provided by the user. Feel free to download it and try it out here. (be sure to look at the installation instruction)

Here's a demonstration of the extension in action.

If you just want to see my process for building the extension, you can just skip to the Getting Started part. Otherwise, feel free to read the entire blog, and I hope you enjoy it.

Why Did I Build This Extension?

Since I started learning full-stack web development from #100Devs in January, many people I've talked to told me that the best way to apply the skills I learned is to build projects. While I built several websites and web applications, I've always wanted to build something more meaningful to me. But every project idea I had was either too grand or impractical.

After chatting with Nick Dejsus, I learned that looking out for things that annoy me daily, big or small, can be an excellent way to find project ideas. Just like how he built the shopping cart website because he noticed a lack of support for shopping cart libraries out there for web developers.

So I started by paying attention to my daily life and looking for problems that I may be able to solve by coding. I quickly found one: like many people who use Reddit, I sometimes get tired of seeing the same topic over and over again. After hours of searching, I was not able to find a reliable solution for filtering posts by keywords, so I decided this will be the problem that I will try to solve.

How Much Did I Know

Before I wrote any code for this extension, I was familiar with HTML, CSS, and JavaScript, and I have built many projects using these tools. While I was comfortable doing JavaScript DOM manipulations, I didn't know the first thing about building a browser extension.

Getting Started

Before I wrote any code for the extension itself, I tried to write a script in the console to remove any Reddit post that matches a keyword I choose. To do this, I first inspected the elements on a random Reddit page, like this: Reddit Inspect ElementNotice how the div I selected has lots of siblings, and they are all under a parent div with the weird class name, "rpBJOHq2PR60pnwJlUyP0"? Well, that parent div with the weird class name is essentially a container that holds all the posts on the page, and each of the direct child div (like the one I selected in the screenshot) is an individual post. In other words, if I delete any direct child under "rpBJOHq2PR60pnwJlUyP0", I delete a post!

To select any post that contains a filter word of my choosing, I had to look for what element holds the title and tags:
Reddit Title h3 ElementReddit Title span ElementIn this case, h3 elements are used for post titles and span elements are used for post tags. With that knowledge, I set up my post selectors like this:

// class name for the post container
let postClassName = '.rpBJOHq2PR60pnwJlUyP0';

// parent container
let list = document.querySelector(`${postClassName}`);
// post titles
let titles = list.getElementsByTagName('h3');
// post tags
let tags = list.getElementsByTagName('span');
Enter fullscreen mode Exit fullscreen mode

The reason I saved that weird class name in a separate variable, "postClassName", is because a lot of class names on Reddit are randomly generated. So just in case that name somehow changes in the future, I only need to update it once in the code.

Now, since the "list" variable contains all the Reddit posts at any given moment, both "titles" and "tags" will also contain all the titles and tags on the page at any given moment. So I had to sort through each title and tag, and if any of them was to match my keyword, I would then select its parent element just below "list" and delete that parent. In other words, I deleted the posts that contain the title or tag that match my keyword. At last, this is the console script I came up with:

// choose any filter word you want
let filter = 'any word';

// class name for the post container
let postClassName = '.rpBJOHq2PR60pnwJlUyP0';

// selectors
let list = document.querySelector(`${postClassName}`);
let titles = list.getElementsByTagName('h3');
let tags = list.getElementsByTagName('span');

// sort through all the titles
Array.from(titles).forEach(post => {
    if (post.textContent.toLocaleLowerCase().includes(filter)) {
        const parent = post.closest(`${postClassName} > div`);
        parent.parentNode.removeChild(parent);
    }
});

// sort through all the tags
Array.from(tags).forEach(post => {
    if (post.textContent.toLocaleLowerCase().includes(filter)) {
        const parent = post.closest(`${postClassName} > div`);
        parent.parentNode.removeChild(parent);
    }
});
Enter fullscreen mode Exit fullscreen mode

Here's an example of using the script to get rid of any posts with titles or tags that contain the word "leak":
Example beforeExample after

Setting Up the Extension

After getting the script working, I looked up some tutorials on how to set up an extension. To quickly get the basics across, I drew this diagram to show the relationship between the browser and the content page.
PS: The main tutorial I used was from Coding Train.
Chrome Relationship DiagramIn a nutshell, if we want to change something on the current page, we would be changing the "content" (orange bubble). So we can make a script file named "content.js", and the script that I showed earlier will go here. But if we want the user to input something, we would need a popup window for the browser. This popup window (green bubble) is a tiny HTML page, and this HTML page would have its own JavaScript file. These files can be called "popup.html" and "popup.js", respectively. Extension filesYou probably noticed there's also a file named "manifest.json". This is the most important file because it tells the browser about your extension (i.e. names, version, etc) and which files are being used. In addition, it also tells the browser what permissions are needed to run this extension. In my case, I needed permissions for scripting or getting tabs information so the extension knows which tabs to send info to later. So here's the manifest code I used for my extension:

{
    "manifest_version": 3,
    "name": "Reddit Filter",
    "version": "1.0",
    "description": "Filter out reddit posts based on keywords",
    "content_scripts": [
        {
            "js": ["content.js"]
        }
    ],
    "action": {
        "default_popup": "popup.html"
    },
    "permissions": ["scripting", "tabs"]
}
Enter fullscreen mode Exit fullscreen mode

While the first few items are just descriptions of the extensions, it's important to note that we have to include the manifest_version (tells Chrome which manifests version to use).

Sending The Message

Here I made a simple popup to provide the means for the user to input something: popup window Since the popup window is essentially an HTML file, I can link the "popup.js" file to it. The job of "popup.js" is to take the word that the user typed in the popup window and send it over to the "content.js" file.

To know which Chrome tab to send the message to, "popup.js" must specify the tab or tabs (see the "params" variable below) using the Chrome API "chrome.tabs". That's why I needed the "tabs" permission that was mentioned earlier. Here I wrote this "sendMessage" function to send the input text from the popup window only to the current tab the user is on:

// popup.js
// send the input keyword to the content script
function sendMessage() {
    // set parameters to select current tab only
    let params = {
        active: true,
        currentWindow: true
    }
    chrome.tabs.query(params, gotTabs);

    // send the keyword input by the user to the current tab
    function gotTabs(tab) {
        let message = {
            txt: userInput.value
        }
        chrome.tabs.sendMessage(tab[0].id, message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Receiving The Message

Once we send a message from the popup window to a specific tab in Chrome, we would need to inform the tab to be ready to receive a message. So in the "content.js", we can use the Chrome API "chrome.runtime" to listen for any message and save the message to a word list once a message is received:

// listen for message, run gotMessage if message is received
chrome.runtime.onMessage.addListener(gotMessage);

// filter word list
let wordList = [];

function gotMessage(message) {
    let newWord = message.txt.toLowerCase();

    // put the new keyword into the word list if it's not already in there
    if (!wordList.includes(newWord)) {
        wordList.push(newWord);
    }

    // whenever we get a new keyword, we call the removePosts function once
    removePosts();
}
Enter fullscreen mode Exit fullscreen mode

To prevent the code snippet from becoming too long, I didn't show the code for the callback "removePosts()", but feel free to look at the Github page for any detail. Basically it's a slightly modified version of the filter script we saw in the "Getting Started" section.

Putting Everything Together

Once I got the filter function and message delivery system working between the browser and the current tab, I had a functioning Reddit filter extension.

Finally, after showing it to my #100Devs classmate Raeshelle, she brought up a good point: the extension only runs the filter function whenever a new word is entered, and not when new posts are loaded from the user scrolling down. So I added this line of code at the very end of "content.js":

document.addEventListener('scroll', removePosts);
Enter fullscreen mode Exit fullscreen mode

Its job is to run "removePosts()" whenever the user scrolls on the page. This is also the reason why I saved the user's input in a word list rather than a single variable that gets overwritten every time a new word is entered. With that in place, we got a working extention!

Conclusion

It was a fun and engaging experience to learn how to build a project that solves a problem that I had. So I am excited to learn more new tools and build more interesting things. Currently, I'm learning how to use React to make my web apps more reactive and use it in my future projects. I will be writing more posts in the future to document my projects and learning progress.

If you like this blog, please follow me and the other awesome devs mentioned above on twitter to check out more updates from our coding journey.

Latest comments (0)