Skip to content

Tutorials

Bryce edited this page Jun 9, 2016 · 29 revisions

Mithril & CoffeeScript Tutorial

Introduction

This page is a tutorial for starting to develop with CoffeeScript and Mithril for our application. For those of us new to CoffeeScript and Mithril, the challenges inherent with learning a new language, and a new framework can lead to more than a few headaches. The target audience is for this is a new Coop, with experience programming, but not necessarily any experience with Mithril CofeeScript, or webapp development.

The reason we are writing a wiki article is because you are going to have a great time coding if you follow this mindset:

It is far easier to work with legacy code when you use the same coding conventions. I will do this, and thus I will be able to understand how they worked more easily.

I won't promise that we do things in the most efficient way possible, but we will try to at least be consistent.


Lesson 1: Getting Started

You should have an understanding of the following topics in order to get a base of knowledge to work with for this lesson:

*The links provided may or may not conform to the styles we are using in SuperPhy.

Terminology:

@ is the CoffeeScript version of JavaScript's this keyword. So when CoffeeScript is compiled into JavaScript, @ becomes this.

When we refer to a static component. It is a component that doesn't have a controller to deal with interactions. All the the data displayed in the view is either passed in as arguments from a parent component, or is static data in the class. See this lesson's exercise for an example of that.

Empty Project. If you want to start testing things out, an empty project with the correct JSFiddle settings is here.

Exercise: Hello World!

This exercise will show you how to make the simplest implementation of a Mithril view. This simply populates the document.body DOM element with a div containing a string "Hello World", when the user is at the root of the URL.

m.route(document.body, "/"
  "/": view: -> m('.', "Hello World!")
)

Lesson 2: Separation of Responsibilities.

You should have an understanding of the following topics in order to get a base of knowledge to work with for this lesson:

*The links provided may or may not conform to the styles we are using in SuperPhy.


We won't get far using just the m.route method. In this lesson we are going to make a component class, and a model class. This will designate distinct responsibilities for different components.

Let's make a name model:

class Name
  constructor: (name) ->
    this.name = name or ""
    return
  greet: () ->
    return  "Hello #{@name}!"

Since you read the links, you know that this class has two instance methods constructor and greet. You know you can have multiple instances of different Names, and you know how to make a new Name(). This is what a model is in our application. You probably have some familiarity with this format from school. This particular example has no Mithril in it, but models can and will have Mithril.

Next, let's make a Component that uses our Name model.

class Hello
  @controller: (args) ->
    @name = new Name(args.name or "")
    return this
  @view: (ctrl, args) ->
    m('.'
      m('.', ctrl.name.greet())
     )

m.route(document.body, "/"
    "/": m.component(Hello, name:"Coop Student")
)

Again, you read the links and know that the @methods are class methods. The whole result is similar to having a dictionary object with functions as it's values, and keys as it's method name, but it allows us to use inheritance as well. When you see this other style of component in the mithril.js tutorial pages, you will now know the equivalent for us in CoffeeScript.

All views return one mithril virtual element. It will not work to return strings or even lists of mithril virtual elements.

Not all controllers return themselves, but most do in our application. We are able to build a dictionary object out the the controller itself. It is also perfectly valid to return a model if that is the only model being used, but for clarity purposes, this is how we makes controllers.

When you tell mithril to try to make a virtual element out of your class, it looks for the two class methods @view and @controller. It goes through the following process:

  • It tries to run @controller if it exists, passing in any arguments that you give it (args).
  • It passes the return of @controller to the @view as (ctrl), as well as the arguments it tried to give to controller. (args).
  • It returns the virtual element object from the view.

When you see a an@view, or an @controller, you know that it is a component class.

Exercise: Greetings

This exercise will show you how to make 1 component use another.

class Name
  constructor: (name) ->
    this.name = name or ""
    return
  greet: () ->
    return  "Hello #{@name}!"

class Hello
  @controller: (args) ->
    @name = new Name(args.name or "")
    return this
  @view: (ctrl, args) ->
    m('.'
      m('.', ctrl.name.greet())
     )

class Hello_Coops
  @controller: (args) ->
    return this
  @view: (ctrl, args) ->
    m('.'
      m.component(Hello, {name: "Coop Student"})
      "I hope you are having a good time with this tutorial!"
     )

m.route(document.body, "/"
    "/": Hello_Coops
)

Exercise: Routing Object

How do I define a route? Our project uses a global object Routes. It has a method add, that takes a url to direct to, and a component. In this exercise, we are going to re-create it.

#Top of file
Routes = {}
Routes.add = (route, class_, args) ->
    args = args || {}
    Routes.routes ?= {}
    if route.substring(0,1) is '/'
        route = route.substring(1)
    Routes.routes["/#{route}"] = view: -> m.component(class_, args)
    
#Components
#...

#Bottom of File
m.route(document.body, "/", Routes.routes)

Now we can define routes inside of the page class. This means we won't have to hunt for a large JSON file that contains our routes. A drawback is we won't have one place to look to see all the the different endpoints we have in the application. Let's fix that with our first endpoint:

class Api
    Routes.add('/', this)
    @view: (ctrl, args) ->
        m('.'
            m('.', key) for key in Object.keys(Routes.routes).sort()
        )

*You must have the default route populated, so we are putting this at /, but currently this Api class is in our project at /api.

If you run this code, the result would just be the /, so let's add another route for clarity purposes.

class Hello
  Routes.add('/hello', this)
  @controller: (args) ->
    @name = new Name(args.name or "")
    return this
  @view: (ctrl, args) ->
    m('.'
      m('.', ctrl.name.greet())
     )

Now we can run this all together to get our desired result. Keep in mind, our real application is a SPA (Single Page Application), but our CoffeeScript is in different files and folders. When we compile the CoffeeScript, we create one large JavaScript file to give to Apache.

How do I go from page to page?

Lesson 4: Inheritance

Routes = {}
Routes.add = (route, class_, args) ->
    args = args || {}
    Routes.routes ?= {}
    if route.substring(0,1) is '/'
        route = route.substring(1)
    Routes.routes["/#{route}"] = view: -> m.component(class_, args)

class SuperComponent
    @view: (args) ->
        args = args or {}
        m('.', "This is From the SuperComponent", args)

class SubComponent extends SuperComponent
    Routes.add('/', this)
    @controller: () ->
        @data = m.prop('')
        return this
    @view:(ctrl, args) ->
        return super(
            m('.', "This is From the SubComponent", args)
        )

m.route(document.body, "/", Routes.routes)

Lesson 5: Promise Objects.

Mithril - Auto-redrawing

There are two ways to bind data asynchronously into the model:

  • User input
  • Ajax

Both of these methods are handled by the mithril promise object: m.prop()

In the following example, we have a fake Ajax request, and a field that you can fill out. Both of these are going to be behind an object that sets and gets the data.

Routes = {}
Routes.add = (route, class_, args) ->
    args = args || {}
    Routes.routes ?= {}
    if route.substring(0,1) is '/'
        route = route.substring(1)
    Routes.routes["/#{route}"] = view: -> m.component(class_, args)

fake_request = () ->
    return m.prop({
        headers: ['a','b','c']
        rows: [
            {'a':"Foo", 'b':"Bar", 'c':"Bing"}
            {'a':"Alpha", 'b':"Beta", 'c':"Gamma"}
            {'a':"Lorem", 'b':"Ipsum", 'c':"Dolor"}
        ]
    })

class Promises
    Routes.add('/', this)
    @controller: (args) ->
        @data = fake_request()
        @name = m.prop("")
        return this
    @view: (ctrl) ->
        return m('.'
            m('table', {border:"1"},
                m('tr', m('th', header) for header in ctrl.data().headers)
                m('tr', m('td', row[header]) for header in ctrl.data().headers
                ) for row in ctrl.data().rows
            )
            m('input[type=text]'
                oninput:m.withAttr('value', ctrl.name)
                value:ctrl.name()
                placeholder:"Name"
            )
            m('.', "Name = #{ctrl.name()}")
        )

m.route(document.body, "/", Routes.routes)

Lesson 6: Requests

How do I get data from a database? (model How do I change/update some data?

Clone this wiki locally