Commit 35a6ed12 authored by Alex Browne's avatar Alex Browne

Add a new framework: GopherJS + Humble

parent 069f01af
......@@ -34,7 +34,8 @@
"examples/polymer/elements/elements.build.js",
"examples/js_of_ocaml/js/*.js",
"examples/typescript-*/js/**/*.js",
"**/generated/**"
"**/generated/**",
"examples/humble/js/**"
],
"requireSpaceBeforeBlockStatements": true,
"requireParenthesesAroundIIFE": true,
......
# Humble + GopherJS • [TodoMVC](http://todomvc.com)
> Humble is a collection of loosely-coupled tools designed to build client-side
> and hybrid web applications using GopherJS, which compiles Go code to
> JavaScript.
>
> [Humble - github.com/go-humble/humble](https://github.com/go-humble/humble)
## Resources
- [Website](https://github.com/go-humble/humble)
- [Documentation](https://github.com/go-humble) (Each package is
documented separately)
### Support
- [GopherJS on StackOverflow](http://stackoverflow.com/search?q=gopherjs)
- [GopherJS Google Group](https://groups.google.com/forum/#!forum/gopherjs)
*Let us [know](https://github.com/go-humble/humble/issues) if you discover anything worth sharing.*
## Demo
A [Live Demo](http://d3cqowlbjfdjrm.cloudfront.net/) of the application is
available online.
## Implementation
[GopherJS](https://github.com/gopherjs/gopherjs) compiles Go to JavaScript code
which can run in the browser. [Humble](https://github.com/go-humble/humble) is
a collection of tools written in Go designed to be compatible with GopherJS.
The following Humble packages are used:
- [router](https://github.com/go-humble/router) for handling the `/active` and
`/completed` routes.
- [locstor](https://github.com/go-humble/locstor) for saving todos to
localStorage.
- [temple](https://github.com/go-humble/temple) for managing Go templates and
packaging them so they can run in the browser.
- [view](https://github.com/go-humble/view) for organizing views, doing basic
DOM manipulation, and delegating events.
The full TodoMVC spec is implemented, including routes.
### Getting up and Running
First, [install Go](https://golang.org/dl/). You will also need to setup your
[Go workspace](https://golang.org/doc/code.html). It is important that you have
an environment variable called `GOPATH` which points to the directory where all
your Go code resides.
To download and install this repository, run
`go get github.com/go-humble/examples`, which will place the project in
`$GOPATH/src/github.com/go-humble/examples` on your machine.
You will also need to install GopherJS with
`go get -u github.com/gopherjs/gopherjs`. The `-u` flag gets the latest version,
which is recommended.
The project uses [temple](https://github.com/go-humble/temple) to precompile
templates. Install temple with `go get -u github.com/go-humble/temple`.
Then run `go generate ./...` to compile the templates and compile the Go code
to JavaScript.
Finally, serve the project directory with `go run serve.go` and visit
[localhost:8000](http://localhost:8000) in your browser.
## Credit
Created by [Alex Browne](http://www.alexbrowne.info)
package main
import (
"log"
"github.com/go-humble/router"
"github.com/go-humble/examples/todomvc/go/models"
"github.com/go-humble/examples/todomvc/go/views"
)
//go:generate temple build templates/templates templates/templates.go --partials templates/partials
//go:generate gopherjs build main.go -o ../js/app.js -m
func main() {
// This is helps development by letting us know the app is actually running
// and telling us the time that the app was most recently started.
log.Println("Starting")
// Create a new todo list.
todos := &models.TodoList{}
if err := todos.Load(); err != nil {
panic(err)
}
// Create an app view with our todo list.
appView := views.NewApp(todos)
// Register a change listener which will be triggered whenever the todo list
// is changed.
todos.OnChange(func() {
// Asynchronously save the todos to localStorage.
go func() {
if err := todos.Save(); err != nil {
panic(err)
}
}()
// Then re-render the entire view.
if err := appView.Render(); err != nil {
panic(err)
}
})
// Create and start a new router to handle the different routes. On each
// route, we are simply going to use a filter to change which todos are
// rendered, then re-render the entire view with the filter applied.
r := router.New()
r.ForceHashURL = true
r.HandleFunc("/", func(_ *router.Context) {
appView.UseFilter(models.Predicates.All)
if err := appView.Render(); err != nil {
panic(err)
}
})
r.HandleFunc("/active", func(_ *router.Context) {
appView.UseFilter(models.Predicates.Remaining)
if err := appView.Render(); err != nil {
panic(err)
}
})
r.HandleFunc("/completed", func(_ *router.Context) {
appView.UseFilter(models.Predicates.Completed)
if err := appView.Render(); err != nil {
panic(err)
}
})
r.Start()
}
package models
// Predicate is a function which takes a todo and returns a bool. It can be
// used in filters.
type Predicate func(*Todo) bool
// Predicates is a data structure with commonly used Predicates.
var Predicates = struct {
All Predicate
Completed Predicate
Remaining Predicate
}{
All: func(_ *Todo) bool { return true },
Completed: (*Todo).Completed,
Remaining: (*Todo).Remaining,
}
// All returns all the todos. It applies a filter using the All predicate.
func (list TodoList) All() []*Todo {
return list.Filter(Predicates.All)
}
// Completed returns only those todos which are completed. It applies a filter
// using the Completed predicate.
func (list TodoList) Completed() []*Todo {
return list.Filter(Predicates.Completed)
}
// Remaining returns only those todos which are remaining (or active). It
// applies a filter using the Remaining predicate.
func (list TodoList) Remaining() []*Todo {
return list.Filter(Predicates.Remaining)
}
// Filter returns a slice todos for which the predicate is true. The returned
// slice is a subset of all todos.
func (list TodoList) Filter(f Predicate) []*Todo {
results := []*Todo{}
for _, todo := range list.todos {
if f(todo) {
results = append(results, todo)
}
}
return results
}
// Invert inverts a predicate, i.e. a function which accepts a todo as an
// argument and returns a bool. It returns the inverted predicate. Where f would
// return true, the inverted predicate would return false and vice versa.
func invert(f Predicate) Predicate {
return func(todo *Todo) bool {
return !f(todo)
}
}
// todoById returns a predicate which is true iff todo.id equals the given
// id.
func todoById(id string) Predicate {
return func(t *Todo) bool {
return t.id == id
}
}
// todoNotById returns a predicate which is true iff todo.id does not equal
// the given id.
func todoNotById(id string) Predicate {
return invert(todoById(id))
}
package models
import "encoding/json"
// Todo is the model for a single todo item.
type Todo struct {
id string
completed bool
title string
list *TodoList
}
// Toggle changes whether or not the todo is completed. If it was previously
// completed, Toggle makes it not completed, and vice versa.
func (t *Todo) Toggle() {
t.completed = !t.completed
t.list.changed()
}
// Remove removes the todo from the list.
func (t *Todo) Remove() {
t.list.DeleteById(t.id)
}
// Completed returns true iff the todo is completed. It operates as a getter for
// the completed property.
func (t *Todo) Completed() bool {
return t.completed
}
// Remaining returns true iff the todo is not completed.
func (t *Todo) Remaining() bool {
return !t.completed
}
// SetCompleted is a setter for the completed property. After the property is
// set, it broadcasts that the todo list was changed.
func (t *Todo) SetCompleted(completed bool) {
t.completed = completed
t.list.changed()
}
// Title is a getter for the title property.
func (t *Todo) Title() string {
return t.title
}
// SetTitle is a setter for the title property.
func (t *Todo) SetTitle(title string) {
t.title = title
t.list.changed()
}
// Id is a getter for the id property.
func (t *Todo) Id() string {
return t.id
}
// jsonTodo is a struct with all the same fields as a todo, except that they
// are exported instead of unexported. The purpose of jsonTodo is to make the
// todo item convertible to json via the json package.
type jsonTodo struct {
Id string
Completed bool
Title string
}
// MarshalJSON converts the todo to json.
func (t Todo) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonTodo{
Id: t.id,
Completed: t.completed,
Title: t.title,
})
}
// UnmarshalJSON converts json data to a todo object.
func (t *Todo) UnmarshalJSON(data []byte) error {
jt := jsonTodo{}
if err := json.Unmarshal(data, &jt); err != nil {
return err
}
t.id = jt.Id
t.completed = jt.Completed
t.title = jt.Title
return nil
}
package models
import (
"github.com/dchest/uniuri"
"github.com/go-humble/locstor"
)
// store is a datastore backed by localStorage.
var store = locstor.NewDataStore(locstor.JSONEncoding)
// TodoList is a model representing a list of todos.
type TodoList struct {
todos []*Todo
changeListeners []func()
}
// OnChange can be used to register change listeners. Any functions passed to
// OnChange will be called when the todo list changes.
func (list *TodoList) OnChange(f func()) {
list.changeListeners = append(list.changeListeners, f)
}
// changed is used to notify the todo list and its change listeners of a change.
// Whenever the list is changed, it must be explicitly called.
func (list *TodoList) changed() {
for _, f := range list.changeListeners {
f()
}
}
// Load loads the list of todos from the datastore.
func (list *TodoList) Load() error {
if err := store.Find("todos", &list.todos); err != nil {
if _, ok := err.(locstor.ItemNotFoundError); ok {
return list.Save()
}
return err
}
for i := range list.todos {
list.todos[i].list = list
}
return nil
}
// Save saves the list of todos to the datastore.
func (list TodoList) Save() error {
if err := store.Save("todos", list.todos); err != nil {
return err
}
return nil
}
// AddTodo appends a new todo to the list.
func (list *TodoList) AddTodo(title string) {
list.todos = append(list.todos, &Todo{
id: uniuri.New(),
title: title,
list: list,
})
list.changed()
}
// ClearCompleted removes all the todos from the list that have been completed.
func (list *TodoList) ClearCompleted() {
list.todos = list.Remaining()
list.changed()
}
// CheckAll checks all the todos in the list, causing them to be in the
// completed state.
func (list *TodoList) CheckAll() {
for _, todo := range list.todos {
todo.completed = true
}
list.changed()
}
// UncheckAll unchecks all the todos in the list, causing them to be in the
// active/remaining state.
func (list *TodoList) UncheckAll() {
for _, todo := range list.todos {
todo.completed = false
}
list.changed()
}
// DeleteById removes the todo with the given id from the list.
func (list *TodoList) DeleteById(id string) {
list.todos = list.Filter(todoNotById(id))
list.changed()
}
<span class="todo-count">
<strong>{{ len .Todos.Remaining }}</strong>
item{{ if ne (len .Todos.Remaining) 1}}s{{end}} left
</span>
<ul class="filters">
<li>
<a {{ if eq .Path "#/"}} class="selected" {{ end }} href="#/">All</a>
</li>
<li>
<a {{ if eq .Path "#/active"}} class="selected" {{ end }} href="#/active">Active</a>
</li>
<li>
<a {{ if eq .Path "#/completed"}} class="selected" {{ end }} href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed {{ if eq (len .Todos.Completed) 0}}hidden{{ end }}">
Clear completed
</button>
{{/*
NOTE: the todomvc tests require that the top-level element for the
todo view is an li element. I would prefer to have the top-level element for
each todo be a div wrapper, and to include the li element inside this
template. See views/app.go for the current workaround.
*/}}
<!-- <li {{ if .Completed }}class="completed"{{ end }}> -->
<div class="view">
<input class="toggle" type="checkbox" {{ if .Completed }}checked{{ end }}>
<label>{{ .Title }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{ .Title }}">
<!-- </li> -->
package templates
// This package has been automatically generated with temple.
// Do not edit manually!
import (
"github.com/go-humble/temple/temple"
)
var (
GetTemplate func(name string) (*temple.Template, error)
GetPartial func(name string) (*temple.Partial, error)
GetLayout func(name string) (*temple.Layout, error)
MustGetTemplate func(name string) *temple.Template
MustGetPartial func(name string) *temple.Partial
MustGetLayout func(name string) *temple.Layout
)
func init() {
var err error
g := temple.NewGroup()
if err = g.AddPartial("footer", `<span class="todo-count">
<strong>{{ len .Todos.Remaining }}</strong>
item{{ if ne (len .Todos.Remaining) 1}}s{{end}} left
</span>
<ul class="filters">
<li>
<a {{ if eq .Path "#/"}} class="selected" {{ end }} href="#/">All</a>
</li>
<li>
<a {{ if eq .Path "#/active"}} class="selected" {{ end }} href="#/active">Active</a>
</li>
<li>
<a {{ if eq .Path "#/completed"}} class="selected" {{ end }} href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed {{ if eq (len .Todos.Completed) 0}}hidden{{ end }}">
Clear completed
</button>
`); err != nil {
panic(err)
}
if err = g.AddPartial("todo", `<!-- <li {{ if .Completed }}class="completed"{{ end }}> -->
<div class="view">
<input class="toggle" type="checkbox" {{ if .Completed }}checked{{ end }}>
<label>{{ .Title }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{ .Title }}">
<!-- </li> -->
`); err != nil {
panic(err)
}
if err = g.AddTemplate("app", `<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{ if gt (len .Todos.All) 0 }}
<section class="main">
<input class="toggle-all" type="checkbox" {{ if eq (len .Todos.All) (len .Todos.Completed) }}checked{{ end }}>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
</ul>
</section>
{{ end }}
{{ if gt (len .Todos.All) 0 }}
<footer class="footer">
{{ template "partials/footer" . }}
</footer>
{{ end }}
`); err != nil {
panic(err)
}
GetTemplate = g.GetTemplate
GetPartial = g.GetPartial
GetLayout = g.GetLayout
MustGetTemplate = g.MustGetTemplate
MustGetPartial = g.MustGetPartial
MustGetLayout = g.MustGetLayout
}
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
{{ if gt (len .Todos.All) 0 }}
<section class="main">
<input class="toggle-all" type="checkbox" {{ if eq (len .Todos.All) (len .Todos.Completed) }}checked{{ end }}>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
</ul>
</section>
{{ end }}
{{ if gt (len .Todos.All) 0 }}
<footer class="footer">
{{ template "partials/footer" . }}
</footer>
{{ end }}
package views
import (
"strings"
"github.com/go-humble/examples/todomvc/go/models"
"github.com/go-humble/examples/todomvc/go/templates"
"github.com/go-humble/temple/temple"
"github.com/go-humble/view"
"honnef.co/go/js/dom"
)
const (
// Constants for certain keycodes.
enterKey = 13
escapeKey = 27
)
var (
appTmpl = templates.MustGetTemplate("app")
document = dom.GetWindow().Document()
)
// App is the main view for the application.
type App struct {
Todos *models.TodoList
tmpl *temple.Template
// predicate will be used to filter the todos when rendering. Only
// todos for which the predicate is true will be rendered.
predicate models.Predicate
view.DefaultView
events []*view.EventListener
}
// UseFilter causes the app to use the given predicate to filter the todos when
// rendering. Only todos for which the predicate returns true will be rendered.
func (v *App) UseFilter(predicate models.Predicate) {
v.predicate = predicate
}
// NewApp creates and returns a new App view, using the given todo list.
func NewApp(todos *models.TodoList) *App {
v := &App{
Todos: todos,
tmpl: appTmpl,
}
v.SetElement(document.QuerySelector(".todoapp"))
return v
}
// tmplData returns the data that is passed through to the template for the
// view.
func (v *App) tmplData() map[string]interface{} {
return map[string]interface{}{
"Todos": v.Todos,
"Path": dom.GetWindow().Location().Hash,
}
}
// Render renders the App view and satisfies the view.View interface.
func (v *App) Render() error {
for _, event := range v.events {
event.Remove()
}
v.events = []*view.EventListener{}
if err := v.tmpl.ExecuteEl(v.Element(), v.tmplData()); err != nil {
return err
}
listEl := v.Element().QuerySelector(".todo-list")
for _, todo := range v.Todos.Filter(v.predicate) {
todoView := NewTodo(todo)
// NOTE: the todomvc tests require that the top-level element for the
// todo view is an li element. Unfortunately there is no way to express
// this while also having template logic determine whether or not the
// li element should have the class "completed". I would prefer to have
// the top-level element for each todo be a div wrapper, and to include
// the li element inside the template. The workaround for now is to create
// the li element and set it's class manually.
todoView.SetElement(document.CreateElement("li"))
if todo.Completed() {
addClass(todoView.Element(), "completed")
}
view.AppendToEl(listEl, todoView)
if err := todoView.Render(); err != nil {
return err
}
}
v.delegateEvents()
return nil
}
// delegateEvents adds all the needed event listeners to the view.
func (v *App) delegateEvents() {
v.events = append(v.events,
view.AddEventListener(v, "keypress", ".new-todo",
triggerOnKeyCode(enterKey, v.CreateTodo)))
v.events = append(v.events,
view.AddEventListener(v, "click", ".clear-completed", v.ClearCompleted))
v.events = append(v.events,
view.AddEventListener(v, "click", ".toggle-all", v.ToggleAll))
}
// CreateTodo is an event listener which creates a new todo and adds it to the
// todo list.
func (v *App) CreateTodo(ev dom.Event) {
input, ok := ev.Target().(*dom.HTMLInputElement)
if !ok {
panic("Could not convert event target to dom.HTMLInputElement")
}
v.Todos.AddTodo(strings.TrimSpace(input.Value))
document.QuerySelector(".new-todo").(dom.HTMLElement).Focus()
}
// ClearCompleted is an event listener which removes all the completed todos
// from the list.
func (v *App) ClearCompleted(ev dom.Event) {
v.Todos.ClearCompleted()
}
// ToggleAll toggles all the todos in the list.
func (v *App) ToggleAll(ev dom.Event) {
input := ev.Target().(*dom.HTMLInputElement)
if !input.Checked {
v.Todos.UncheckAll()
} else {
v.Todos.CheckAll()
}
}
// triggerOnKeyCode triggers the given event listener iff the keCode for the
// event matches the given keyCode. It can be used to gain finer control over
// which keys trigger a certain event.
func triggerOnKeyCode(keyCode int, listener func(dom.Event)) func(dom.Event) {
return func(ev dom.Event) {
keyEvent, ok := ev.(*dom.KeyboardEvent)
if ok && keyEvent.KeyCode == keyCode {
listener(ev)
}
}
}
// addClass adds class to the given element. It retains any other classes that
// the element may have.
func addClass(el dom.Element, class string) {
newClasses := class
if oldClasses := el.GetAttribute("class"); oldClasses != "" {
newClasses = oldClasses + " " + class
}
el.SetAttribute("class", newClasses)
}
// removeClass removes the given class from the element it retains any other
// classes that the element may have.
func removeClass(el dom.Element, class string) {
oldClasses := el.GetAttribute("class")
if oldClasses == class {
// The only class present was the one we want to remove. Remove the class
// attribute entirely.
el.RemoveAttribute("class")
}
classList := strings.Split(oldClasses, " ")
for i, currentClass := range classList {
if currentClass == class {
newClassList := append(classList[:i], classList[i+1:]...)
el.SetAttribute("class", strings.Join(newClassList, " "))
}
}
}
package views
import (
"strings"
"github.com/go-humble/examples/todomvc/go/models"
"github.com/go-humble/examples/todomvc/go/templates"
"github.com/go-humble/temple/temple"
"github.com/go-humble/view"
"honnef.co/go/js/dom"
)
var (
todoTmpl = templates.MustGetPartial("todo")
)
// Todo is a view for a single todo item.
type Todo struct {
Model *models.Todo
tmpl *temple.Partial
view.DefaultView
events []*view.EventListener
}
// NewTodo creates and returns a new Todo view, using the given todo as the
// model.
func NewTodo(todo *models.Todo) *Todo {
return &Todo{
Model: todo,
tmpl: todoTmpl,
}
}
// Render renders the Todo view and satisfies the view.View interface.
func (v *Todo) Render() error {
for _, event := range v.events {
event.Remove()
}
v.events = []*view.EventListener{}
if err := v.tmpl.ExecuteEl(v.Element(), v.Model); err != nil {
return err
}
v.delegateEvents()
return nil
}
// delegateEvents adds all the needed event listeners to the Todo view.
func (v *Todo) delegateEvents() {
v.events = append(v.events,
view.AddEventListener(v, "click", ".toggle", v.Toggle))
v.events = append(v.events,
view.AddEventListener(v, "click", ".destroy", v.Remove))
v.events = append(v.events,
view.AddEventListener(v, "dblclick", "label", v.Edit))
v.events = append(v.events,
view.AddEventListener(v, "blur", ".edit", v.CommitEdit))
v.events = append(v.events,
view.AddEventListener(v, "keypress", ".edit",
triggerOnKeyCode(enterKey, v.CommitEdit)))
v.events = append(v.events,
view.AddEventListener(v, "keydown", ".edit",
triggerOnKeyCode(escapeKey, v.CancelEdit)))
}
// Toggle toggles the completeness of the todo.
func (v *Todo) Toggle(ev dom.Event) {
v.Model.Toggle()
}
// Remove removes the todo form the list.
func (v *Todo) Remove(ev dom.Event) {
v.Model.Remove()
}
// Edit puts the Todo view into an editing state, changing it's appearance and
// allowing it to be edited.
func (v *Todo) Edit(ev dom.Event) {
addClass(v.Element(), "editing")
input := v.Element().QuerySelector(".edit").(*dom.HTMLInputElement)
input.Focus()
// Move the cursor to the end of the input.
input.SelectionStart = input.SelectionEnd + len(input.Value)
}
// CommitEdit sets the title of the todo to the new title. After the edit has
// been committed, the todo is no longer in the editing state.
func (v *Todo) CommitEdit(ev dom.Event) {
input := v.Element().QuerySelector(".edit").(*dom.HTMLInputElement)
newTitle := strings.TrimSpace(input.Value)
// If the newTitle is an empty string, delete the todo. Otherwise set the
// new title.
if newTitle != "" {
v.Model.SetTitle(newTitle)
} else {
v.Model.Remove()
}
}
// CancelEdit resets the title of the todo to its old value. It does not commit
// the edit. After the edit has been canceled, the todo is no longer in the
// editing state.
func (v *Todo) CancelEdit(ev dom.Event) {
removeClass(v.Element(), "editing")
input := v.Element().QuerySelector(".edit").(*dom.HTMLInputElement)
input.Value = v.Model.Title()
input.Blur()
}
<!doctype html>
<html lang="en" data-framework="humble">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Humble + GopherJS • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- The following line prevents favicon.ico requests -->
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
</head>
<body>
<section class="todoapp">
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/albrow">Alex Browne</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="js/app.js"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
{
"name": "todomvc-app-css",
"version": "2.0.1",
"description": "CSS for TodoMVC apps",
"license": "CC-BY-4.0",
"repository": {
"type": "git",
"url": "https://github.com/tastejs/todomvc-app-css"
},
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
"files": [
"index.css"
],
"keywords": [
"todomvc",
"tastejs",
"app",
"todo",
"template",
"css",
"style",
"stylesheet"
],
"gitHead": "f1bb1aa9b19888f339055418374a9b3a2d4c6fc5",
"bugs": {
"url": "https://github.com/tastejs/todomvc-app-css/issues"
},
"homepage": "https://github.com/tastejs/todomvc-app-css",
"_id": "todomvc-app-css@2.0.1",
"scripts": {},
"_shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052",
"_from": "todomvc-app-css@>=2.0.0 <3.0.0",
"_npmVersion": "2.5.1",
"_nodeVersion": "0.12.0",
"_npmUser": {
"name": "sindresorhus",
"email": "sindresorhus@gmail.com"
},
"maintainers": [
{
"name": "sindresorhus",
"email": "sindresorhus@gmail.com"
},
{
"name": "addyosmani",
"email": "addyosmani@gmail.com"
},
{
"name": "passy",
"email": "phartig@rdrei.net"
},
{
"name": "stephenplusplus",
"email": "sawchuk@gmail.com"
}
],
"dist": {
"shasum": "f64d50b744a8a83c1151a08055b88f3aa5ccb052",
"tarball": "http://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz"
},
"directories": {},
"_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.1.tgz"
}
# todomvc-app-css
> CSS for TodoMVC apps
![](screenshot.png)
## Install
```
$ npm install --save todomvc-app-css
```
## Getting started
```html
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
```
See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template).
## License
<a rel="license" href="http://creativecommons.org/licenses/by/4.0/deed.en_US"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/by/4.0/80x15.png" /></a><br />This <span xmlns:dct="http://purl.org/dc/terms/" href="http://purl.org/dc/dcmitype/InteractiveResource" rel="dct:type">work</span> by <a xmlns:cc="http://creativecommons.org/ns#" href="http://sindresorhus.com" property="cc:attributionName" rel="cc:attributionURL">Sindre Sorhus</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/4.0/deed.en_US">Creative Commons Attribution 4.0 International License</a>.
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
/* global _ */
(function () {
'use strict';
/* jshint ignore:start */
// Underscore's Template Module
// Courtesy of underscorejs.org
var _ = (function (_) {
_.defaults = function (object) {
if (!object) {
return object;
}
for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
var iterable = arguments[argsIndex];
if (iterable) {
for (var key in iterable) {
if (object[key] == null) {
object[key] = iterable[key];
}
}
}
}
return object;
}
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};
return _;
})({});
if (location.hostname === 'todomvc.com') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
}
/* jshint ignore:end */
function redirect() {
if (location.hostname === 'tastejs.github.io') {
location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
}
}
function findRoot() {
var base = location.href.indexOf('examples/');
return location.href.substr(0, base);
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
xhr.send();
xhr.onload = function () {
if (xhr.status === 200 && callback) {
callback(xhr.responseText);
}
};
}
function Learn(learnJSON, config) {
if (!(this instanceof Learn)) {
return new Learn(learnJSON, config);
}
var template, framework;
if (typeof learnJSON !== 'object') {
try {
learnJSON = JSON.parse(learnJSON);
} catch (e) {
return;
}
}
if (config) {
template = config.template;
framework = config.framework;
}
if (!template && learnJSON.templates) {
template = learnJSON.templates.todomvc;
}
if (!framework && document.querySelector('[data-framework]')) {
framework = document.querySelector('[data-framework]').dataset.framework;
}
this.template = template;
if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({
backend: true
});
} else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append();
}
this.fetchIssueCount();
}
Learn.prototype.append = function (opts) {
var aside = document.createElement('aside');
aside.innerHTML = _.template(this.template, this.frameworkJSON);
aside.className = 'learn';
if (opts && opts.backend) {
// Remove demo link
var sourceLinks = aside.querySelector('.source-links');
var heading = sourceLinks.firstElementChild;
var sourceLink = sourceLinks.lastElementChild;
// Correct link path
var href = sourceLink.getAttribute('href');
sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
} else {
// Localize demo links
var demoLinks = aside.querySelectorAll('.demo-link');
Array.prototype.forEach.call(demoLinks, function (demoLink) {
if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
}
});
}
document.body.className = (document.body.className + ' learn-bar').trim();
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
};
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect();
// getFile('learn.json', Learn);
})();
{
"name": "todomvc-common",
"version": "1.0.2",
"description": "Common TodoMVC utilities used by our apps",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tastejs/todomvc-common"
},
"author": {
"name": "TasteJS"
},
"main": "base.js",
"files": [
"base.js",
"base.css"
],
"keywords": [
"todomvc",
"tastejs",
"util",
"utilities"
],
"gitHead": "e82d0c79e01687ce7407df786cc784ad82166cb3",
"bugs": {
"url": "https://github.com/tastejs/todomvc-common/issues"
},
"homepage": "https://github.com/tastejs/todomvc-common",
"_id": "todomvc-common@1.0.2",
"scripts": {},
"_shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
"_from": "todomvc-common@>=1.0.0 <2.0.0",
"_npmVersion": "2.7.4",
"_nodeVersion": "0.12.2",
"_npmUser": {
"name": "sindresorhus",
"email": "sindresorhus@gmail.com"
},
"dist": {
"shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
"tarball": "http://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz"
},
"maintainers": [
{
"name": "sindresorhus",
"email": "sindresorhus@gmail.com"
},
{
"name": "addyosmani",
"email": "addyosmani@gmail.com"
},
{
"name": "passy",
"email": "phartig@rdrei.net"
},
{
"name": "stephenplusplus",
"email": "sawchuk@gmail.com"
}
],
"directories": {},
"_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz",
"readme": "ERROR: No README data found!"
}
# todomvc-common
> Common TodoMVC utilities used by our apps
## Install
```
$ npm install --save todomvc-common
```
## License
MIT © [TasteJS](http://tastejs.com)
{
"private": true,
"dependencies": {
"todomvc-app-css": "^2.0.0",
"todomvc-common": "^1.0.0"
}
}
// +build ignore
package main
import (
"log"
"net/http"
)
func main() {
addr := "localhost:8000"
log.Println("Serving on http://" + addr)
log.Fatal(http.ListenAndServe(addr, http.FileServer(http.Dir("."))))
}
......@@ -173,6 +173,9 @@
<li class="routing">
<a href="examples/js_of_ocaml/" data-source="http://ocsigen.org/js_of_ocaml/" data-content="Js_of_ocaml is a compiler of OCaml bytecode to Javascript.">js_of_ocaml</a>
</li>
<li class="routing">
<a href="examples/humble/" data-source="https://github.com/go-humble/humble" data-content="Humble is a collection of loosely-coupled tools designed to build client-side and hybrid web applications using GopherJS, which compiles Go code to JavaScript.">Humble + GopherJS</a>
</li>
</ul>
</div>
<div class="js-app-list" data-app-list="labs">
......
......@@ -1092,6 +1092,28 @@
}]
}]
},
"humble": {
"name": "Humble + GopherJS",
"description": "Humble is a collection of loosely-coupled tools designed to build client-side and hybrid web applications using GopherJS, which compiles Go code to JavaScript.",
"homepage": "https://github.com/go-humble/humble",
"examples": [{
"name": "Example",
"url": "examples/humble"
}],
"link_groups": [{
"heading": "Resources",
"links": [{
"name": "Source Code and Documentation",
"url": "https://github.com/go-humble"
}, {
"name": "GopherJS on Stack Overflow",
"url": "http://stackoverflow.com/search?q=gopherjs"
}, {
"name": "GopherJS Google Group",
"url": "https://groups.google.com/forum/#!forum/gopherjs"
}]
}]
},
"javascript": {
"name": "JavaScript",
"description": "JavaScript® (often shortened to JS) is a lightweight, interpreted, object-oriented language with first-class functions, most known as the scripting language for Web pages, but used in many non-browser environments as well such as node.js or Apache CouchDB.",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment