Extensible className additions


#1

The application I’m working on depends upon DOM layout considerations which demand given parent nodes to qualify child components’ root nodes with classNames. This mixin has made my life easy for a while:

// classy.js
import Ember from 'ember';

// External context often defines the requirements of the wrapping element
// Enable generic component root customisation through passed in properties
export default Ember.Mixin.create( {
  classNameBindings : [ 'classNameProperty' ],
  classNameProperty : Ember.computed( 'class', className )
} );

It is now possible to invoke things like this:

// component-x.js
import Ember from 'ember';
import classy from '../lib/classy';

export default Ember.Component.extend( classy );
<div class="grid column">
  {{component-x class="grid__item"}}
</div>

This has been serving me well. The handlebars templates read nicely, I minimise DOM cruft and end up with an idiomatic flexible layout. The problem is that, despite the adorable signature of the classNameBindings API — namely an array of strings which are evaluated for one of Ember’s several internal logic-as-a-string DSLs — classNameBindings is not extensible. As cool as arrays of custom logic-syntax string DSLs are, they’re still just static properties which defy inheritance & composition. Therefore as soon as component-x gets internal requirements for qualifying its root node with classes, the classy mixin behaviour is silently lost.

If we had such a thing as a classNameBind method, which performed arbitrary logic and returned a string, then we could rely on invoking this._super to ensure any extension was non-destructive. However I’m having difficulty figuring out how this would integrate with observability concerns etc.

Any ideas? :smiley:


#2

All this is the result of not having found the right parts of the docs (I still can’t, but if I tentatively opened a ticket every time I thought a feature should exist but I couldn’t find it, I wouldn’t get any work done). It turns out that you can totally pass in class, and classNameBindings will continue to work, and classNames is a valid component property which will be consumed as expected. So the original problem — namely the ability to easily set classes from within a component while simultaneously allowing classes to be passed in — is a non-issue. The problem of extensibility can be achieved with init modifiers to the classNameBindings property. This isn’t all intuitive, so I wrote a plugin for it: this provides a mixin factory which will introduce new classNames or classNameBindings without overwriting compositions & inheritances:

import Ember from 'ember'

const pushTo = key => ( ...items ) =>
  Ember.Mixin.create( {
    init(){
      this._super( ...arguments )

      this[ key ].push( ...items )
    }
  } )

const names    = pushTo( 'classNames' )
const bindings = pushTo( 'classNameBindings' )

export default Object.assign( names, { names, bindings } )

The above relies on the fact that lifecycle methods such as init can recursively call the methods previously bound. Therefore I can safely use any number of inits to perform dynamic logic as long as each makes sure to invoke its _super. The exposed API works as follows:

import Ember  from 'ember'
import classy from './path/to/classy'

export default Ember.Component.extend(
  classy( 'my-component', 'variadic-classes' ),

  classy.names( 'same-as-above-just-more-explicit' ),

  classy.bindings( 'lets:reinvent:ternaries' ),

  componentDefinition
)

As demonstrated above I can, for clarity, invoke the factory several times in one component and rely on all bindings to perform. Similarly, I could reopen or extend this component and introduce more.

Hope this proves of use to someone else when they need it.