BEM notation and hardcoded classnames

Just curious. Does anybody here use BEM (“Block, Element, Modifier”) concept for WP-based projects?

I started using its CSS notation about 3 years ago, found it nice and got used to it. But as WP provides a bunch of core functions with hardcoded markup, I had to write some workrounds using filters and even regular expressions to replace classnames in default output.

For example nav-menus are my “favourite”. There are plenty of states and flags like “has-children”, “current-menu-item” or even “relation_current-page-…” etc. And they are hardcoded with no direct filters availiable.

I doubt that someone really uses all that falgs. I use barely 3-4 but have to loop through the whole array of ‘wp_nav_menu_objects’ to pass each classname of each item through the special procedure for splitting, and exploding, and comparing, and replacing, and splitting again…

So I think that it would be really nice if all hardcoded markup valuable strings (if they would present at all) must pass through apply_filters() or provide any similar mechanism. Something that allows me
array_merge( $all_default_classnames, array( ‘my_class_name_key’ => ‘my-renamed-class-name’ ) ).

P.S. I understand that replacing default classnames might cause theoretical compability issues with plugins, but when using custom Walkers in a custom theme, it doesn’t make any trouble. It’s my own responsibility, so I’d like to have an option.

2 Likes

I think there are some filters for these. I find all that markup so “noisy” that I usually get rid of it, and I think I use filters to do so. But not 100% sure.

Away from my desk now and won’t be back for a couple of days, but I will try to remember to check.

1 Like

Thanks for your reply, @timkaye!

At the moment I wrote those bem-specific functions (~2 years ago), there were no direct filters for classnames. At least, I could not find them, reading not only documentation, but the original “nav-menu-template.php” file too.

Just checked Codex. There are still only 3 related hook filters (others are for the different purposes).

Two of them contain already generated HTML (for seprate items and menu in whole):

wp_nav_menu_items / wp_nav_menu_{$menu->slug}_items
Filter Hook: Filters the HTML list content for navigation menus.

wp_nav_menu
Filters the HTML content for navigation menus.

…but parsing HTML in this case is a hell-alike, because classnames vary a lot and there are tons of combinations to search and replace. I use ‘wp_nav_menu’ + preg_replace to rename “.sub-menu” & “.sub-sub-menu” as those container classes don’t belong to ‘wp_nav_menu_objects’ and there is no other way to change them.

The third hook is exactly what I use in my current workaround:

wp_nav_menu_objects
Filter Hook: Filters the sorted list of menu item objects before generating the menu’s HTML.

It passes an array of menu items (as objects). Each item has a property $item->classes, containing an array of applied classnames. So, as I mentioned above, it’s a rather ugly workaround. Looping N items * M classnames and comparing each to a predefined list of possible values is not the best solution ever.


// Filtering Nav Menu objects for 

function bem_nav_menu_objects( $items, $menu_args ) {
	if ( ! empty( $items ) ) {
		foreach ( $items as $item ) {
			if ( ! empty( $item->classes ) ) {
				$item->classes = bem_nav_menu_item_classes(
					$item->classes,
					$menu_args->menu
				);
			}
		}
	}

	return $items;
}

add_filter( 'wp_nav_menu_objects', 'bem_nav_menu_objects' ,10, 2 );


// Replacing standard classnames in nav menu items

function bem_nav_menu_item_classes( $classes = array(), $menu_name = 'menu' ) {
	
	$default_class = 'menu-item';

	// Default BEM block settings
	$bem_block = $menu_name;
	$bem_element = 'item';
	$bem_mods = array();

	if ( ! empty( $classes ) ) {

		foreach ( $classes as $class ) {
			$mod_str = '';
			// Trying to split classname by standard prefix 'menu-item-'
			$temp = explode($default_class . '-', $class, 2);
			if ( ! empty( $temp[1] ) ) {
				// It may contain "has-children"
				if ( $temp[1] === 'has-children') {
					// Adding this flag as a boolean modifier
					$mod_str .= $temp[1];
				} else {
					// Other class names are key-value pairs, trying to split
					$m = explode( '-', $temp[1], 2 );
					if ( ! empty( $m[0] ) ) {
						// Using the key as modifier name
						$mod_str .= $m[0];
						if ( ! empty( $m[1] ) ) {
							// Formatting value to BEM notation (underslashes turn into hyphens)
							$mod_str .= '_' .
									str_replace( '_', '-', $m[1] );
						}
					}
				}
			} else {
				// If it's not a 'menu-item-...' class, assume it is a 'current_page_...'
				$temp = explode('current_page_', $class, 2);
				if ( ! empty( $temp[1] ) ) {
					//  Formatting it as a value for a 'relation' modifier
					//$mod_str = 'relation_current-page-' . $temp[1];
				} else {
					// It might also be a 'current-...' classname
					$temp = explode('current-', $class, 2);
					if ( ! empty( $temp[1] ) ) {
						// Current item should be marked by 'active' modifier
						if ($class === 'current-menu-item') {
							$bem_mods[] = 'active';
						}
						// It might be an ancestor or a descender, writing it to 'relation' modifier
						//$mod_str = 'relation_current-' . $temp[1];
					} else {
						// Maybe its a page? Checking for 'page-item-'
						$temp = explode('page-item-', $class, 2);
						if ( ! empty( $temp[1] ) ) {
							// It's a 'relation', too
							//$mod_str = 'relation_page-item-' .
									$temp[1];
						}
						// page_item class itself is a 'relation', once again
						elseif ( $class === 'page_item') {
							//$mod_str = 'relation_page-item';
						} else {
							// Custom class? Plugged? Unknown? Let it be...
							$mod_str = $class;
						}
					}
				}
			}
			// Adding a formed modifier to common array of modifiers
			$bem_mods[] = $mod_str;
		}
		// Removing possible duplicates
		$bem_mods = array_unique( $bem_mods );
	}

	// Generating proper BEM class with all that mofiers
	return bem_get_classes( $bem_block, $bem_element, $bem_mods );

}


