Scaling with a Progressive JS Framework
Declarative
Rendering
Components
Client-side
Routing
Large Scale
State Management
github.com/ryanhightower/...
<form name="questionnaire-form" id="questionnaire-form" method="post">
<div class="question heading">
<h3>Do you prefer Block Print or Manuscript?</h3>
</div>
<div class="answer">
<input type="radio" name="text_style" id="block" value="block" checked="checked">
<label for="block">Block Print</label>
<input type="radio" name="text_style" id="manuscript" value="manuscript">
<label for="manuscript">Manuscript</label>
<a id="continue-1" href="#"> Continue </i></a>
</div>
<div class="question heading">
<h3>How many students are in your class?</h3>
</div>
<div class="answer">
<input type="number" name="num-students" id="num-students" min="1" value="1">
<label for="num-students">Students</label>
<a id="continue-2" href="#"> Continue </a>
</div>
<div class="question heading">
<h3></h3>
</div>
<div class="answer">
<input type="submit" id="submit-curriculum" value="Calculate Materials">
</div>
</form>// ----------------------------------------------------------
// curriculum.js
// used to show questionnaire
// ----------------------------------------------------------
$(document).ready(function(){
// variables
var $form = $("#curriculum-form"); // cache DOM elements for speed and easy access.
var $result = $('span.result');
var $next = $('a[id*=continue]');
var $numStudents = $('#num-students');
var $studentInputs = $('#num-students').find('input[name*="students"]');
var valid = {
}
$form.accordion();
$form.on('submit', submitHandler);
function submitHandler (e) {
e.preventDefault();
return false;
}
$form.find('#submit-curriculum').on('click', function(e){
e.preventDefault();
$form.submit(e);
swal('Form Submitted', 'Way to go there, Tiger!', 'success');
});
// radio buttons
var $label = $form.find("input[type='radio'] + label");
$label.click(function(){
$(this).parent().siblings('p.continue').find('a').addClass('red-text');
});
// Send them to the itemized form if needed.
$next.eq(0).click( function(e){
e.preventDefault();
// console.log( $form.find('[name=returning]:checked').val() );
if( $form.find('[name=returning]:checked').val() == "YES" ){
// alert("go to next page");
// window.location = "/kindergarten-curriculum/itemized-form/";
// return false;
swal('Did you know?', 'If you`ve already purchased the curriculum, you can find the individual items on the next page.', 'info');
} else {
$result.eq(0).html("NO")
openNext($form);
}
});
// Block or Manuscript
$next.eq(1).click( function(e){
e.preventDefault();
// console.log( $form.find('[name=returning]:checked').val() );
var val = $form.find('[name=image_option]:checked').val();
$result.eq(1).html( val )
if($('#ui-id-5').hasClass('hide')){
$form.accordion('option','active', 3);
}else{
openNext($form);
}
if( val == "Manuscript" ){
$result.eq(1).addClass('script');
} else {
$result.eq(1).removeClass('script');
}
});
...
how many steps does it take to make a sandwich?
var app = new Vue({
el: '#app',
data: {
show: {
sections: {
'q1': true,
'q2': false,
'submit': false
}
},
answers: {
'style':'',
'students': 1
}
},
methods: {
toggleQuestion (section) {
this.show.sections[section] = !this.show.sections[section];
},
submitQuestionnaire () {
// your ajax method here
if( ! this.validateAnswers() ) {
swal('Oops!', 'You didn`t answer one or more questions correctly. Try again.', 'error');
return;
}
swal('Way to go, Tiger!', 'Your answers have been received!', 'success');
},
validateAnswers () {
return this.answers.students >= 1
&& this.answers.style === 'block'
|| this.answers.style === 'manuscript'
}
}
});<form name="questionnaire-form" method="post" action="">
<!-- Question 1 -->
<div class="question heading">
<h3>Do you prefer Block Print or Manuscript?</h3>
</div>
<div class="answer bookStyle" v-show="show.sections.q1">
<input type="radio" name="image_option" id="blockimage" value="block" checked="checked"
v-model="answers.style">
<label class="btn block" for="blockimage">Block Print</label>
<input type="radio" name="image_option" id="manuscript" value="manuscript"
v-model="answers.style">
<label for="manuscript">Manuscript</label>
<p class="continue"><a href="#"
@click.prevent="toggleSection('q2')">Continue</i></a></p>
</div>
<!-- Question 2 -->
<div class="question heading">
<h3>How many students are you purchasing the curriculum for?</h3>
</div>
<div class="answer numStudents" v-show="show.sections.q2">
<input type="number" name="num-students" min="1" value="1"
v-model="answers.students.number">
<label for="num-students">Students</label>
<!-- Continue -->
<p class="continue"><a href="#"
@click.prevent="toggleSection('submit')">Continue</a></p>
</div>
</div>
<!-- Submit -->
<div class="answer text-right" v-show="show.sections.submit">
<div class="col-sm-8">
<input type="submit" value="Calculate Curriculum"
@click.prevent.stop="submitQuestionnaire">
</div>
</div>
</form>
<!-- index.html -->
<!-- Question 2 -->
...
<div class="answer numStudents" v-show="show.sections.q2">
...
</div>// questionnaire.js
const app = new Vue({
el: "#app",
data: {
show: {
sections: {
'q1': true,
'q2': false,
'submit': false
}
}
}
});
<!-- Question 1 -->
...
<a href="#" v-on:click.prevent="toggleSection('q2')">Continue</a>const app = new Vue({
el: "#app",
data: {
show: {
sections: {
'q1': true,
'q2': false,
'submit': false
}
}
},
methods: {
toggleSection (section) {
this.show.sections[section] = ! this.show.sections[section];
}
}
});
<!-- Submit -->
...
<input type="submit" value="Calculate Curriculum"
@click.prevent.stop="submitQuestionnaire"> data: {
show: {...},
answers: {
'students': 1,
'style': 'block'
},
methods: {
toggleSection (section) {...},
submitQuestionnaire () {
if( ! this.validateAnswers() ) {
alert('Oops! You didn`t answer one or more questions correctly.');
return false;
}
document.questionnaireForm.submit();
},
validateAnswers () {
return this.answers.students >= 1
&& this.answers.style === 'block'
|| this.answers.style === 'manuscript'
}
}
<input type="radio" name="text_style" value="block" v-model="answers.style">
<input type="radio" name="text_style" value="manuscript" v-model="answers.style">
<input type="number" name="num-students" min="1" value="1" v-model="answers.students">
data: {
show: {...},
answers: {
'students': 1,
'style': 'block'
},
methods: {
toggleSection (section) {...},
submitQuestionnaire () {
if( ! this.validateAnswers() ) {
alert('Oops! You didn`t answer one or more questions correctly.');
return false;
}
document.questionnaireForm.submit();
},
validateAnswers () {
return this.answers.students >= 1
&& this.answers.style === 'block'
|| this.answers.style === 'manuscript'
}
}
<!-- index.html -->
...
<rh-questionnaire></rh-questionnaire>
...
<script src="js/components/questionnaire.js"></script>// questionnaire.js
const rhQuestionnaire = Vue.component('rhQuestionnaire', {
template: `<form name="questionnaire-form">...</form>`,
data () {
return {
show: { ... },
answers: { ... }
}
},
methods: {...}
});
const app = new Vue({
el: '#app',
components: {
rhQuestionnaire
}
});Product Form
component
<!-- index.html -->
...
<rh-questionnaire></rh-questionnaire>
<rh-product-form :products="myProductsArray"></rh-product-form>
...
<script src="js/components/product-row.js"></script>
<script src="js/components/product-form.js"></script>// product-form.js
const rhProductForm = Vue.component('rhProductForm', {
props: ['products'],
template: `<form name="product-form">...
<product-row v-for="product in products" :product="product"></product-row>
... </form>`,
components: { productRow }
data () {...},
methods: {...}
});
const app = new Vue({
el: '#app',
components: {
rhQuestionnaire,
rhProductForm
}
});Product Form
Product Row
VueRouter
<!-- index.html -->
...
<router-view></router-view>
<script src="vendor/js/vue.min.js"></script>
<script src="vendor/js/vue-router.min.js"></script>
<script src="js/main.js"></script>
<!-- component scripts -->// main.js
const router = new VueRouter({
routes: [
{ path: '/', component: 'rhQuestionnaire' },
{ path: '/products', component: 'rhProductForm' },
{ path: '/review', component: 'rhReviewOrder' }
]
});
var app = new Vue({
router, // shorthand for router: router
data: { ... }
}).$mount('#app');<!-- index.html -->
...
<ul>
<router-link to="/">Questionnaire</router-link>
<router-link to="/products">Required Products</router-link>
<router-link to="/review">Review Order</router-link>
</ul>
====
<!-- rendered html -->
<ul>
<a class="router-link-active">Questionnaire</a>
<a class="">Required Products</a>
<a class="">Review Order</a>
</ul><!-- index.html -->
...
<ul>
<router-link tag="li" to="/"><a>Questionnaire</a></router-link>
<router-link tag="li" to="/products"><a>Required Products</a></router-link>
<router-link tag="li" to="/review"><a>Review Order</a></router-link>
</ul>
====
<!-- rendered html -->
<ul>
<li class="router-link-active"><a>Questionnaire</a></li>
<li class=""><a>Required Products</a></li>
<li class=""><a>Review Order</a></li>
</ul>// questionnaire.js
...
template: `...
<input type="submit" @click.prevent="submitQuestionnaire" value="Calculate Materials">
...`,
methods: {...
submitQuestionnaire () {
if (! this.validateAnswers()) {...}
this.$router.push('/products');
}
}
});State Management Pattern + Library
//store.js
const store = new Vuex.Store({
state: {
cart: {
'SKU011': 1,
... },
answers: {...},
products: [...]
},
mutations: {
addToCart (state, products) {
new Map( Object.entries(products) ).forEach((quantity, sku)=>{
if(state.cart[sku]){
state.cart[sku] += quantity
}else {
Vue.set(state.cart, sku, quantity);
}
});
}
},
actions: {
addToCart (context, products) {
myApi.addToCart(products)
.then((response) => {
context.commit('addToCart', response.data.products)
})
.catch((error) => {
// log error and inform user ...
});
}, ...
}
});
//product-form.js
data () {
return {
localCart: {...},
isSubmitting: false
}
},
computed: {
answers () { return this.$store.state.answers; }
},
methods: {
addToCart () {
if(! this.validateForm()) return false;
this.isSubmitting = true;
this.$store.dispatch('addToCart', this.localCart)
.then( () => swal('Products Added!', 'Continue to checkout.', 'success') )
.then( (res) => {
if(res){
this.goToCheckout();
}
})
.catch((error) => { // handle error })
.then(()=>{ this.isSubmitting = false; });
},
updateSuggestedQuantities(){
this.studentProducts.map((product)=>{
this.localCart[product.sku] = this.answers.students;
});
},
goToCheckout () {
this.$router.push({path:'review'});
},
validateForm () { return true; }
}, ...
social: @ryanhightower
email: ryan@yours.co
https://yours.co