Search code examples
javascripthtmlcssstylesheet

change stylesheet by button click for specific element only


So, this question is based on a piece of homework that I'm currently working on. I was provided with an HTML document and two stylesheets. The HTML document is really simple:

<html>
<head>
   <title>DOM-scripting: change style</title>
   <link rel="stylesheet" href="style/style1.css" id="stylesheet"/>
</head>
<body>
    <nav class="chooseStyle">
        <input type="button" id="style1Button" value="Style 1"/>
        <input type="button" id="style2Button" value="Style 2"/>
    </nav>
    <article>
        <h1>Style change using DOM-scripting</h1>
        <p> sample text </p>
        <p> sample text </p>
        <p> sample text </p>
    </article>
    <nav class="links">
        <ul>
            <li><a href="link">school name</a></li>
            <li><a href="link">school name</a></li>
       </nav>

    <script src="scriptToUse.js"></script>
</body>
</html>

style1.css consists of the following:

body {
    background-color: #676632;
}

input[type="button"] {
    color: #F5F265;
    background-color: #9B9302;
}

article {
    margin-left: 150px;
    margin-right: 150px;
    text-indent: 20px;
    color: #C9C892;
}

h1 {
    color: #EEF1BA;
    text-align: center;
}

a {
    color: #A1FA56;
    text-decoration: none;
    font-weight: bold;
}

ul {
    padding: 0;
}

li {
    list-style-type: none;
}

Style2.css consists of the following:

body {
    color: #EEEEAA;
}

input[type="button"] {
    color: #9B9302;
    background-color: #F5F265;
}

article {
    margin-left: auto;
    margin-right: auto;
    text-align: justify;

    color: #A3A163;
}

h1 {
    color: #AAAA66;
    text-align: left;
}

a {
    color: #CDCF9B;
}

ul {
    padding: 0;
}

li {
    list-style-type: none;
}

The first part of the exercise states that I need to create a javascript file which allows the stylesheet for the webpage to change according to the button that was clicked. The first button refers to the first stylesheet and the second refers to the second stylesheet. I figured this out fairly easy and everything works fine.

However, the second part of the exercise states the following: "Now edit the javascript file so that only the style of the article is changed when the buttons are clicked. DO NOT edit the CSS file." Meaning that the href for the stylesheets should still be changed, like in the first part, but now it may only affect the style of the article element instead of the style of the whole page.

I have been searching for related questions for the past hour but nothing seems to really provide a solution. I have read some questions stating that it is impossible to apply a stylesheet only to a specific element without changing anything in the stylesheet itself. Keep in mind that I am only in my first year of college and have only just begun learning javascript, meaning that a solution should not be that hard to find. I guess?

The Javascript code I wrote so far:

'use strict';

const handleLoad = () => {
    let style1 = document.getElementById('style1Button');
    let style2 = document.getElementById('style2Button');
    style1.addEventListener('click', handleClick);
    style2.addEventListener('click', handleClick);
};

const handleClick = (event) => {
    if(event.target.value === "Style 1") {
        document.getElementById("stylesheet").href = "style/style1.css";
        // should only change article style 
        // currently still referring to the whole document
    } else {
        document.getElementById("stylesheet").href = "style/style2.css";
        // should also only change article style
        // also still referring to the whole document
    }
};

window.addEventListener('load', handleLoad);

Is there any way I could block the CSS rules for all elements except the article element? Or maybe change the classes or ID's for the elements that should not be affected?

Thanks a lot in advance :)


This is what finally solved it. (code is probably not as compact and correct as it could be but it works so I'm more than happy :D):

'use strict';

const handleLoad = () => {
    let style1 = document.getElementById('style1Button');
    let style2 = document.getElementById('style2Button');
    style1.addEventListener('click', handleClick);
    style2.addEventListener('click', handleClick);
    makeShadow();
};

const handleClick = (event) => {
    let article = document.querySelector('article');
    let shadow = article.shadowRoot;

    let style = event.target.value === "Style 1" ? "style/style2.css" : "style/style1.css";

    for(let node of shadow.childNodes) {
        if(node.hasAttribute('href')){
            node.href = style;
        }
    }
};

const makeShadow = () => {
    let article = document.querySelector('article');
    let articleClone = makeClone(article);

    let shadow = article.attachShadow({mode:'open'});

    let link = document.createElement('link');
    link.rel = "stylesheet";
    link.href = 'style/style1.css';

    shadow.appendChild(articleClone);
    shadow.prepend(link);
};

const makeClone = (article) => {
    let clone = article.cloneNode();
    clone.innerHTML = article.innerHTML;
    return clone;
};

window.addEventListener('load', handleLoad);

Solution

  • In This case we can make use of shadowRoot as an encapsulation for styles.

    The idea is like adding a css file which contains a ton of styles to a page which only have the article element, obviously the other styles won't have any effect.

    We can do the same using a shadowRoot put the article in it's own little world and add a file which contains more styles.

    The code is heavily commented. If you have any question leave a comment :)

    First on load we prepare the shadow Element:

    const handleLoad = () => {
    
        // Old Code for button handlers still needed
        let style1 = document.getElementById('style1Button');
        let style2 = document.getElementById('style2Button');
        style1.addEventListener('click', handleClick);
        style2.addEventListener('click', handleClick);
    
    
        // New Code Below
    
        // Select the article from the page 
        const article = document.querySelector('article');
        // Create an empty div Element to work as a bucket
        const placeholderElement = document.createElement('div');
        // set it's id attribute to select it later
        placeholderElement.setAttribute('id', 'placeholderElementShadowRot')
        // replace the article with our div in the document
        // the article element is not completly lose at this point
        // we still have in the variable article
        article.replaceWith(placeholderElement);
        // attache a shadow to the div element
        const shadow = placeholderElement.attachShadow({ mode: 'open' });
        // sets the shadow's innerHTML equal to article outerHTML
        shadow.innerHTML = article.outerHTML;
    
    
        // create a link
        const link = document.createElement('link');
        // set it's href attribute to the first style file
        link.setAttribute("href", 'style1.css');
        // set the rel attribute
        link.setAttribute("rel", "stylesheet");
        // add the link to the shadow
        shadow.appendChild(link)
    };
    

    And on button click

    const handleClick = (event) => {
        // Select our div we created before using the id
        const placeholderElement = document.querySelector('#placeholderElementShadowRot');
        // since we already attached a shadow 
        // we can use it through the property .shadowRoot
        const shadow = placeholderElement.shadowRoot;
    
        // based on which button we add the relevant style file
        if (event.target.value === "Style 1") {
            shadow.querySelector('link').setAttribute("href", "style1.css")
        } else {
            shadow.querySelector('link').setAttribute("href", "style2.css")
        }
    };
    

    Note: You have a typo in your JS, The handler is called handleclick lower case click but you're adding an uppercase Click handleClick to addEventListener