Skip to content

Instantly share code, notes, and snippets.

@eladg
Created July 18, 2016 06:20
Show Gist options
  • Save eladg/58c2e93163a2eaf7d527ce21bcee0f7b to your computer and use it in GitHub Desktop.
Save eladg/58c2e93163a2eaf7d527ce21bcee0f7b to your computer and use it in GitHub Desktop.
Simple ReactJS Contacts App
var app = app || {};
(function() {
'use strict';
var ContactsApp = React.createClass({
getInitialState: function() {
// was considering a hash design which would
// simplify lookup (instead of using indexOf),
// yet since input is not tied to a form, an
// element is require to be able to access it's id.
// An array was a good enough solution for the need,
// yet I beleive an key/value design would work better
// for big data structures.
return {
data: [
{
id: this.uuid(),
name: "Elad Gariany",
email: "elad@gariany.com",
notes: "Super cool guy",
edit: false,
validated: true,
},
{
id: this.uuid(),
name: "Elad Gariany2",
email: "ee@gariany.com",
notes: "You should totally hire him... 😇",
edit: false,
validated: true,
},
{
id: this.uuid(),
name: "Tokiyoto Kakapipi",
email: "toki@yoto.com",
notes: "",
edit: true,
validated: true,
}
]
};
},
// generate a unique identifier for each person on the table.
// borrowed from: https://github.com/tastejs/todomvc/blob/master/examples/react/js/utils.js#L7
uuid: function () {
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
.toString(16);
}
return uuid;
},
// creates a new model and add it to the view state.
// rows are considered invalidated until they are properly saved
handleAdd: function(model) {
var data = this.state.data;
var newItem = {
id: this.uuid(),
name: "Enter New Name",
email: "email@example.org",
notes: "",
edit: true,
validated: false,
}
data.push(newItem);
this.setState({data: data});
},
// change the state of a record to "editable", which will generate
// a ContactRowEdit container rather then a regular ContactRow
// once the table will be re-rendered.
handleEdit: function(model) {
var data = this.state.data;
model.edit = true
data[model.id] = model;
this.setState({data: data});
},
// located the given binded data to the tables data state
// and remove it from the array. The row will be missing
// once the table will be re-rendered.
handleDelete: function(model) {
var data = this.state.data;
var index = data.indexOf(model);
data.splice(index, 1);
this.setState({data: data});
},
// handle user clicking on the save button
handleSave: function(model) {
var data = this.state.data;
var index = data.indexOf(model);
var item = data[index];
// I've used getElementById since the original format dictated a table layout.
// Mixing a form/inputs and a table caused some troubles, so I used a primitive solution.
// A better layout would be a proper react form with binded inputs.
// reference: https://facebook.github.io/react/docs/forms.html
item.name = document.getElementById('input-name-'+model.id).value;
item.email = document.getElementById('input-email-'+model.id).value;
item.edit = false;
item.notes = "";
item.validated = true;
// validate email is formated
if (!this.validateEmailFormat(item.email)) {
item.notes = item.email + " is misformatted email :("
item.validated = false;
}
// validate email is unique
var emails = data.map(function(e){return e.email} );
if (!this.validateEmailUniqueness(item.email, emails)) {
item.notes = item.email + " already exists on another entry"
item.validated = false;
}
data[index] = item;
this.setState({data: data});
},
// validates that given email appears only once
//
// KNOWN ISSUE: the validation of an email uniqueness is performed
// per contact save request and not on the data level.
// Therefor, if 2 rows have the same email, and the
// second was deleted, the first will be considered invalid
// until it will go through validation (happends on handleSave).
//
validateEmailUniqueness: function(email, emailsArray) {
var counter = 0;
for (var i = 0; i < emailsArray.length; i++) {
if (emailsArray[i] === email) {
counter++;
}
}
return counter === 1;
},
// Return true for properly formatted emails
//
// borrowed from: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
validateEmailFormat: function(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
},
// ContactsApp Rendering method.
//
// Render ContactRow or ContactRowEdit for each element on the
// props data model. Render the "+ new contact" button on the
// bottom of the table.
render: function() {
return (
<div>
<section>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th colSpan="2"></th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{this.state.data.map(function(item) {
if (!item.edit) {
return (
<ContactRow
key={item.id}
data={item}
onEdit={this.handleEdit.bind(this, item)}
onDelete={this.handleDelete.bind(this, item)}
/>
);
} else {
return (
<ContactRowEdit
key={item.id}
data={item}
onSubmit={this.handleSave.bind(this, item)}
onDelete={this.handleDelete.bind(this, item)}
/>
);
}
}, this)}
</tbody>
</table>
</section>
<ButtonsSection
onClick={this.handleAdd}
></ButtonsSection>
</div>
);
},
});
// ButtonsSection Rendering method.
//
// A simple button to add new contact to the list.
var ButtonsSection = React.createClass({
render: function() {
return (
<section>
<button onClick={ this.props.onClick }>+ Add Contact 👳</button>
</section>
);
}
});
// ContactRow Rendering method.
//
// A regular contact's raw on the table.
// If something is wrong with the record (i.e. email validation),
// add the 'invalidated' class to highlight it to the user.
// The Note section was used to spell out what's wrong with the record.
var ContactRow = React.createClass({
render: function() {
return (
<tr className={this.props.data.validated ? "" : "invalidated"}>
<td>{this.props.data.name}</td>
<td>{this.props.data.email}</td>
<td><button onClick={this.props.onEdit.bind(this, this.props.data)}>Edit</button></td>
<td><button onClick={this.props.onDelete}>Delete</button></td>
<td><i><small>{this.props.data.notes}</small></i></td>
</tr>
);
}
});
// ContactRowEdit Rendering method.
//
// An editable table row. I've decided to used a primitive id for reasons
// that are listed above on the handleSave method.
var ContactRowEdit = React.createClass({
render: function() {
return(
<tr className={this.props.data.validated ? "" : "invalidated"}>
<td><input defaultValue={this.props.data.name} id={"input-name-"+this.props.data.id}/></td>
<td><input defaultValue={this.props.data.email} id={"input-email-"+this.props.data.id}/></td>
<td><button onClick={this.props.onSubmit}>Save</button></td>
<td><button onClick={this.props.onDelete}>Delete</button></td>
<td><i><small>{this.props.data.notes}</small></i></td>
</tr>
);
}
});
// ==========================================================================
// Application mounting point.
// ==========================================================================
ReactDOM.render(
<ContactsApp></ContactsApp>,
document.getElementById('contactsapp')
);
})();
<head>
<meta charset="utf-8" />
<title>Contacts</title>
<style>
/* Just enough CSS to make things not hurt your eyes. */
@import url(https://cdn.rawgit.com/tylerchilds/Vanilla-HTML/master/css/vanilla.css);
</style>
</head>
<body class="vanilla">
<div id="contactsapp">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"</script>
<script src="https://fb.me/react-15.2.1.js"></script>
<script src="https://fb.me/react-dom-15.2.1.js"></script>
<script src="http://fb.me/JSXTransformer-0.12.0.js"></script>
</body>
.invalidated {
background-color:#fff0f0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment