Last active
October 26, 2016 11:41
-
-
Save scriptype/85eebf708a41df94b58168aa3ac7c622 to your computer and use it in GitHub Desktop.
Single Page Application that doesn't depend on a framework
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function EventEmitter(options) { | |
this._listeners = {} | |
} | |
EventEmitter.prototype.on = function(event, handler) { | |
this._listeners[event] = this._listeners[event] || [] | |
this._listeners[event].push(handler) | |
} | |
EventEmitter.prototype.off = function(event, handler) { | |
if (this._listeners[event]) { | |
this._listeners[event] = this._listeners[event].filter(h => h !== handler) | |
} | |
} | |
EventEmitter.prototype.emit = function(event, data) { | |
if (this._listeners[event]) { | |
this._listeners[event].forEach(handler => { | |
requestAnimationFrame(() => { | |
handler({ type: event, data }) | |
}) | |
}) | |
} | |
} | |
function Store(options = {}) { | |
var _super = new EventEmitter | |
_super.constructor.call(this) | |
this._bindEvents = options.bindEvents || {} | |
this._dispatcher = options.dispatcher || { | |
on() {}, | |
off() {}, | |
emit() {} | |
} | |
if (options.initialize instanceof Function) { | |
options.initialize.call(this) | |
} | |
this._options = options | |
this._eventHandlers = Object.keys(this._bindEvents) | |
.reduce((memo, handler) => { | |
return Object.assign({}, memo, { | |
[this._bindEvents[handler]]: this._options[handler].bind(this) | |
}) | |
}, {}) | |
Object.keys(this._eventHandlers).forEach(event => { | |
this._dispatcher.on(event, this._eventHandlers[event]) | |
}) | |
} | |
Store.prototype = Object.create(EventEmitter.prototype) | |
var PRELOADED_USER_DATA = [{ | |
id: 'abc', | |
name: 'John', | |
entries: ['123'] | |
}, { | |
id: 'def', | |
name: 'Doe', | |
entries: ['456', '789'] | |
}, { | |
id: 'ghj', | |
name: 'Trek', | |
entries: [] | |
}] | |
var PRELOADED_ENTRY_DATA = [{ | |
id: '123', | |
writer: 'abc', | |
content: 'Lorem ipsum dolor', | |
createdAt: (new Date('8.30.2011')).getTime() | |
}, { | |
id: '456', | |
writer: 'def', | |
content: 'Sit amet', | |
createdAt: (new Date('10.27.1993')).getTime() | |
}, { | |
id: '789', | |
writer: 'def', | |
content: 'Sit amet', | |
createdAt: (new Date('3.15.2003')).getTime() | |
}] | |
var Constants = { | |
ADD_ENTRY: 'ADD_ENTRY', | |
ADD_USER: 'ADD_USER', | |
SET_ACTIVE_USER_ID: 'SET_ACTIVE_USER_ID' | |
} | |
var Dispatcher = new EventEmitter() | |
var App = { | |
UserStore: new Store({ | |
dispatcher: Dispatcher, | |
bindEvents: { | |
onAddEntry: Constants.ADD_ENTRY, | |
onAddUser: Constants.ADD_USER, | |
onSetActiveUserID: Constants.SET_ACTIVE_USER_ID | |
}, | |
initialize() { | |
this.data = { | |
users: PRELOADED_USER_DATA, | |
activeUserID: PRELOADED_USER_DATA[0].id | |
} | |
}, | |
onAddEntry(event) { | |
var entry = event.data | |
var writer = this.data.users.find(user => user.id === entry.writer) | |
writer.entries.push(entry.id) | |
this.emit('change') | |
}, | |
onAddUser(event) { | |
var user = event.data | |
this.data.users.push(user) | |
this.emit('change') | |
}, | |
onSetActiveUserID(event) { | |
var id = event.data.id | |
this.data.activeUserID = id | |
this.emit('change') | |
} | |
}), | |
EntryStore: new Store({ | |
dispatcher: Dispatcher, | |
bindEvents: { | |
onAddEntry: Constants.ADD_ENTRY | |
}, | |
initialize() { | |
this.data = { | |
entries: PRELOADED_ENTRY_DATA | |
} | |
}, | |
onAddEntry(event) { | |
var entry = event.data | |
this.data.entries.push(entry) | |
this.emit('change') | |
}, | |
}), | |
Actions: { | |
addUser(name) { | |
Dispatcher.emit(Constants.ADD_USER, { | |
id: 'user-' + Date.now(), | |
name, | |
entries: [] | |
}) | |
}, | |
setActiveUserID(id) { | |
Dispatcher.emit(Constants.SET_ACTIVE_USER_ID, { id }) | |
}, | |
addEntry(entry) { | |
var timestamp = Date.now() | |
Dispatcher.emit(Constants.ADD_ENTRY, { | |
id: 'entry-' + timestamp, | |
createdAt: timestamp, | |
writer: entry.writer, | |
content: entry.content | |
}) | |
} | |
}, | |
UserListComponent() { | |
var onClickUser = (event, user) => { | |
this.Actions.setActiveUserID(user.id) | |
} | |
return ` | |
<h4>Users</h4> | |
<ul class="user-list"> | |
${this.state.usersData.users.map(user => { | |
this.addEvents(`user-item-${user.id}`, { | |
click: e => onClickUser(e, user) | |
}) | |
var isActive = user.id === this.state.usersData.activeUserID | |
var activeMod = isActive ? 'user-list__item--active' : '' | |
var sanitizedName = user.name | |
.replace(/\</g, '<') | |
.replace(/\>/g, '>') | |
return ` | |
<li | |
class="user-list__item ${activeMod}" | |
data-events-key="user-item-${user.id}"> | |
${sanitizedName} (${user.entries.length}) | |
</li> | |
` | |
}).join('')} | |
</ul> | |
` | |
}, | |
UserComponent(user) { | |
return ` | |
<h3>User: ${user.name}</h3> | |
${this.NewEntryFormComponent(user.id)} | |
<h4>Entries (${user.entries.length})</h4> | |
<ul class="entries-list"> | |
${ | |
user.entries | |
.map(entryID => { | |
return this.state.entriesData.entries.find(e => { | |
return e.id === entryID | |
}) | |
}) | |
.sort((prev, curr) => { | |
if (prev.createdAt > curr.createdAt) { | |
return 1 | |
} else if (prev.createdAt < curr.createdAt) { | |
return -1 | |
} | |
return 0 | |
}) | |
.map(entry => { | |
var date = new Date(entry.createdAt) | |
var sanitizedContent = entry.content | |
.replace(/\</g, '<') | |
.replace(/\>/g, '>') | |
return ` | |
<li class="entries-list__item"> | |
<p class="entries-list__item-content"> | |
${sanitizedContent} | |
</p> | |
<p class="entries-list__item-date"> | |
${date.toDateString()} | |
</p> | |
</li> | |
` | |
}) | |
.join('') | |
} | |
</ul> | |
` | |
}, | |
NewUserFormComponent() { | |
var onSubmitForm = event => { | |
event.preventDefault() | |
var formData = new FormData(event.target) | |
var username = formData.get('username') | |
this.Actions.addUser(username) | |
} | |
this.addEvents('add-user-form', { | |
submit: onSubmitForm | |
}) | |
return ` | |
<form | |
class="add-user-form" | |
data-events-key="add-user-form"> | |
<label | |
class="add-user-form__label" | |
for="username"> | |
Add New User: | |
</label> | |
<input | |
required | |
class="add-user-form__username-input" | |
name="username" | |
id="username" | |
type="text" | |
placeholder="type username then hit enter" /> | |
</form> | |
` | |
}, | |
NewEntryFormComponent(writer) { | |
var onSubmitForm = event => { | |
event.preventDefault() | |
var formData = new FormData(event.target) | |
var content = formData.get('new-entry-content') | |
this.Actions.addEntry({ | |
writer, | |
content | |
}) | |
} | |
this.addEvents('add-entry-form', { | |
submit: onSubmitForm | |
}) | |
return ` | |
<form | |
class="add-entry-form" | |
data-events-key="add-entry-form"> | |
<label | |
class="add-entry-form__label" | |
for="entry"> | |
Add New Entry: | |
</label> | |
<div class="add-entry-form__controls"> | |
<textarea | |
required | |
class="add-entry-form__entry-input" | |
name="new-entry-content" | |
id="new-entry-content" | |
type="text"></textarea> | |
<button | |
type="submit" | |
class="add-entry-form__submit-button"> | |
Add Entry | |
</button> | |
</div> | |
</form> | |
` | |
}, | |
MainComponent() { | |
var activeUser = this.state.usersData.users.find(user => { | |
return user.id === this.state.usersData.activeUserID | |
}) | |
return ` | |
<div class="main"> | |
<div class="sidebar"> | |
${this.NewUserFormComponent()} | |
${this.UserListComponent()} | |
</div> | |
<div class="content"> | |
${this.UserComponent(activeUser)} | |
</div> | |
</div> | |
` | |
}, | |
getInitialState() { | |
return { | |
usersData: this.UserStore.data, | |
entriesData: this.EntryStore.data | |
} | |
}, | |
init() { | |
this.state = this.getInitialState() | |
this.UserStore.on('change', this.render.bind(this)) | |
this.EntryStore.on('change', this.render.bind(this)) | |
this.render() | |
}, | |
render() { | |
this.state.usersData = this.UserStore.data | |
this.state.entriesData = this.EntryStore.data | |
this.DOMEvents = {} | |
console.log('render', this.state) | |
var el = document.getElementById('app') | |
el.innerHTML = this.MainComponent() | |
this.attachDOMEvents() | |
}, | |
attachDOMEvents() { | |
Object.keys(this.DOMEvents).forEach(key => { | |
var nodes = document.querySelectorAll(`[data-events-key=${key}]`) | |
Array.from(nodes).forEach(node => { | |
Object.keys(this.DOMEvents[key]).forEach(event => { | |
var handler = this.DOMEvents[key][event] | |
node.addEventListener(event, handler) | |
}) | |
}) | |
}) | |
}, | |
addEvents(eventKey, fields) { | |
this.DOMEvents[eventKey] = fields | |
} | |
} | |
App.init() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id='app'></div> | |
<script src="app.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
h1, h2, h3, h4 { | |
margin-top: 0; | |
padding-top: 0; | |
} | |
html, body { | |
width: 100%; | |
height: 100%; | |
} | |
body { | |
font: 100 16px/1.6 helvetica, arial, sans-serif; | |
background: #fff; | |
} | |
.main { | |
display: flex; | |
flex-direction: column; | |
} | |
.sidebar, | |
.content { | |
padding: 15px; | |
box-sizing: border-box; | |
} | |
@media (min-width: 560px) { | |
.main { | |
flex-direction: row; | |
} | |
.sidebar { | |
width: 33.3%; | |
} | |
.content { | |
width: 66.6%; | |
} | |
} | |
.user-list { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
} | |
.user-list__item { | |
padding: .5em .25em; | |
border-bottom: 1px solid #ddd; | |
cursor: pointer; | |
} | |
.user-list__item--active { | |
font-weight: bold; | |
color: #03c; | |
border-color: currentColor; | |
cursor: default; | |
} | |
.add-user-form { | |
display: flex; | |
flex-direction: column; | |
margin-bottom: 2em; | |
padding: 1em; | |
background: #eee; | |
border-radius: 3px; | |
} | |
.add-user-form__label { | |
font-size: .8em; | |
} | |
.add-user-form__username-input { | |
display: block; | |
padding: .25em; | |
} | |
.add-user-form__label { | |
font-size: .8em; | |
} | |
.add-user-form__username-input { | |
display: block; | |
padding: .25em; | |
} | |
.add-entry-form { | |
display: flex; | |
flex-direction: column; | |
margin-bottom: 2em; | |
} | |
.add-entry-form__controls { | |
display: flex; | |
flex-direction: row; | |
} | |
.add-entry-form__entry-input { | |
display: block; | |
width: 75%; | |
font-size: 1.25em; | |
padding: .5em; | |
margin-right: 5%; | |
border-color: #ccc; | |
border-radius: 3px; | |
} | |
.add-entry-form__submit-button { | |
box-sizing: border-box; | |
width: 20%; | |
font-size: .9em; | |
padding: 1em; | |
background: #ddd; | |
border: none; | |
border-radius: 3px; | |
} | |
.add-entry-form__submit-button:hover { | |
background: #eee; | |
} | |
.add-entry-form__submit-button:active { | |
background: #ccc; | |
} | |
.entries-list { | |
list-style: none; | |
margin: 0; | |
padding: 0; | |
} | |
.entries-list__item { | |
padding: .5em 1em; | |
margin-bottom: 1em; | |
background: #eee; | |
} | |
.entries-list__item-date { | |
font-size: .8em; | |
text-align: right; | |
color: #666; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment