Presenter: Ashley Simons
"A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES"
import React from 'react';
import validator from 'validator';
export default class TextInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', validate: false };
this.isValid = this.isValid.bind(this);
this.validationMessage = this.validationMessage.bind(this);
this.onChange = this.onChange.bind(this);
this.shouldValidateForm = this.shouldValidateForm.bind(this);
this.onBlur = this.onBlur.bind(this);
}
isValid(){
if(this.props.required && !this.props.value){
this.message = `${this.props.label} is required. `;
return false;
}
if(this.props.type == "email" && !validator.isEmail(this.props.value)){
this.message = "Must be a valid email address. ";
return false;
}
if(this.props.minLength && !validator.isLength(this.props.value, {min:this.props.minLength, max: undefined})){
this.message = `Must be at least ${this.props.minLength} characters. `;
return false;
}
return true;
}
onChange(e){
this.props.onTextChange(e.target.value);
}
validationMessage(){
return this.message;
}
shouldValidateForm(){
return this.props.shouldValidateForm || this.state.validate;
}
onBlur(e){
this.setState({validate: true});
}
render() {
const { label, required, type, value, id, name, placeholder } = this.props;
return (
<div className={ (this.shouldValidateForm() && !this.isValid()) ? "form-group has-error": "form-group" }>
<label htmlFor={id}>{label} { required && <span className="required-input">*</span> } </label>
<input type={type == "password" ? "password" : "text"} onBlur={this.onBlur} id={id} className="form-control"
name={name} placeholder={placeholder} value={value} onChange={this.onChange} />
{ (this.shouldValidateForm() && !this.isValid())
&& <p className="help-block">{this.validationMessage()}</p> }
</div>
);
}
}
<form method="POST" onSubmit={this.onSubmit} id="loginForm">
<TextInput
name="email"
id="email"
value={this.props.username}
label="Email"
required={true}
type="email"
onTextChange={this.props.onUsernameChange}
shouldValidateForm={this.props.shouldValidateForm}
ref={ textInput => this.usernameInput = textInput }
/>
<TextInput
name="password"
id="password"
value={this.props.password}
label="Password"
type="password"
required={true}
shouldValidateForm={this.props.shouldValidateForm}
onTextChange={this.props.onPasswordChange}
ref={ textInput => this.passwordInput = textInput }
/>
<input type="submit" id="submit" className="btn btn-primary btn-lg m-t-xs col-xs-12" value="Login" />
</form>
How are we going sailing?
Amost everything you were afraid to ask about
ES6 / ES2015
Block scoped `let`
let foo = 'bah';
foo = "fred";
`const` also block scoped
const user = { firstname: "Jeff" } ;
user.surname = "Hobbs";
const myFunction = function(first, second){
// `this` keyword is initialised here
return first + second;
}
Arrow Function
const myFunction = (first, second) => {
// `this` will only exist if it already exists
return first + second;
}
... spreads out your arrays and `iterable` objects
const myFunction = function(){
for(let value of arguments) {
console.log(value);
}
}
const arrayToMerge = ["bazz", "dance"];
const myArray = [...arrayToMerge, "foo", "bah", "zip"];
myFunction(...myArray);
// ouputs:
// arg is bazz
// arg is dance
// arg is foo
// arg is bah
// arg is zip
Chain asynchronous along in order
CipherService.hashPassword(user)
.then(function(user){
return user.saveWithPromise();
})
.then(function(user){
user.firstname = 'New One';
return user;
})
.then(function(user){
// made it here despite not returning a promise
}
.catch(err => done(err)); // no catch = swallowed errors
hashPassword: function (user) {
return new Promise(function(resolve, reject){
bcrypt.genSalt(10, function(err, salt) {
if (err) return reject(err);
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) return reject(err);
user.password = hash;
resolve(user);
});
});
});
}
$ npm install sails -g
$ sails new sails-react-example
$ cd sails-react-example
$ npm install --save sails-disk prototype models/save to disk
Run with:$ sails lift
$ npm install --save-dev webpack grunt-webpack
$ npm i -S react react-dom
$ npm i -D babel-core babel-loader babel-preset-env babel-preset-react babel-preset-airbnb
$ npm install --save babel-polyfill async/await support anyone?
Create tasks/config/webpack.js
const path = require('path');
const BASE_DIR = path.resolve(__dirname, '../..');
const BUILD_DIR = path.resolve(__dirname, '../../', '.tmp/public/js');
const APP_DIR = path.resolve(__dirname, '../../', 'assets/js');
module.exports = function(grunt) {
grunt.config.set('webpack', {
dev: {
context: BASE_DIR,
entry: ['babel-polyfill', APP_DIR + '/index.jsx'],
output: {
path: BUILD_DIR,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.jsx?/,
include: APP_DIR,
loader: 'babel-loader',
options: {
presets: ["env", "react"]
}
}
]
}
}
});
grunt.loadNpmTasks('grunt-webpack');
};
Edit tasks/config/copy.js
module.exports = function(grunt) {
grunt.config.set('copy', {
dev: {
files: [{
expand: true,
cwd: './assets',
src: ['**/*.!(coffee|less|jsx)'],
dest: '.tmp/public'
}],
},
build: {
files: [{
expand: true,
cwd: '.tmp/public',
src: ['**/*'],
dest: 'www'
}]
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
};
Edit tasks/register/compileAssets.js
module.exports = function(grunt) {
grunt.registerTask('compileAssets', [
'clean:dev',
'jst:dev',
'less:dev',
'copy:dev',
'coffee:dev',
'webpack:dev'
]);
};
Edit tasks/register/syncAssets.js
module.exports = function(grunt) {
grunt.registerTask('syncAssets', [
'jst:dev',
'less:dev',
'sync:dev',
'coffee:dev',
'webpack:dev'
]);
};
Edit views/layout.ejs
...
...
Edit views/homepage.ejs
Create assets/js/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
const { render } = ReactDOM;
class HelloWorld extends React.Component {
render() {
return (
Hello World
);
};
}
render (
<HelloWorld />, document.getElementById("react-container")
);
ashleysimons/sails-react-example
Basic install of webpack/react ... e8627884
with Waterline
$ npm i -S sails-mysqldatabase adapter
$ npm i -S dotenvone config for sails and migrations
$ npm i -g db-migrate db-migrate-mysql database migrations
Update config/connections.js with
...
someMysqlServer: {
adapter: 'sails-mysql',
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE_NAME
},
testMysqlServer: {
adapter: 'sails-mysql',
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_TEST_DATABASE_NAME
},
...
Update config/models.js
module.exports.models = {
connection: 'someMysqlServer',
migrate: 'safe'
};
Add to top of config/bootstrap.js
require('dotenv').config();
...
Create .env.example and copy to .env
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_HOST=127.0.0.1
MYSQL_DATABASE_NAME=demo
MYSQL_TEST_DATABASE_NAME=demo_test
Create database.json
{
"dev": {
"host": { "ENV" : "MYSQL_HOST" },
"user": { "ENV" : "MYSQL_USER" },
"password" : { "ENV" : "MYSQL_PASSWORD" },
"database": { "ENV" : "MYSQL_DATABASE_NAME" },
"driver": "mysql",
"multipleStatements": true
}
}
$ db-migrate create users
Update migrations/20170309030724-users.js
'use strict';
var dbm;
var type;
var seed;
/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
};
exports.up = function(db) {
return db.runSql("CREATE TABLE `user` (\
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,\
`firstname` varchar(255) DEFAULT NULL,\
`surname` varchar(255) DEFAULT NULL,\
`email` varchar(255) DEFAULT NULL,\
`password` varchar(255) DEFAULT NULL,\
`terms` tinyint(1) DEFAULT '0',\
`createdAt` datetime DEFAULT NULL,\
`updatedAt` datetime DEFAULT NULL,\
PRIMARY KEY (`id`),\
UNIQUE KEY `email` (`email`)\
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
};
exports.down = function(db) {
return db.runSql("DROP TABLE `user`");
};
exports._meta = {
"version": 1
};
$ db-migrate up
$ sails generate api user generates controller and model
Sails maps controllers and models with the same name into automatic REST behaviour by default. It's called the Blueprint API or Blueprints.
Update api/models/User.js
/**
* User.js
*
* @description :: This represents a user in the system.
* @docs :: http://sailsjs.org/documentation/concepts/models-and-orm/models
*/
module.exports = {
attributes: {
firstname: {
type: 'string',
required: 'true',
},
surname: {
type: 'string',
required: 'true',
},
password: {
type: 'string',
minLength: 10,
required: true,
},
email: {
type: 'string',
unique: true,
index: true,
required: true,
},
terms: {
type: 'boolean',
required: true,
boolean: true,
},
toJSON: function(){
const obj = this.toObject();
delete obj.password;
return obj;
}
},
beforeValidate: function(user, cb){
user.terms = (user.terms == 'on' || user.terms == true) ? true : undefined;
cb();
},
beforeCreate: function(user, cb) {
// hash password
cb();
}
}
Look ma! No controller logic.
GET http://localhost:1337/user
GET http://localhost:1337/user/create?firstname=foo
ashleysimons/sails-react-example
Setting up MySQL, migrations and dotenv ... d45a7df
wishing Adelaide is big enough for
with enzyme and mocha
$ npm i -D mochaour test runner
$ npm i -D babel-registerdynamically transpile all test code
$ npm i -D enzyme jsdom react-addons-test-utilsAirbnb framework, emulated dom
$ npm i -D sinon chaimocks/stubs, assert/expect
$ npm i -D fetch-mock node-localstoragehttp fetch mocking, html5 local storage
$ npm i -D mute mute output temporarily in tests
$ npm i -D barrels supertest fixtures, http requests in tests
Add test/bootstrap.test.js
const sails = require('sails');
require('babel-register')();
require('babel-polyfill'); // Not required for Node, but the test runner might be something else some time
const LocalStorage = require('node-localstorage').LocalStorage;
/**
* print the stack of the error, rather than node just warning you and providing only the error message
*/
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('');
const { window } = jsdom;
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.map(prop => Object.getOwnPropertyDescriptor(src, prop));
Object.defineProperties(target, props);
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js'
};
copyProps(window, global);
global.FormData = document.defaultView.FormData;
global.localStorage = new LocalStorage('./.localStorage/');
global.window.localStorage = global.localStorage;
before(function(done) {
// Increase the Mocha timeout so that Sails has enough time to lift.
this.timeout(5000);
sails.lift({
log: {
level: 'error'
},
models: {
connection: 'testMysqlServer',
migrate: 'drop'
},
environment: 'test',
}, function(err) {
if (err) return done(err);
var Barrels = require('barrels');
var barrels = new Barrels();
global.fixtures = barrels.data;
barrels.populate(function(err) {
done(err, sails);
});
});
});
after(function(done) {
// here you can clear fixtures, etc.
sails.lower(done);
});
Add .babelrc
{
presets: ["env", "react", "airbnb"]
}
Update package.json
// in the scripts section
"test": "NODE_PATH=`pwd` PORT=9999 NODE_ENV=test node ./node_modules/mocha/bin/mocha -R spec -b --recursive"
Add test/fixtures/User.json
[
{
"firstname": "Greg",
"surname": "Applebee",
"password": "demopassword",
"email": "alpha_first_greg@example.com",
"terms": "on"
},
{
"firstname": "Jeff",
"surname": "Jones",
"password": "demopassword",
"email": "alpha_second_jeff@example.com",
"terms": "on"
}
]
test/integration/controllers/UserController.test.js
import { expect, assert } from 'chai';
import request from 'supertest';
const alphaSortFunction = (a, b) => {
if(a.email < b.email) return -1;
if(a.email > b.email) return 1;
return 0;
};
describe('UserController', function() {
const userPath = '/user';
let myRequest;
before(function(){
global.fixtures['user'].sort(alphaSortFunction);
});
describe(`GET ${userPath}`, function(){
beforeEach(function(){
myRequest = request(sails.hooks.http.app)
.get(userPath)
.set('Accept', 'application/json')
});
it('returns the same amount of users as in the fixtures', function(done){
myRequest.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.lengthOf(global.fixtures['user'].length);
done();
});
});
it('firstnames match the fixtures', function(done){
myRequest.end(function(err, res) {
if (err) return done(err);
res.body.sort(alphaSortFunction);
for(var i = 0; i < global.fixtures['user'].length;i++){
expect(res.body[i].firstname).to.equal(global.fixtures['user'][i].firstname);
}
done();
});
});
it('surnames match the fixtures', function(done){
myRequest.end(function(err, res) {
if (err) return done(err);
res.body.sort(alphaSortFunction);
for(var i = 0; i < global.fixtures['user'].length;i++){
expect(res.body[i].surname).to.equal(global.fixtures['user'][i].surname);
}
done();
});
});
it('emails match the fixtures', function(done){
myRequest.end(function(err, res) {
if (err) return done(err);
res.body.sort(alphaSortFunction);
for(var i = 0; i < global.fixtures['user'].length;i++){
expect(res.body[i].email).to.equal(global.fixtures['user'][i].email);
}
done();
});
});
});
describe(`POST ${userPath}`, function() {
it('should create user', function (done) {
let firstname = 'Fred';
let surname = 'Bass';
let email = 'foo@example.com';
let password = 'mydemopassword';
let terms = 'on';
request(sails.hooks.http.app)
.post(userPath)
.set('Accept', 'application/json')
.send({ firstname: firstname, surname: surname, email: email, password: password, terms: terms })
.expect('Content-Type', /json/)
.expect(201)
.end(function(err, res) {
if (err) return done(err);
assert.equal(res.body.firstname, firstname);
assert.equal(res.body.surname, surname);
assert.equal(res.body.email, email);
done();
});
});
});
});
test/integration/jsx/share.js
import React from 'react';
import { expect } from 'chai';
import { mount, render } from 'enzyme';
import Share from 'assets/js/components/share.jsx';
import sinon from 'sinon';
import {
MemoryRouter as Router,
} from 'react-router-dom';
describe("<Share />", function() {
let wrapper;
const user = {
publicUrls: [ {url: 'http://localhost/#/friend/jf0s34ljla'} ]
};
beforeEach(function () {
document.execCommand = sinon.stub().returns(true);
wrapper = mount(<Router><Share user={user} /></Router>);
});
afterEach(function(){
delete document.execCommand;
});
describe("by default", function () {
it("contains a share url", function () {
expect(wrapper.find('.input-group').html()).to.contain(user.publicUrls[0].url);
});
});
describe("upon icon click", function(){
it("flashes a success about copying to clipboard", function(){
wrapper.find('.hover').simulate('click', {});
expect(wrapper.find('.input-group').html()).to.contain("Copied!");
});
it("copies to clipboard", function(){
wrapper.find('.hover').simulate('click', {});
expect(document.execCommand.calledOnce).to.be.true;
});
});
});
test/integration/jsx/login-container.test.js
import React from 'react';
import { expect, assert } from 'chai';
import { mount } from 'enzyme';
import LoginContainer from 'assets/js/components/login/login-container.jsx';
import Login from 'assets/js/components/login/login.jsx';
import fetchMock from 'fetch-mock';
import 'isomorphic-fetch';
import sinon from 'sinon';
describe("<LoginContainer /> integration", function() {
let history;
beforeEach(function(){
history = { push: sinon.spy() };
});
describe("by default", function () {
let wrapper;
beforeEach(function () {
wrapper = mount(<LoginContainer history={history} />);
});
it("renders a login component", function () {
expect(wrapper.find(Login)).to.have.length(1);
});
});
describe("upon empty submit", function(){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {user: "foo"}, status: 200});
onUserChange = sinon.spy();
wrapper = mount(<LoginContainer history={history} onUserChange={onUserChange} />);
sinon.spy(FormData.prototype, 'append');
wrapper.find('form').simulate('submit', { preventDefault: sinon.spy() });
});
afterEach(function(){
fetchMock.restore();
FormData.prototype.append.restore();
});
it("shows required validation errors", function(){
expect(wrapper.find('#email').parent().hasClass('has-error')).to.be.true;
expect(wrapper.find('#password').parent().hasClass('has-error')).to.be.true;
});
it("shows a danger flash message", function(){
expect(wrapper.find('#status').text()).to.contain('There are missing');
expect(wrapper.find('#status').hasClass('alert-danger')).to.be.true;
});
});
describe("upon submit", function(){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {user: "foo"}, status: 200});
onUserChange = sinon.spy();
wrapper = mount(<LoginContainer history={history} onUserChange={onUserChange} />);
wrapper.find('#email').simulate('change', {target: {value: global.fixtures['user'][0].email}});
wrapper.find('#password').simulate('change', {target: {value: global.fixtures['user'][0].password}});
sinon.spy(FormData.prototype, 'append');
wrapper.find('form').simulate('submit', { preventDefault: sinon.spy() });
});
afterEach(function(){
fetchMock.restore();
FormData.prototype.append.restore();
});
it("submits the form with the expected fields", function(done){
setTimeout(function(){
expect(FormData.prototype.append.calledWith('email', global.fixtures['user'][0].email)).to.be.true;
expect(FormData.prototype.append.calledWith('password', global.fixtures['user'][0].password)).to.be.true;
done();
}, 0);
});
});
});
test/unit/jsx/login-container.test.js
import React from 'react';
import { expect, assert } from 'chai';
import { shallow } from 'enzyme';
import LoginContainer from 'assets/js/components/login/login-container.jsx';
import Login from 'assets/js/components/login/login.jsx';
import fetchMock from 'fetch-mock';
import 'isomorphic-fetch';
import sinon from 'sinon';
describe("<LoginContainer />", function() {
let history;
beforeEach(function(){
history = { push: sinon.spy() };
});
describe("by default", function () {
let wrapper;
beforeEach(function () {
wrapper = shallow(<LoginContainer history={history} />);
});
it("renders a login component", function () {
expect(wrapper.find(Login)).to.have.length(1);
});
});
describe("upon submit", function(){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {user: "foo"}, status: 200});
onUserChange = sinon.spy();
wrapper = shallow(<LoginContainer history={history} onUserChange={onUserChange} />);
wrapper.instance().onSubmit();
});
afterEach(function(){
fetchMock.restore();
});
it("sets a info sending flash state", function(){
expect(wrapper.state('flashMessage')).to.contain("Sending");
expect(wrapper.state('flashType')).to.equal('info');
});
it("sets a success flash state", function(done){
setTimeout(function(){
expect(wrapper.state('flashMessage')).to.contain("successful");
expect(wrapper.state('flashType')).to.equal('success');
done();
}, 0);
});
it("passes json response to onUserChange handler", function(done){
setTimeout(function(){
expect(onUserChange.calledOnce).to.be.true;
expect(onUserChange.calledWith("foo"));
done();
}, 0);
});
it("navigates to start", function(done){
setTimeout(function(){
expect(history.push.calledWith("/started")).to.be.true;
done();
}, 0);
});
});
describe("upon submit with 500 error", function(){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {}, status: 500});
wrapper = shallow(<LoginContainer history={history} onUserChange={onUserChange} />);
wrapper.instance().onSubmit();
});
afterEach(function(){
fetchMock.restore();
});
it("sets a danger technical error flash state", function(done){
setTimeout(function(){
expect(wrapper.state('flashMessage')).to.contain("technical problem");
expect(wrapper.state('flashType')).to.equal('danger');
done();
}, 0);
});
});
describe("upon submit with 401 unauthorized", function(done){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {}, status: 401});
wrapper = shallow(<LoginContainer history={history} onUserChange={onUserChange} />);
wrapper.instance().onSubmit();
});
afterEach(function(){
fetchMock.restore();
});
it("sets a danger validation error flash state", function(done){
setTimeout(function(){
expect(wrapper.state('flashMessage')).to.contain("Could not find a user with those credentials");
expect(wrapper.state('flashType')).to.equal('danger');
done();
}, 0);
});
});
describe("upon submit with 400 error", function(done){
let wrapper, onUserChange;
beforeEach(function () {
fetchMock.mock("*", {body: {}, status: 400});
wrapper = shallow(<LoginContainer history={history} onUserChange={onUserChange} />);
wrapper.instance().onSubmit();
});
afterEach(function(){
fetchMock.restore();
});
it("sets a danger validation error flash state", function(done){
setTimeout(function(){
expect(wrapper.state('flashMessage')).to.contain("incorrect details");
expect(wrapper.state('flashType')).to.equal('danger');
done();
}, 0);
});
});
});
ashleysimons/sails-react-example
Setting up testing for React.js and Sails.js ... 032575e
Large react tests 520859e