Docs

Everything You Need to Know

Tutorials

Organizing Your Code

It's About Parts

The part of ChocolateChip-UI that you will always use, regardless of the type of app you're making, is components. The second part that you'll probably need to use is a navigation list with routing. To better understand how you can organize your app code, you need to know how components work.

Components

Although we write code to make an app, when we run it, we see a UI. ChocolateChip-UI's components let you break that UI down into managable chunks. You can create components to do this in three ways. You can have a component render data to the screen. This might be just a snippet of text, or an object or a array of objects. You can also add event listeners to your components. You can also define the template the component uses when you setup the component. This results in a complete component where you do not need to look elsewhere to understand its functionality. Or you could create a component whose only purpose is to wire up actions for the UI.

When your app has a lot of components, it gets hard to dig through the code to find the component you want. You can combine all of your components into one object as a namespace. Below we show a before and after:

// Scope components to App object:
const app = {

  // Define components:
  specialRedsComponent: new Component({
    element: '#picksRed',
    render: (wine) => html`<li>${wine}</li>`
  }),

  specialWhitesComponent: new Component({
    element: '#picksWhite',
    render: (wine) => html`<li>${wine}</li>`
  }),

  selectedWineComponent: new Component({
    element: '#wineDetail',
    render: (wine) => html`<li>${wine}</li>`
  }),

  filteredWinesComponent: new Component({
    element: '#filteredWines',
    render: (wine) => html`<li>${wine}</li>`
  }),

  wineryComponent: new Component({
    element: '#componentWinery',
    actions: [{
      event: 'tap',
      callback: function() {
        const location = $(this).attr('data-location')
        window.location.href= 'http://maps.myApple.com/?q=' + location
      }
    }]
  })
}

// Render the components:
myApp.SpecialRedsComponent.render(bestWines[0].data)
myApp.SpecialWhitesComponent.render(bestWines[1].data)

In the above example, the last component, WineryComponent, is only for wiring up an event. That's why it has the property noTemplate. The other components are all callable off the App object. This gives you namespacing and one place to look for all of your components.

Another common problem with UIs are event registration. If you have a lot of interactive elements, such as buttons, etc., you can wind up with actions dispersed throughout your code. It can be hard to find the event your looking for. In the following example, a screen had a lot of elements with registered actions. We created a component for the screen and attached all those loose event there. When you add actions to a component, ChocolateChip-UI uses event delegation based on the component's element. In this case its the screen.

// Create a component to register actions:
const appComponent = new Component({
  element: '#main',
  actions: [
    {
      event: 'keypress',
      element: '#add-todo',
      callback: function(e) {
        if (e.keyCode == 13) {
          const todo = $('#add-todo').val();
          $('#add-todo')[0].value = '';
          if (todo) {
            todosData.unshift({state: 'active', value: todo, id: $.uuid()});
            renderActiveTodos(todosData);
            todoComponent.render(todosData);
            toggleButtonState('#show-all');
          }
          localStorage.setItem('chui-todo', JSON.stringify(todosData))
        }
      }
    },
    /**
     * Add todo item:
     */
    {
      event: 'tap',
      element: '.add',
      callback: function() {
        console.log('adding todo')
        const todo = $('#add-todo').val()
        $('#add-todo')[0].value = ''
        if (todo) {
          todosData.unshift({state: 'active', value: todo, id: $.uuid()})
          renderActiveTodos(todosData)
          todoComponent.render(todosData)
          toggleButtonState('#show-all')
        }
        localStorage.setItem('chui-todo', JSON.stringify(todosData))
      }
    },
    /**
     * Set state of todo:
     */
    {
      event: 'tap',
      element: '.set-state',
      callback: function() {
        const parent = $(this).closest('li').array[0]
        const id = parent.dataset.id
        parent.classList.toggle('active')
        const state = parent.classList.contains('active') ? 'active' : 'completed'
        const index = todosData.findIndex(todo => todo.id == id)
        todosData[index].state = state
        renderActiveTodos(todosData)
        localStorage.setItem('chui-todo', JSON.stringify(todosData))
      }
    },
    /**
     * Delete a todo:
     */
    {
      event: 'tap',
      element: '.delete-item',
      callback: function() {
        const id = this.dataset.id
        const index = todosData.findIndex(todo => todo.id === id)
        /**
         * Remove item from list:
         */
        todosData.splice(index, 1)
        todoComponent.render(todosData)
        renderActiveTodos(todosData)
        localStorage.setItem('chui-todo', JSON.stringify(todosData))
      }
    },
    /**
     * Handle visibility of todo items  by state.
     * Here we're adding even to all buttons,
     * then using the button id to determine 
     * what to do in the callback.
     */
    {
      event: 'tap',
      element: 'button',
      callback: function() {
        const id = this.id
        const todoItems = $('#todo-items li')
        switch(id) {
          /**
           * Show all todos:
           */
          case 'show-all':
            todoItems.css({display: '-webkit-flex', 'display': 'flex'});
            toggleButtonState(this);
            break
          /**
           * Show only active todos:
           */
          case 'show-active':
            todoItems.forEach(item => {
              if (item.classList.contains('active')) {
                $(item).css({display: '-webkit-flex', 'display': 'flex'})
              } else {
                $(item).hide()
              }
            })
            toggleButtonState(this);
          break
          /**
           * Show only completed todos:
           */
          case 'show-completed':
            todoItems.forEach(item => {
              if (item.classList.contains('active')) {
                $(item).hide()
              } else {
                $(item).css({display: '-webkit-flex', 'display': 'flex'})
              }
            })
            toggleButtonState(this)
          break
        }
      }
    }
  ]
})

Routes

The other main item you will use in your app is routes. You set up routes by creating a router object and defining routes on it. This means there's just one place to look for routes:

// Define Routes:
//===============
const routes = new Router()
routes.addRoute([
  {
    route: 'fragranceList',
    callback: function(genre) {
      chosenGenre = []
      fragrances.forEach(function(fragrance) {
        if (fragrance.genre === genre) {
          chosenGenre.push(fragrance)
        }
      })
      AvailableFragrancesComponent.render(chosenGenre)
      FragrancesGenreTitleComponent.render([genre])
      $('#backToGenre span').text(genre)
    }
  },
  {
    route: 'detail',
    callback: function(sku) {
      const chosenFragrance = chosenGenre.filter(function(fragrance) {
        return fragrance.sku === sku
      })
      FragranceDetailComponent.render(chosenFragrance)
      DetalTitleComponent.render([chosenFragrance[0].product_title])
      $('#addTocart').data('fragrance', chosenFragrance[0])
    }
  }
])

Pubsub/h3>

If you want to decouple your code, you'll use mediators. However, you'd probably never have more than a couple. You can namespace them with components or routes or however you like.

Create Namespaces

If you have a complex app with different things going on, create separate namespaces for each division. For example, Registration, Shoppingcart, etc. Add all the relavant code to the namespace. That way all the code related to a block of functionality is in one place. Below is a Shopping cart namespace:

const cart = {

  cartComponent: new Component({
    element: '#purchaseItems',
    state: cartState,
    render: ('item') => html`
      <li>${ item }</li>`
  }),

  totalItemsComponent: new Component({
    element: '#totalItems',
    render: ('total') => html`${ item }`
  }),

  totalCostComponent: new Component({
    element: '#totalCost',
    render: ('total') => html`${ total }`
  }),

  totalPurchasedItemsComponents: new Component({
    element: '#purchaseDetails',
    state: cartState,
    render: ('item') => html`
     <li>${ item }</li> `
  }),

  totalPurchaseCostComponent: new Component({
    element: '#confirmtotalCost',
    render: ('cost') => html`
      <li>${ cost }</li>`
  }),

  cartEvents: new Component({
    element: '#cart',
    actions: {
      event: 'tap',
      element: '#cancelOrder',
      callback: function() {
        cart.cartState.purge()
        cart.totalItemsComponent.empty()
        cart.totalCostComponent.empty()
        cart.cartComponent.empty()
        $.GoBackTo('main')
      })
      }
    }
  })
}

The you would access these like so:

cart.purchaseItems.render()
cart.totalPurchasedItemsComponents.render()
// Etc.
        

Build Scripts

When you create a new project with the chui command line tool you get a build script. This allows you to organize you project's code into separate files in the dev folder and assemble them in project's dev/app.js file by importing them. The build script will automatically bundle them together as one file.

To see examples of projects built around ES6 modules, examine the ChocolateChip-UI's reference apps. You can learn more about them.