2

What's the correct approach to accessing a component in an event handler? Or, what should I be doing instead?

I have a UI section that is essentially a div-button. When the user clicks it, then the "button" is replaced with an input. When the user is finished inputting, it goes back to a "button".

Because I'm porting this from Backbone, I was using jQuery to flip visibility on the two pieces and set focus on the input. After reading about how config functions in components are given the component, I was wondering if I should be given the component's DOM element, or take another approach entirely (maybe conditional within m() functions?).

State 1 State 2

{
    controller: function () {
        this.clickPlus = () => {
            $('#newActivityPlusIcon').hide()
            $('#newActivityPlaceholder').css('background-color', 'lightgray')

            const $newActivityInput = $('#newActivityInput')
            $newActivityInput.show()
            $newActivityInput.focus()
        }
        this.keyUpInput = (event) => {
            //If ESC key pressed
            if (event.keyCode === 27) {
                const $newActivityInput = $('#newActivityInput');
                $newActivityInput.hide()
                $newActivityInput.val('')

                $('#newActivityPlaceholder').css('background-color', '')
                $('#newActivityPlusIcon').show()
            }
        }
    },
    view: (ctrl) => {
        return m('section', {id: 'newActivity'}, [
            m('article', {id: 'newActivityPlaceholder', class: 'activityBox', onclick: ctrl.clickPlus}, [
                m('span', {id: 'newActivityPlusIcon'}, '+'),
                m('input', {id: 'newActivityInput', placeholder: 'type name', onkeyup: ctrl.keyUpInput}),
            ])
        ])
    }
}
Barney
  • 16,181
  • 5
  • 62
  • 76
Eric Majerus
  • 1,099
  • 1
  • 12
  • 23

2 Answers2

2

Personally, I would expect to go with a conditional which means that the DOM only contains the element that's currently pertinent.

A rough JSBin example of how I think I'd structure this: http://jsbin.com/razisicelo/edit?html,js,output

The use of ctrl.inputValue is just illustrative.

Bryce
  • 391
  • 3
  • 8
2

The crucial practical difference between Mithril & Backbone components is that Mithril views are written in Javascript. Some of the old best practices of web MVC are turned on their head as a result of the opportunities this opens up:

  1. You can easily express conditional logic in views themselves. As Bryce shows, ternary operations ( condition ? true : false ) are a great way of making a view self-complete: in other words, the view function can express all possible states for the DOM, and everything it needs to do.
  2. You can also define event handlers and element bindings in the view itself, meaning you no longer need the controller to be aware of the structure of the view, or define view logic. This enables you to write controllers whose sole function is to define the state and holistic actions of a component.

The special config property is only necessary if you absolutely need to pass a reference to a DOM element to the controller or another component - which is rarely the case - or to trigger special logic like animation the first time the element is rendered. In this case, it isn't necessary.

In the code below, I've managed to isolate 2 properties, input and active, and one action, cancel, that resets those properties. These describe all the state the component needs. The rest is purely the view's concern.

The view decides how to read from and write to these properties as dictated by the concerns of the DOM API. For example, how to read an input's value is a concern of the input itself: likewise, determining that the keyCode 27 signifies 'esc' is DOM logic and can be kept separate from the controller logic of changing state.

Separating these concerns becomes useful when UX and business requirements change: as an example, I've taken the arbitrary decision to make clicking off or tabbing away from the input also clear the input and loose active state. This doesn't require adding an extra action in the controller - we bind the same cancel function to the onblur event handler.

Other small tweaks:

  • Expressing the view as an arrow function looses the return keyword, which IMO makes it easier to read the view as a single declarative statement.
  • We can loose the IDs, since we no longer need to reference the view from the controller: all our state bindings are made directly in the view itself.
  • Static attributes like class and placeholder can be expressed directly in the selector string. This makes it clear that these attributes are always the same: now the DOM attributes object is only invoked for dynamic code - functions and conditional properties.
  • Array brackets to group children are redundant: you can simply insert the children directly one after the other.

https://jsbin.com/xopinoy/edit?js,output


{
  controller: function () {
    this.active = m.prop( false )
    this.input  = m.prop( '' )

    this.cancel = () => {
      this.active( false )
      this.input( '' )
    }
  },

  view: ctrl =>
    m( 'section',
      m( 'article.activityBox', {
        onclick: event => 
          ctrl.active(true),
        style : {
          background : 
            ctrl.active() 
              ? 'lightgrey' 
              : ''
        }
      },

        ctrl.active()
          ? m( 'input[placeholder="type name"]', { 
              value: ctrl.input(),
              oninput: event =>
                ctrl.input( event.target.value ), 
              onkeyup: event => {
                if( event.keyCode === 27 )
                  ctrl.cancel()
              },
              onblur: ctrl.cancel
            } )
          : m( 'span', '+' )
      )
    )
} 
Barney
  • 16,181
  • 5
  • 62
  • 76