Technologies: React, Typescript

I'm sure this is a duplicate, but I just don't know how to look for this...

I want to create a wrapper for the styled-components package...
API's reference for the package that I'm talking about

Wherever I create a component I have to call it like this:

import styled from 'styled-components'

const mydiv = styled.div<WhateverInterface>`
  height: 20px;
`

This is the only place I ever saw this kind of notation, where at the end of the declaration there is just a string...

Wherever I pass a prop to this specific div, to have it overwrite the default value, I would need to do something like this:

import styled from 'styled-components'

const mydiv = styled.div<WhateverInterface>`
  height: ${props => props.height ? props.height : '20px'};
`

I created a function that does this for all keys, so I would like to create a wrapper for the styled.[div,select,span,etc]... a function that would essentially be called like this:

import styled from 'styled-components'

function wrapper(styleComponent, rules) {
   return styledComponent`
       ${props => mergePropsAndDefaultRules(props, rules)}
   `
}

const mydiv = wrapper(styled.div<WhateverInterface>, `
    height: 20px;
`);

The problem is... I don't know how this string that comes at the end of styled.div<WhateverInterface> works, behaves, etc... where does the prop inside it even come from?

Any information about this string thing will be much appreciated <3


PS: in case it was not clear, the function I randomly inserted in my code mergePropsAndDefaultRules would work something like this:

  • For each CSS property in each of them, it would check if it existed in both..if existed in both, set the property to the value in the props, if existed only in default, set the css property to the value inside the default rules, same for the props...

example:

props = {
  height: '30px',
  width: '20px',  
}

defaultRules = `
   height: 20px;
   border: 1px solid black;
`
const output = mergePropsAndDefaultRules(props, defaultRules)
console.log(typeof output, output)
/* 
Output:
string,
border: 1px solid black;
height: 30px;
width: 20px;
*/

1 Answers

0
Alexandre Dias On

So... I figured it out thanks to @Tholle's comment...


type CSSStyle = {[key: string] : (string|boolean)};

type CSSProperty = CSSStyle | {[key:string]: CSSStyle};

type Styles =  {[key: string] : CSSProperty};

interface IStyleGenerator {
    generateCSS: (props: Styles, standart: string) => string;
}

const CaseHelper = {
    snakeToCamel: s => s.replace(/(\-\w)/g, function(m){return m[1].toUpperCase();}),
    camelToSnake: c => c.replace(/([A-Z])/g, a => `-${a.toLowerCase()}`)
}

class LiberStyle implements IStyleGenerator {
    readonly defaultNamespace : string = 'standart';

    /**
     * 
     * @param propsStyle the style field from the props
     * @param standartStyle the css rules as string
     */
    generateCSS(propsStyle: Styles, standartStyle: string) : string {
        let rules = standartStyle.split('\n').map(a => a.trim()); // O(n * k);
        const defaultStyle = {};
        let namespace = this.defaultNamespace;
        defaultStyle[namespace] = {};
        const namespacePattern = /&:(\w+).+/;
        const hasDoubleMark = new RegExp(/:/);
        for(let rule of rules) {
            if(rule.length <= 0) continue;
            rule = rule.trim();
            if(rule.length <= 2) continue;
            if(rule === '}') {
                namespace = this.defaultNamespace
            } else {
                if(rule.length >= 2 && rule[0] === '&' && rule[1] === ':') {
                    namespace = rule.replace(namespacePattern, '$1');
                    defaultStyle[namespace] = {};
                } else {
                    if(!hasDoubleMark.test(rule)) continue;
                    const [key, value] = this.extractKeyValue(rule);
                    defaultStyle[namespace][key] = value;
                }
            }
        }
        const propsRemappedStyle = {};
        propsRemappedStyle[this.defaultNamespace] = {};
        Object['entries'](propsStyle).forEach(entry => {
            const [nmspace, rules] = entry;
            if(typeof rules === 'object') {
                propsRemappedStyle[nmspace] = rules
            } else {
                propsRemappedStyle[this.defaultNamespace][nmspace] = rules;
            }
        })
        return this.stringifyRules(propsRemappedStyle, defaultStyle);

    }

    /**
     * 
     *  PRIVATE METHODS ---------------------------------------------
     * 
     */


    extractKeyValue(rule : string) {
        let [key, value] = rule.split(':');
        value = value.trim();
        key = CaseHelper.snakeToCamel(key.trim());
        return [key, value.substring(0,value.length - 1)];
    }

    myInclude(array : any[], value : string) {
        let found = false;
        array.forEach((e:any) => {
            found = e === value;
            return found;
        })
        return found;
    }

    getNamespaceKeys(propKeys : any[]= [], defaultKeys : any[] = []) {
        let keys = defaultKeys.filter(style => !this.myInclude(propKeys,style));
        let ret = keys.concat(propKeys);
        // console.log({ret , propKeys, defaultKeys});
        return ret;
    }

    getKeysAndNamespace(propStyles : Styles, defaultStyles : Styles) {
        let namespacedKeys = Object.keys(propStyles)
        let namespaceMissingKeys = Object.keys(defaultStyles).filter((nmspace:string) => (!this.myInclude(namespacedKeys,nmspace) && (!this.defaultNamespace)));
        namespacedKeys = namespacedKeys.concat(namespaceMissingKeys);
        let allKeys = {};
        namespacedKeys.forEach(namespace => {
            allKeys[namespace] = this.getNamespaceKeys(Object.keys(propStyles[namespace] || {}), Object.keys(defaultStyles[namespace] || {}));
        })
        return { keys: allKeys, namespaces: namespacedKeys };
    }


     stringifyNamespace(namespace, keys, propRules, defaultRules) {
        let unifiedRules = '';
        let tab = namespace !== this.defaultNamespace ? '\t\t' : '\t'; 
        const camelToSnake = CaseHelper.camelToSnake;
        keys.forEach(key => {
            unifiedRules = `${unifiedRules}\n${tab}${camelToSnake(key)}: ${propRules[key] ? propRules[key] : defaultRules[key]};`
        });
        return namespace !== this.defaultNamespace ? `\t&:${namespace} {${unifiedRules}\n\t}` : unifiedRules;
    }

    stringifyRules(propsStyle : Styles, defaultStyle : Styles) {
        const  { keys, namespaces } = this.getKeysAndNamespace(propsStyle, defaultStyle);
        // console.log({keys, namespaces, propsStyle, defaultStyle });
        return namespaces.reduce((final, namespace) => {
            return `${final}\n${this.stringifyNamespace(namespace, keys[namespace], propsStyle[namespace], defaultStyle[namespace])}`
        }, '');
    }

}

export default new LiberStyle();

The code is not perfect... but you can use it creating a wrapper like this:

import LiberStyle from './liberStyle';

interface A {
}

interface B {

}

function generic(oi) {
    return 
}

const __props__ : B = {
    style: { 
        height: '40px',
        hover: {
            color: 'red'
        }
    },
    show: true

}
/**
 * This is just a function I created to mimick styled.div because I didnt have it installed on the PC I wrote this code
 */
function styled_div<T>(staticStrings: TemplateStringsArray, ...params) {
    let merged = '';
    const size = Math.max(staticStrings.length, params.length);
    for(let i=0; i < size; i++) {
        if(staticStrings[i]) {
            merged = merged + staticStrings[i].trim();
        }
        if(params[i]) {
            if(typeof params[i] === 'function') {
                merged = merged + params[i](__props__);
            } else if(typeof params[i] === 'object') {
                merged = merged + JSON.parse(params[i]);
            } else if(typeof params[i] === 'string') {
                merged = merged + params[i].trim();
            } else {
                merged = merged + params[i];
            }
        }
    }
    return merged;
}

function merge(props, staticStrings: TemplateStringsArray, params: any[]) {
    let merged = '';
    const size = Math.max(staticStrings.length, params.length);
    for(let i=0; i < size; i++) {
        if(staticStrings[i]) {
            merged = merged + staticStrings[i].trim();
        }
        if(params[i]) {
            if(typeof params[i] === 'function') {
                merged = merged + params[i](props);
            } else if(typeof params[i] === 'object') {
                merged = merged + JSON.parse(params[i]);
            } else {
                merged = merged + params[i].trim();
            }
        }
    }
    return merged;
}


const styled = {
    div: function div<T>(staticStrings: TemplateStringsArray, ...params) {
        return styled_div<T>`
            ${props => {
                const m = merge(props, staticStrings, params);
                //console.log({ style: props.style, m});
                return LiberStyle.generateCSS(props.style,m)}
            }
        `
    },
}

const waka = styled.div<A>`
    display: ${props => props.show ? 'block' : 'none'};
    height: 20px;
    width: 30px;
`;

console.log(waka);

It's not perfect, but I hope it sheds some light if anyone is having the same problem...