// Replacing .sub-menu & .sub-sub-menu classes for conatiners (ul's)

function bem_nav_menu_filter( $nav_menu, $args ) {
	return preg_replace(
		'/((sub-)+)(menu)/i',
		$args->menu . '__$1$3',
		$nav_menu
	);
}
add_filter( 'wp_nav_menu', 'bem_nav_menu_filter', 9, 2);


// Replacing .sublink & .link in links attributes

function bem_nav_menu_link_attributes ( $atts, $item, $args, $depth ) {
	if ( $depth > 0) {
		$atts['class'] = bem_get_class( $args->menu, 'sublink' );
	} else {
		$atts['class'] = bem_get_class( $args->menu, 'link');
	}
	return $atts;
}
add_filter( 'nav_menu_link_attributes',
		'bem_nav_menu_link_attributes', 9, 4 );
...

And all that spaghetti is a result of hardcoded classnames. I’m not a skilled developer (not a developer at all, trully saying), so I can’t write a beautiful regexp which magically solves all my problems :slight_smile: And refactoring that nightmare hangs in my todo since 2017. Well, maybe next year :slight_smile:

Seems like you are having trouble by doing it backwards: trying to redo the existing classes instead of just styling them.

I wrote a proposal for a core function that filtered all attributes, so a theme could use it and all plugins could have a standard way to affect all of the theme’s output. Of course, there was pushback since core doesn’t use it, but I think it should, and there wouldn’t be the problem you are describing.
I use it in my theme, and it makes everything consistently easy to modify.

1 Like

BEM standard is not about decoration itself. The main idea is to refuse cascades. Using BEM notation incapsulates CSS rules within blocks, so each classname is practically unique across the whole project. It allows builing applications based on separate reusable components (atomic, react etc) and share them among projects easily. Also this structure is very suitable for using it with CSS preprocessors.

So the whole story is about standartization. BEM certainly has pros and contras both, but I’ve already made my chose and now jusе looking for a chance to use it in a bit more comfortable and natural way.

And I suggest there are plenty of other possible reasons to manage those excessive and heterogeneous classnames. That’s why I mentioned it here.

2 Likes

That looks interesting and promising. I’ll read the whole thread more attentively a bit later, but at first look your function seems quite useful.

To me, this says that BEM would not be useful in the context of the underlying system. Having unique class names in core would defeat the purpose of them being in core. Having standards is a good thing, but they should be generic in core and specific in pluigns and themes.
I saw this same problem with the new class names added to content by GB. It’s all backward, too specific. Standard names should be general, like alignleft. It’s a nightmare for a theme to style all the different “standard” classes for GB blocks when it’s so easy to style HTML elements, regardless of where they are and make a few exceptions for special case classes like menus.

2 Likes

I agree in general and don’t even try to force a new standard. I just asked for a filter — an optional possibility to change default behaviour for specific needs. Isn’t that a purpose of a filter?

BTW, “alignlefts” are very dramatic when partly redesigning huge projects. When somewhere in “custom-template-bla-bla-12344.php” a container has a custom stylization via such general classname, you can’t even imagine that while inspecting other instances on 99% of pages. And to my opinion that is a nightmare, because control is lost and no one in company knows)

But, I respect your vision as well. There is no “silver bullet” anyway, so pros and contras depend on personal needs and experience.

1 Like

@norske: I said I’d check when I got back. This is the function I use to clean up all that noise of CSS classes in the menu:

function wp_nav_menu_attributes_filter( $var ) {
	return is_array( $var ) ? array_intersect( $var, array( '' ) ) : '';
}
add_filter( 'nav_menu_css_class', 'wp_nav_menu_attributes_filter', 100, 1 );
add_filter( 'nav_menu_item_id', 'wp_nav_menu_attributes_filter', 100, 1 );
add_filter( 'page_css_class', 'wp_nav_menu_attributes_filter', 100, 1 );

So you can see that there are three filters that you might be able to use.

Just in case anyone is tempted to just copy and paste the function, be aware that it has the effect of removing the ability to highlight the menu item that represents the current page. If, like me, you need or want this function, but also want to be able to have the ability to highlight the menu item that represents the current page, you will need to use a little javascript to do so, like this:

var url = window.location.href;
$('#primary-menu a[href="' + url + '"]').addClass('current_page_item');

Obviously, the precise ID and class name will depend on your theme.

2 Likes

Brilliant. Thanks a lot, @timkaye!

That surprised me very much. I did not expect to find additional filters directly in Walker class (outside its view template “nav-menu-template.php”). Probably I misunderstand some aspects of WP output handling. I’ll reconsider this. Next time I won’t rely on templates only, digging it deeper.

Appreciate your help, it may turn into real use.

1 Like

Expect the unexpected :slight_smile:

I am fine with adding filters for something like this. The problem is there are a lot of places with hardcoded markup so we’d need to come up with a spec / scope first.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.