Skip to content

Instantly share code, notes, and snippets.

@pelonpelon
Last active September 17, 2015 02:13
Show Gist options
  • Save pelonpelon/c15b9fe8251d0f5fa31b to your computer and use it in GitHub Desktop.
Save pelonpelon/c15b9fe8251d0f5fa31b to your computer and use it in GitHub Desktop.
Mithril Snippets 2
'use strict'
function createView(ctrl, opts, children) {
return m('h1', 'Cached')
}
function controller() {
return {
view: m.prop()
}
}
function view(ctrl, opts, children) {
var view = ctrl.view()
if (!view || opts.refresh) {
view = createView(ctrl, opts, children)
ctrl.view(view)
return view
}
return {subtree: 'retain'}
}
module.exports = {
controller: controller,
view: view
}
//controller: function(){
// this.flagMap = {}
//},
//view: function(){
// m("input[type=checkbox]", {onclick: m.withAttr("checked", function(checked) {ctrl.flagMap[itemInTheLoop.id] = checked})})
//}
// you may also need to set a config in m("input") to delete the flagMap key,
//if items in the list can be deleted,
//e.g. config: function(el, init, ctx) {ctx.onunload = function() {delete ctrl.flagMap[itemInTheLoop.id]}}
Widget = {}
Widget.controller = function () {
var ctrl = this
ctrl.todos = [
{ name: 'One', checked: false },
{ name: 'Two', checked: true },
{ name: 'Three', checked: false }
]
ctrl.toggle = function (index, isChecked) {
ctrl.todos[index].checked = isChecked
}
}
Widget.view = function (ctrl) {
return m('.todos', [
m('p', "Number checked: " + ctrl.todos.filter(isChecked).length),
ctrl.todos.map(function (todo, idx) {
return m('label', [
todo.name,
m('input[type=checkbox]', {
checked: todo.checked,
onchange: ctrl.withAttr('checked', function(isChecked) { ctrl.toggle(idx, isChecked) })
})
])
})
])
}
function isChecked (todo) {
return todo.checked
}
function classList( classes ) {
if ( Array.isArray( classes ) ) {
return classes.filter( function ( item ) {
return !!item;
} ).join( ' ' );
}
// Assume Object
var keys = Object.keys( classes );
return keys.filter( function ( key ) {
return !!classes[ key ];
} ).join( ' ' );
}
classList( [
1 === 1 ? '1is1' : '',
0.1 + 0.2 === 0.3 ? '0.3is0.3' : ''
] );
//-> "1is1"
classList( {
'1is1': 1 === 1,
'0.3is0.3': 0.1 + 0.2 === 0.3
} );
//-> "1is1"
var tableComponent = {
controller( rows ){
this.rows = rows.map( cells =>
cells.map( content => ( {
content,
active : m.prop( false )
} ) )
)
this.clear = () => this.rows.forEach( cells =>
cells.forEach( cell =>
cell.active( false )
)
)
},
view : ( { rows, clear } ) =>
m( 'table',
rows.map( cells =>
m( 'tr',
cells.map( cell =>
m.component( cellComponent, cell, clear )
)
)
)
)
}
var cellComponent = {
view : ( ctrl, cell, clear ) =>
m( 'td', {
className : cell.active()
? 'active'
: '',
onclick : () => {
clear()
cell.active( true )
}
},
cell.content
)
}
var tableData = Object.keys( window )
.reduce( ( table, key, index ) => {
if( ( index % 5 ) === 0 )
table.push( [] )
table[ table.length - 1 ].push( key )
return table
}, [] )
m.mount(
document.body,
m.component(
tableComponent,
tableData
)
)
// pass events as arg to component
events: {
onclick: tapDelegate.handleClick
}
---
// component
for (n in args.events) {
fn = args.events[n];
props[n] = function(e) {
fn(e, component, ctrl);
};
}
// es6
var component = {
controller : function( ...args ){
this.redraw = force =>
this.root
&& m.module(
this.root,
component.view(
force
? new component( ...args )
: this,
...args
).children
},
view : ctrl => m( '', {
config : el => ctrl.root = el
}, whatever )
}
var datePickerComponent = {
view : ctrl => m( '.datePickerRoot', {
config : ( el, init ) => {
if( !init ){
var datePicker = $( el ).datePicker()
ctrl.onunload = datePicker.destroy()
}
} )
}
m.module( document.body, {
view : function( ctrl ){
return m( 'form', {
style : {
font : '1em/1.5 sans-serif'
}
},
m( 'label',
m( 'input[type=checkbox]', {
checked : ctrl.disabled,
onclick : function(){
ctrl.disabled = !ctrl.disabled
}
} ),
' I consent to blah blah blah'
),
m( 'br' ),
m( 'button', {
onclick : function( e ){
e.preventDefault()
alert( 'kthx' )
},
disabled : !ctrl.disabled
}, 'Confirm' )
)
}
} )
class ExpandList
constructor: (@items) ->
@expanded = {}
toggle: (item) =>
if @expanded[item]
delete @expanded[item]
else
@expanded[item] = true
class App
controller: =>
# Get the model from somewhere:
list = ["A", "B", "C", "D"]
@expList = new ExpandList list
@ # Return this to set the App object to controller
// List needs key attr
view: =>
m 'div', @expList.items.map (i) =>
expanded = @expList.expanded[i]
m '.item', {
onclick: => @expList.toggle i,
style: if expanded then {height: "100px"} else {}
},
# Display additional properties of the list
# objects here if expanded:
(if expanded then "- " else "+ ") + i
m.module document.body, new App
```css
.item {
font-size: 150%;
border: 1px solid gray;
padding: 4px;
cursor: pointer;
}
```
//model
var app = {}
app.state = {pageY: 0, pageHeight: window.innerHeight}
var items = []
for(var i = 0; i < 5000; i++) {
items.push({
title: 'Foo Bar ' + i
})
}
//yes, window.innerHeight is a data source, so it goes in the model
window.addEventListener("scroll", function(e) {
app.state.pageY = Math.max(e.pageY || window.pageYOffset, 0);
app.state.pageHeight = window.innerHeight;
m.redraw() //notify view
})
//controller
app.controller = function() {
this.items = items
this.draws = 0
this.dps = 'Wait for it...'
this.last = 0
setInterval( function(){
this.dps = this.draws - this.last
this.last = this.draws
}.bind( this ), 1000 )
}
//view
app.view = function(ctrl) {
var pageY = app.state.pageY
var begin = pageY / 31 | 0
// Add 2 so that the top and bottom of the page are filled with
// next/prev item, not just whitespace if item not in full view
var end = begin + (app.state.pageHeight / 31 | 0 + 2)
var offset = pageY % 31
ctrl.draws++
return [
m(".list", {style: {height: ctrl.items.length * 31 + "px", position: "relative", top: -offset + "px"}}, [
m("ul", {style: {top: app.state.pageY + "px"}}, [
ctrl.items.slice(begin, end).map(function(item) {
return m("li", item.title)
})
])
]),
m('.redrawCount', {
style : {
position: 'fixed',
bottom : 0,
width : '100%',
background: '#fff'
}
}, [
m( 'p', 'Number of draws: ' + ctrl.draws ),
m( 'p', 'Frames per second: ' + ctrl.dps )
]
)
]
}
m.module(document.body, app)
m.initComponent = function (component, options, content) {
var controller = new component.controller(options)
controller.render = function (options2, content2) {
return component.view(controller, options2 || options, content2 || content)
}
return controller
}
Widget = {
controller: function () {
this.css = Widget.stylesheet().classes
},
view: function (ctrl) {
// ctrl.css.head is not a true selector; it's a CSS class that JSS will uniquely generate during runtime
return m('.widget', [
m('h3', { class: ctrl.css.head }),
m('div', { class: ctrl.css.body })
])
},
styles: {
head: {
'font-size': '3rem'
},
body: {
'padding': '2rem',
'margin': '0 0 0.5rem 0'
}
},
// This could be a mixin
stylesheet: function () {
this._stylesheet || (this._stylesheet = jss.createStyleSheet(this.styles).attach())
return this._stylesheet
}
}
// could be a missing forced redraw here
function load(){
if( arguments.length ){
loading = true
m.redraw( true )
var request = m.request.apply( undefined, arguments )
request.then( function(){
loading = false
} )
return request
}
else {
return loading
}
}
// Reusable panel. Provides header styling, etc.
var Panel = m.component({
view: function(ctrl, args, children) {
return m('.mypanel', [
m('.mypanel-header', [args.header || ''], children)
]);
}
});
// One type of panel
var Menu = m.component({
view: function(ctrl, args) {
return Panel({
header: m('h3', 'Menu')
}, [
m('li', 'Menu item 1'),
m('li', 'Menu item 2')
]);
}
});
// Another panel
var SomeWidget = m.component({
view: function(ctrl, args) {
return Panel({
header: m('h3', 'Panel header')
}, 'Panel content');
}
});
// A page with multiple panels
var Page = m.component({
controller: function() {},
view: function(ctrl, args) {
return m('div', {}, [
m('h1', args.header),
Menu(),
SomeWidget()
]);
}
});
m.mount(document.body, Page({
header: "Page"
}));
// using a closure
m.route( document.body, '/', {
'/:path...' : {
controller : ( function closure( ctrl, initialised ){
return function controller(){
ctrl.last = new Date().toString()
if( !initialised ){
initialised = true
ctrl.first = new Date().toString()
}
return ctrl
}
}( {} ) ),
view : function( ctrl ){
return [
m( 'h1', 'Hiya' ),
m( 'p', 'Welcome to route: ', m.route() ),
m( 'p', 'The controller was initialised at ', ctrl.first ),
m( 'p', '...And updated at ', ctrl.last ),
m( 'h2', 'Navigate about to trigger component re-init:' ),
m( 'ol',
m( 'li',
m( 'a', {
config : m.route,
href : '/here'
}, 'Here' )
),
m( 'li',
m( 'a', {
config : m.route,
href : '/there'
}, 'There' )
)
)
]
}
}
} )
function connect( connection ){
if( !connection ) connection = m.deferred()
m.request( {
method : 'GET',
url : '/place'
} ).then(
connection.resolve
function( error ){
// No idea how your server responds - change to suit
if( error === '404' ){
connect( connection )
}
else {
connection.reject( error )
}
}
)
return connection.promise
}
var Router = function(module, name) {
return {
controller: function() {
// Do something generic like calling Google Analytics from here
console.log("Router", name)
return new module.controller()
},
view: module.view
}
}
m.route(document.getElementById("page"), "/", {
"/": Router(app, "app"),
"/project/:id": Router(project, "project")
});
//I can't provide a set of routes to the user, so I need to give them a loading page while I wait for their session to be authenticated
// (and then give them access to all the routes their profile dictates) or denied (in which case it's /login /signup only).
// What I'm doing is re-initialising the m.route whenever permissions change. Again, if all the controllers specify m.redraw.strategy(
// 'diff' ), you won't do any unnecessary DOM thrashing. So if there's a session token in localStorage, I'll query the server with that// token to see if it's still valid. But in the meantime I show the user a loading page:
// Run on init
m.route( document.body, '/', {
// Using a ... at the end of a path param means it can be any length of segments. So ':path...' will capture anything.
'/:path...' : {
controller(){
// Preserve entry point
localStorage.setItem( 'entryPoint', m.route() );
},
// Wait til we've determined what's going on
view : function(){
return m( 'div', 'Loading...' );
}
}
} );
Then if the session comes back refreshed, I run
// Stop redraws: we're about to make multiple redraw triggers
m.startComputation();
// Set up the real route
m.route( document.body, '/', authenticatedRouteMap );
// Re-route to wherever the user was headed in the first place
// If it's a bad route, it'll revert to the default set above
m.route( localStorage.getItem( 'entryPoint' ) );
// We're done
m.endComputation();
// If the session comes back expired, they get what they would have in the first place: signup, login, etc.
m.spy = function (m, plugins) {
plugins = type.call(plugins) === ARRAY ? plugins : [plugins];
function res() {
var vdom = m.apply(null, arguments)
for (var i = 0, N = plugins.length; i < N; i++) {
plugin[i](vdom);
// or maybe this?
// vdom = plugin[i](vdom) || vdom;
}
return vdom;
}
for (var k in m) if (m.hasOwnProperty(k)) res[k] = m[k];
return res;
}
// Here's a small demo illustrating a model entity to fetch and save data from a web service,
// a controller that exposes the entity and a save method to the view,
// and a form template that can be updated by the user and submitted.
//model
var Thing = function(data) {
this.name = m.prop(data.name)
}
Thing.get = function(id) {
return m.request({method: "GET", url: "/api/things/:id", data: {id: id}, type: Thing})
}
Thing.update = function(data) {
return m.request({method: "POST", url: "/api/things", data: data, type: Thing})
}
//module
var demoModule = {}
//controller
demoModule.controller = function() {
var thing = Thing.get(1)
return {
thing: thing
save: function() {
Thing.update(thing)
}
}
}
//view
demoModule.view = function(ctrl) {
return m("form", [
m("input[placeholder=Name]", {
oninput: m.withAttr("value", ctrl.thing.name),
value: ctrl.thing.name()
},
m("button[type=button]", {onclick: ctrl.save})
])
}
//run
m.module(document.body, demoModule)
var test = false
m.module(document.body, {
view: function (ctrl) {
return m('.foo', {
class: test ? "bar" : "",
onclick: function() {test = !test},
config: function(el, init) {
if (!init) el.addEventListener("transitionend", function() {alert(1)})
}
}, "test");
}
});
// css
// .foo {background:red;transition:all 1s ease;}
// .bar {background:green;}
watchedProp = function (initialValue, onChange) {
var value = initialValue || null;
return function getterSetter( input ) {
if( arguments.length && input !== value){
value = input;
onChange(value);
}
return value;
}
};
class Wizard
constructor: ->
@firstName = m.prop()
@lastName = m.prop()
@step = m.prop 0
firstStep: =>
@step 1
m.route '/wizard/step_1'
nextStep: =>
@step 2
m.route '/wizard/step_2'
lastStep: =>
@step 3
alert "Your name is " + @firstName() + " " + @lastName()
m.route '/wizard/step_3'
stepView: =>
switch @step()
when 0 then m "a[href=#]", onclick: @firstStep, "Start wizard"
when 1
[
m "span", "First name: "
m "input", oninput: m.withAttr "value", @firstName
m "a[href=#]", onclick: @nextStep, "Next"
]
when 2
[
m "span", "Last name: "
m "input", oninput: m.withAttr "value", @lastName
m "a[href=#]", onclick: @lastStep, "Next"
]
when 3 then "Finished"
view: =>
[
m "header", "Step " + @step()
m "main", @stepView()
m "footer", "Footer"
]
wizard = new Wizard
# Create a simple module that uses the wizard
# to avoid re-initialization of the object and data
wizModule =
controller: ->
view: wizard.view
m.route.mode = 'pathname'
m.route document.body, '/',
'/': wizModule
'/wizard/step_1': wizModule
'/wizard/step_2': wizModule
'/wizard/step_3': wizModule
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment