Skip to content

Instantly share code, notes, and snippets.

@scriptype
Last active October 26, 2016 11:41
Show Gist options
  • Save scriptype/85eebf708a41df94b58168aa3ac7c622 to your computer and use it in GitHub Desktop.
Save scriptype/85eebf708a41df94b58168aa3ac7c622 to your computer and use it in GitHub Desktop.
Single Page Application that doesn't depend on a framework
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, '&lt;')
.replace(/\>/g, '&gt;')
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, '&lt;')
.replace(/\>/g, '&gt;')
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()
<div id='app'></div>
<script src="app.js"></script>
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