4
\$\begingroup\$

I'm starting out on a project that will standardise some increasingly complex webcomponents. In a final version the goal is to support multiple different design systems (as the components might be used for different brands, which require different color schemes, border radiuses, margins, etc.). This is my attempt at creating a template that can easily be adapted for other (and more complex) webcomponents. For now, it's only a button with different styles / sizes and limited functionality. Colors are only placeholders for now and will most likely be refactored to variables. Once I'm confident in the approach I'll extend functionality and start on the next components.

I expect there to be a lot (!) of room improvement, as this implementation is just what seemed the most intuitive to me when starting to work with webcomponents.


component-button.js

class ComponentButton extends HTMLElement {

    constructor() {
        super();

        this.attachShadow({mode: 'open'});
    }

    connectedCallback() {    
        this.defineProperties(props);

        this.render();
    }


    /**
     * Define properties (including getters and setters) for componentButton
     * @param props Array of property objects {name, validValues, defaultValue}
     */
    defineProperties(props) {
        const props = [
            { name: "variant", validValues: new Set(["primary", "secondary", "text"]), defaultValue: "primary" },
            { name: "size", validValues: new Set(["s", "m", "l"]), defaultValue: "m" },
            { name: "disabled", validValues: new Set(["true", "false"]), defaultValue: "false" }
        ];

        for (let { name, validValues, defaultValue } of props) {
            const attribute = this.getAttribute(name);
            const initialValue = validValues.has(attribute) ? attribute : defaultValue;

            Object.defineProperty(this, name, { value: initialValue });
        }
    }

    /**
     * Getter for all relevant css-classes from relevant properties
     * @returns {string}
     */
    get classes() {
        let clss = [
            `my-button`,
            `my-button_${this.size}`,
            `my-button_${this.variant}`
        ];

        if (eval(this.disabled)) {
            clss.push(`my-button_disabled`)
        }

        return clss.join(" ");
    }

    render() {
        this.shadowRoot.innerHTML = `
            ${this.styles}

            <button class="${this.classes}">
                <slot class="my-button__slot"> Click </slot>
            </button>
        `;
    }

    /**
     * Getter for all relevant css-styles
     * @returns {string}
     */
    get styles() {
        return `
            <style>
                .my-button {
                    position: relative;
                    overflow: hidden;
                    display: flex;
                    margin: 5px;
                    justify-content: center;
                    padding: 18px 30px;
                    text-align: center;
                    text-decoration: none;
                    cursor: pointer;
                    font-weight: 300;
                    font-family: inherit;
                    transition: all .3s cubic-bezier(.75, .02, .5, 1);
                }
                
                .my-button_s {
                    font-size: 16px;
                    min-width: 144px;
                }
                
                .my-button_m {
                    font-size: 18px;
                    min-width: 162px;
                }
                
                .my-button_l {
                    font-size: 20px;
                    min-width: 180px;
                }
                
                .my-button_primary {
                    border: 1px solid darkblue;
                    background-color: darkblue;
                    color: #FFF;
                }
                
                .my-button_primary:hover {
                    border: 1px solid blue;
                    background-color: blue;
                    color: #FFF;
                }
                
                .my-button_primary.my-button_disabled {
                    border: 1px solid #333;
                    background-color: #333;
                    color: #FFF;
                    cursor: not-allowed;
                }
                
                .my-button_secondary {
                    border: 1px solid darkblue;
                    background-color: transparent;
                    color: darkblue;
                }
                
                .my-button_secondary:hover {
                    border: 1px solid blue;
                    background-color: transparent;
                    color: blue;
                }
                
                .my-button_secondary.my-button_disabled {
                    border: 1px solid #4c4c4c;
                    background-color: transparent;
                    color: #4c4c4c;
                    cursor: not-allowed;
                }
                
                .my-button_text {
                    border: none;
                    background-color: transparent;
                    color: darkblue;
                }
                
                .my-button_text.my-button_s, .my-button_text.my-button_m, .my-button_text.my-button_l {
                    min-width: auto;
                }
                
                .my-button_text .my-button__slot {
                    display: block;
                    position: relative;
                }
                
                .my-button_text:hover {
                    color: blue;
                }
                
                .my-button_disabled {
                    color: #4c4c4c;
                    cursor: not-allowed;
                }
            </style>
        `;
    }
}

window.customElements.define('component-button', componentButton)

index.html

For visual testing

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Button Visual Test</title>

    <script src="./component-button.js"></script>
</head>
<body>

<div style="display: flex; flex-direction: column">

    <component-button> Full Default Button </component-button>

    <component-button variant="secondary"> Default Secondary Button </component-button>

    <component-button variant="text"> Default Text Button </component-button>

    <component-button variant="primary" size="s" disabled="true"> Primary Size_S disabled </component-button>

    <component-button variant="secondary" size="m" disabled="true"> Secondary Size_M disabled </component-button>

    <component-button variant="text" size="l" disabled="true"> Text Size_L disabled </component-button>

</div>

</body>
</html>
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

Not really a full answer: I would rename defineProperties to resolveProperties which is a more commonly used name for this type of action.

Maybe I'd move the props to the top of the class, as first line. My JS isn't what it used to be, but in most languages it's good practice to place "configurable settings" at the top, so that when you make a small change which has nothing to do with logic (eg a default value), you don't have to go through actual logic to search for it.

Personally I really really really dislike styling in components and think that should be part of a S/CSS file, but it seems that the trend is to do it inline.

\$\endgroup\$
4
  • \$\begingroup\$ Thanks you for your suggestions. I'd also probably prefer moving styles outside the component, do you have any pointers towards best practices? \$\endgroup\$ Commented Oct 11, 2021 at 11:32
  • \$\begingroup\$ I've been thought that styling should be done in a stylesheet :P And with scss these days, you can seperate everything into easy files, like a buttons.scss and form.sss etc. I like that method, but to each their own \$\endgroup\$
    – Martijn
    Commented Oct 11, 2021 at 12:29
  • \$\begingroup\$ Yeah I understand, but I expected there to be some things to watch out for with regards to the shadow DOM. Since I want the component itself to have some styles that don't interfere with my global stylesheet. \$\endgroup\$ Commented Oct 11, 2021 at 12:56
  • \$\begingroup\$ Thats too long ago for me to say something trustworthy about, heheh \$\endgroup\$
    – Martijn
    Commented Oct 11, 2021 at 13:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.