-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorials
created by Bryce Drew for PHAC
This page is a tutorial for starting to develop with CoffeeScript and Mithril for our application. For those of you 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.
You have probably heard of Collective Code Ownership. That is a tough thing to implement when all the developers are leaving before their replacements come for the next semester. The purpose of this tutorial is to facilitate your transition to the team, and allow you to 'own the code'.
All of the code is now your responsibility. You can change, modify, or break the code as you see fit. But you should follow a style, or mold the code to a style that works. When you finish your term, the additions you make should be synonymous with the existing code, so the next Co-op doesn't need to comprehend two distinct formats to do the same thing.
I won't promise that we do things in the most efficient way possible, but we will try to at least be consistent.
Prerequisates: This tutorial has been set up so that you can run it in your browser with JSFiddle. This is not the most efficient way to program CoffeeScript, and working on the application won't work running it there. When you do start developing, I suggest using Sublime Text 3, with SublimeLinter-coffeelint, but it is up to your personal preference how you develop your code.
You should have an understanding of the following topics in order to get a base of knowledge to work with for this lesson:
- CoffeeScript Overview
- The JavaScript 'this' keyword
- Mithril 101 - Getting Started
- Mithril 102 - How routing works
*The links provided may or may not conform to the styles we are using in our application.
@
is the CoffeeScript version of JavaScript's this
keyword. So when CoffeeScript is compiled into JavaScript, @
becomes this
.
Empty Project. If you want to start testing things out, an empty project with the correct JSFiddle settings is here.
js2.coffee This is a handy tool to understand quickly what the JavaScript looks like from the code that you are compiling. It also lints syntax errors, because JSFiddle won't.
Exercise 1a: Hello World!
This exercise will show you how to make a simple implementation of a Mithril view. This simply populates the document.body DOM element via a virtual element: a div with contents "Hello World". when the user is at the root of the URL.
m.route(document.body, "/"
"/": view: -> m('.', "Hello World!")
)
Give it a try by adding this to your JSFiddle.
*If it doesn't work, you need to check the settings to make sure it recognizes that you are using CoffeeScript, and that it knows to source the Mithril.js framework.
Exercise 1b: FooBar
Let's not have just one endpoint. Our application is a SPA (Single Page Application). This means that we can route to different pages, but the source-code is only loaded once. JSFIddle only let's us route to different endpoints via code, so for this exercise, lets populate one view with a hyperlink to the other, and vice-versa.
m.route(document.body, "/foo"
"/foo": view: ->
m('a', {href:"/bar", config:m.route}
"Bar"
)
"/bar": view: ->
m('a', {href:"/foo", config:m.route}
"Foo"
)
)
We have a working example of Mithril, and are starting to understand how we can use mithril to build applications sections of code.
You should have an understanding of the following topics in order to get a base of knowledge to work with for this lesson:
- CoffeeScript Class and Instance Methods
- Mithril 103 - Components (Specifically Classic MVC)
*The links provided may or may not conform to the styles we are using in our application.
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 Name
s, 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 objects and method calls.
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")
)
Since you have read the links, you 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.
*Using a class
for a component compiles different than the javascript examples at mithril.js, but I feel this way allows a more legible comprehension than using dictionaries as components.
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 2a: Greetings
This exercise will show you how to nest components.
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
)
As you can see, we can have mithril virtual elements nested as content in other virtual elements.
Exercise 2b: Refactor FooBar
We are going to spruce up some of our old code. Remember those hyperlinks we made earlier? They were essentially using the same code, with static values where variables should have been. So let's move it all out to it's own component:
class Link
@controller: (args) ->
@link = args.link
@text = args.text
return this
@view: (ctrl, args) ->
m('a', {href: ctrl.link, config:m.route}
ctrl.text
)
m.route(document.body, "/foo"
"/foo": m.component(Link, link: "/bar", text: "Bar")
"/bar": m.component(Link, link: "/foo", text: "Foo")
)
Hey! Now we don't need to worry about implementing a hyper-link in the future. If you imagine this to be a little more complicated, we can
Exercise 2c: 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, "/api", 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('/api', this)
@view: (ctrl, args) ->
m('.'
m('.', key) for key in Object.keys(Routes.routes).sort()
)
If you run this code, the result would just be the /api
, 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? JSFiddle won't let you type it into the address bar, as they have their own routing, but if you put an hyperlink as your virtual element like in FooBar example, you navigate to a non-default view from that. For using our application, if the view is valid, you can just type it into the url on your browser.
Exercise 2d: Inheritance
class 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, m('hr'))
@view:(ctrl, args) ->
return super(
m('.', "This is From the SubComponent", args)
)
m.route(document.body, "/", Routes.routes)
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 our application.
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()
Why do we want to do that? We just told you! We want to be able to take user input, perform any nessesary operations (including Ajax requests), and show the User the updated views. In our previous lessons, we are making views that only have one static view. We need to instead, create an interface with the User.
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.
Exercise: Intro To Promises
class Example
@controller: (args) ->
@name = m.prop("")
return this
@view: (ctrl) ->
m('.'
m('input[type=text]'
oninput:m.withAttr('value', ctrl.name)
value: ctrl.name()
placeholder:"Name"
)
m('.', "Name = #{ctrl.name()}")
)
m.route(document.body, "/",
'/': Example
)
Exercise: Mock Request
We are going to make a mock request. These are nice when we don't have functionality, but want to test a component that needs that functionality. I will let the link above explain the rest.
class MockData
constructor: () ->
@data = 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"}
]
})
get: () ->
return @data
class Example
@controller: (args) ->
@data = new MockData().get()
return this
@view: (ctrl) ->
return 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.route(document.body, "/",
'/': Example
)
Exercise: Requests - For Real
So we went through all this, and now you are thinking: How do I actually make some requests? Well,
One of the many reasons we are using mithril, is we are
How do I get data from a database? (model How do I change/update some data?