The Vue from up Here

Scaling with a Progressive JS Framework

What do you mean, a "Progressive Framework"?

Levels of Complexity

Declarative

Rendering

Components

Client-side

Routing

Large Scale

State Management

Code Example

github.com/ryanhightower/...

Code Example (html)

<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>

Code Example (js)

// ----------------------------------------------------------
// 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');
		}
	});


...

Problems

  1. The HTML is constrained by the jQuery conventions.
  2. Adding features is cumbersome and difficult, both to implement and read.
  3. The validation naturally uses thed DOM directly as its source of truth.

 

Declarative Rendering

Make me a sandwich

how many steps does it take to make a sandwich?

Imperative Process

  1. Pull out two pieces of bread.
  2. Grab the peanut butter and jam.
  3. Get out a butter knife.
  4. Lay the pieces of bread on a flat surface.
  5. Open the peanut butter
  6. Dip the knife in the peanut butter and scoop about 2 tablespoons worth out of the jar....

Declarative Process

  1. Make a sandwich.
  2. Give sandwich to child.
  3. Enjoy some of the hidden snacks while your child is occupied.

What does this look like with Vue?

Code Example (js)

  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'
      }
    }
  });

Code Example (html)

<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>

Directives

are special attributes that apply reactive behavior to the rendered DOM.

Directives (v-show)

<!-- 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
        }
      }
    }
});

Directives (v-on)


  <!-- 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];
        }
    }
});

Directives (v-on)


  <!-- 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'
        }
    }

Data Binding (v-model)

<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'
        }
    }

Data Binding (demo)

Components

Components

<!-- 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
    }
});

Components

Product Form

component

Components

<!-- 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
    }
});

Components

Product Form

Product Row

Client-side Routing

VueRouter

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');

VueRouter

<!-- 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>

VueRouter

VueRouter

// questionnaire.js
...
    template: `...
        <input type="submit" @click.prevent="submitQuestionnaire" value="Calculate Materials">
    ...`,
    methods: {...
        submitQuestionnaire () {
            if (! this.validateAnswers()) {...}
            this.$router.push('/products');
        }
    }
});

But wait, there's more!

  • Nested routes
  • Named views
  • Route guards
  • Lifecycle hooks
  • and more!

State Management

Vuex

State Management Pattern + Library

Vuex

//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 ...
        });
    }, ...
  }
});

Vuex

//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; }
}, ...

Vuex

Ryan Hightower

social: @ryanhightower

email: ryan@yours.co

https://yours.co

Made with Slides.com