Zaqx Zaqx - 4 months ago 13
Javascript Question

PostCSS: How to conditionally add a new rule based on declaration in current rule?

Hello I’ve been experimenting with PostCSS and am struggling a bit with writing a plugin that appends a new rule when it finds certain CSS properties.

What I’m trying to achieve…

Starting CSS:

.selector {
color: red;
not-yet-standard-property-a: 10px;
not-yet-standard-property-b: 20px;
}


After PostCSS plugin:

.selector {
color: red;
not-yet-standard-property-a: 10px;
not-yet-standard-property-b: 20px;
}

.ie .selector {
standard-property-a: 10px;
standard-property-b: 20px;
}


It’s easy for me to add a new rule whenever I see one of these
not-yet-standard-property-*


return function (css) {
css.walkRules(function (rule) {
rule.walkDecls('not-yet-standard-property-a', function (decl) {
css.insertAfter(rule, '.ie ' + rule.selector + '{ standard-property-a: '+ decl.value +' }');
});
rule.walkDecls('not-yet-standard-property-b', function (decl) {
css.insertAfter(rule, '.ie ' + rule.selector + '{ standard-property-b: '+ decl.value +' }');
});
rule.walkDecls('not-yet-standard-property-c', function (decl) {
css.insertAfter(rule, '.ie ' + rule.selector + '{ standard-property-c: '+ decl.value +' }');
});
});
}


But the output is not ideal…

.selector {
color: red;
not-yet-standard-property-a: 10px;
not-yet-standard-property-b: 20px;
not-yet-standard-property-c: 53px;
}
.ie .selector {
standard-property-c: 53px;
}
.ie .selector {
standard-property-b: 20px;
}
.ie .selector {
standard-property-a: 10px;
}


Ideally the new rule would only be added once after it’s walked over the entire rule but PostCSS doesn’t seem to allow conditionals inside of the walkRules function so I’m not sure how to block it from creating a new rule for every rule it sees.

I‘ve linked to a demo of the above code. Any suggestions for better architecture would be greatly greatly appreciated, like I said before, I’m rather new to this. Thanks!

Demo



Edit: Solved. Many thanks to Andrey Sitnik! I’ve marked his answer as correct since the concepts in it were what I needed. For those looking for a complete solution to this see the following code (and demo!):

function isNotYetStandardProp (propName) {
if (propName.indexOf('not-yet-standard-property-') > -1) {
return true;
}
else {
return false;
}
}

function transformPropName (propName) {
return propName.replace("not-yet-", "");
}

var skipNext = false;
var convertedProps = [];
var newRule;
var newNode;

return function (css) {

css.walkRules(function(rule) {
// The skipNext flag is used to prevent walkRules from walking onto any
// newly created rules and recursively creating new rules based on the
// newly added rule.
if (!skipNext) {
// Reset to remove values from last loop
convertedProps = [];
newRule = null;
newNode = null;

// Search through nodes of current rule and build up array
// containing each property that needs to change.
rule.nodes.filter(function (node) {
if ((node.type === 'decl') && isNotYetStandardProp(node.prop)) {
newNode = node.clone();
newNode.prop = transformPropName(node.prop);
convertedProps.push(newNode);
}
});

// If there were properties to convert in this rule create a new
// rule to put them in.
if (convertedProps.length > 0) {
newRule = rule.cloneAfter({ selector: '.ie ' + rule.selector });
newRule.removeAll().append(convertedProps);
skipNext = true;
}
}
else {
// We only want to skipNext once.
skipNext = false;
}
})

}


Demo of solution


Answer

You don’t need a walkDecls inside walkRules, just work with rule.nodes:

css.walkRules(rule => {
    const nonStandard = rule.nodes.filter(node => {
        return if node.type === 'decl' && checkPropName(node.prop);
    });
    if ( nonStandard.length > 0 ) {
        const clone = rule.cloneAfter({ selector: '.ie ' + rule.selector });
        clone.append(nonStandard);
    }
})