Anderson Pierre Cardoso

about

Simple Component Structure Pattern for Javascript

04 May 2014

This is a small pattern for javascript simple auto-loading components.

After working with js in many different ways, I stumbled on this simple structure pattern, and so far I'm loving its simplicity.

The idea is: You have a component starter function which receives a root element, when a components:start event is triggered, this function will find and start any element with a data-components attribute for that root tree.

Any component is a self contained unit, which should know only about itself, and talks to the 'external' world through events.

Lets see this in details below.

OBS: The examples are written in coffeescript. But these are easy to understand and translate to plain js.

Components

We are going to start backwards, to be more didatic.

An component should look something like:

<button id="button-id"
  data-components="grumpyButton"
  data-grumpyButton='{"message": "do not click-me! stop it!"}'>
  >=(
</button>

Now for adding behavior:

App.components.grumpyButton = (container) ->
  container: container

  init: ->
    @data = @container.data("grumpyButton")
    @startListeners()

  startListeners: ->
    @container.on 'click', @onClick.bind(this)

  onClick: (evt) ->
    alert @data.message

As we can see, the function with the component name is a factory, with receives a container and have a init function which starts the component behavior. Whenever we click the grumpyButton he opens an alert complaining.

A component modeled like this, is easily testable. You can pass a simple 'mocked' container, and test its methods in isolation easily.

Component Manager

Now for the component manager.

# Event bus for using pub/sub. You should alway use this mediator
# to communicate between components
mediator =
  obj: $({})
  publish: (channel, data) -> @obj.trigger(channel, data)
  subscribe: (channel, fn) -> @obj.bind(channel, fn)
  unsubscribe: (channel, fn) -> @obj.unbind(channel, fn)

# components namespace
components = {}

# Initialize a component and add the instance to the container data
setupContainer = (container) ->
  container = $(container) unless container.jquery

  # get components names,
  # e.g, [data-components="modal editor"] => ["modal", "editor"]
  names = container.data('components').split /\s+/
  _.each names, (name) =>
    # get the component factory
    component = components[name]?(container)
    # initialize the component
    component.init()


# setup all components for a DOM root
startComponents = (evt, root=document) ->
  $(root).find('[data-components]').each (i, container) =>
    setupContainer(container)


mediator.subscribe 'components:start', startComponents

# setup global App namesmpace
window.App =
  mediator: mediator
  components: components

The manager handles the setup of our components.

Finnaly start listening:

$ ->
  App.mediator.publish 'components:start'

You can pass any root element to the components:start. I.e, if you have component, lets say a modal box, which load some html via ajax, all you need to do is:

App.mediator 'components:start', receivedHtml.

Observations

Now with our manager in place you can easily write self contained reusable and composable components.

If you are using something like Rails, you can combine this with partials and the asset pipeline, making this even more awesome, by loading only the necessary components for each page.

Since I'm a huge fan of simplicity, i've really enjoyed this way of organizing my pieces of javascript behavior, making them modular and easily composable.

A final real world example

This is a component for modal boxes We are using on Meppit (a MootiroMaps rebuild).

#= require jquery.modal

App.components.modal = (container) ->
  {
    container: container

    defaults: {
      fadeDuration: 150
      zIndex: 200
    }

    init: ->
      @data = @container.data("modal") || {}
      @target = if @data.remote || @data.autoload then @container else @referedElement()
      @start()

    referedElement: ->
      $("#{ @container.attr('href') }")

    open: () ->
      @target.modal(@defaults)
      false

    startComponents: () ->
      setTimeout( ->
        currentModal = $('.modal.current')
        App.mediator.publish('components:start', currentModal)
      , @defaults.fadeDuration)

    start: ->
      if @data.autoload
        @open(this)
      else
        @container.on 'click', @open.bind(this)

      if @data.remote
        # trigger components:start for ajax loaded elements
        @container.on 'modal:ajax:complete', @startComponents.bind(this)

  }

For using it I can do:

<!-- =====================================================
Simple html element modal
-->
<a href="#my-modal" id="my-modal-btn" data-components="modal">
  Open Modal
</a>

<div id="my-modal"> ... content ... </div>
<!-- for styling we have a `@include modal` sass mixin -->


<!-- =====================================================
Ajax loading modal
-->
<a href="/path/to/my/action" id="my-modal-btn"
                             data-components="modal"
                             data-modal='{"remote": true}'>
  Open Modal
</a>

<!--
on my controller
def action
  render :layout => nil # render /action.html.erb without layout. Any
                        # component inside the generated DOM, will be
                        # automatically loaded by the modal.
end
-->

Thats it! :)




You can leave a comment on the corresponding github issues page => Comments