Vue JS - Filtering items from user input

Hi there,

I’m just starting out with looking at Vue (training in Vue 2 at the moment due to the site infrastructure we have) and have built out a simple quiz/filtering app. All of the inputs are being generated based on the object items and their parameters such as gender, bike brand, colour etc. However, I’m now at the stage where I want to connect these inputs to the list of items which are initially shown from the unfiltered object items.

I’ve found various guides which detail how to filter based on single parameter value. However what I’m struggling with is that my app has a range of input types which are dependent on the user selection - granted most of them are checkboxes but as a result they require me to track multiple inputs from the user and filter accordingly. For example, I imagine that filtering by the radio button inputs is quite simple as I’d do a like-for-like match, whereas multiple selections of brands and colours seems much more complex?

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Bike Finder</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <div id="app">
        <product></product>
    </div>
    <script src="vue.js"></script>
    <script src="script_filtereditems.js" async defer></script>
</body>

</html>
Vue.component('product', {
    props: {

    },
    template: `
    <div class="container">
        <h2>About You</h2>
        <div class="question-section question-section--aboutyou">
            <div class="container__question">
                <strong>Preferred bike gender?</strong>
                <div>
                    <template v-for="gender in filteredGenders">
                        <input type="radio" :id="gender" name="gender" :key="gender"><label :for="gender">{{gender}}</label>
                    </template>
                </div>
            </div>
            <div class="container__question">
                <strong>What terrain do you normally ride on?</strong>
                <div>
                    <template v-for="terrain in filteredTerrains">
                        <input type="checkbox" :id="terrain" :name="terrain" :key="terrain"><label :for="terrain">{{terrain}}</label>
                    </template>
                </div>
            </div>
            <div class="container__question">
                <strong>What is your price range?</strong>
                <div>
                    <input type="checkbox" id="priceRangeNoPref"><label for="priceRangeNoPref">No preference</label>
                    <input type="checkbox" id="priceRange200400"><label for="priceRange200400">&pound;200-400</label>
                    <input type="checkbox" id="priceRange400600"><label for="priceRange400600">&pound;400-600</label>
                    <input type="checkbox" id="priceRange6001000"><label for="priceRange6001000">&pound;600-1,000</label>
                    <input type="checkbox" id="priceRange1000plus"><label for="priceRange1000plus">&pound;1,000+</label>
                </div>
            </div>
            
            <div class="container__question">
                <strong>What is your average mileage per week?</strong>
                <div>
                    <input type="radio" id="avgMilesUndisclosed" name="average-miles"><label for="avgMilesUndisclosed">Prefer not to say</label>
                    <input type="radio" id="avgMilesLess25" name="average-miles"><label for="avgMilesLess25">Less than 25</label>
                    <input type="radio" id="avgMiles2550" name="average-miles"><label for="avgMiles2550">25-50 miles</label>
                    <input type="radio" id="avgMiles50100" name="average-miles"><label for="avgMiles50100">50-100 miles</label>
                    <input type="radio" id="avgMiles100plus" name="average-miles"><label for="avgMiles100plus">100+ miles</label>
                </div>
            </div>
            
            <button id="butAboutYouComplete">Complete, next section!</button>
        </div>

        <h2>Bike Preferences</h2>
        <div class="question-section question-section--bike-preferences">
            <div class="container__question">
                <strong>Do you have any brand favourites?</strong>
                <div>
                    <template v-for="brand in filteredBrands">
                        <input type="checkbox" class="favourite-brand" :key="brand" :name="brand" :id="brand"><label :for="brand" class="favourite-brand">{{ brand }}</label>
                    </template>
                </div>
            </div>
            <div class="container__question">
                <strong>Any particular colour you're looking for?</strong>
                <div class="container__answer--colours">
                    <template v-for="colour in filteredColours">
                        <input type="checkbox" class="colour" :key="colour" :name="colour" :id="colour"><label :for="colour" class="colour" :style="{'background-color': colour}"></label>
                    </template>
                </div>
            </div>
            <div class="container__question">
                <strong>What is preference on gears?</strong>
                <div>
                    <template v-for="gear in filteredGears">
                        <input type="checkbox" class="favourite-gears" :key="gear" :name="gear" :id="gear"><label :for="gear" class="favourite-gear">{{gear}}</label>
                    </template>
                </div>
            </div>
            <div class="container__question">
                <strong>What is your preferred suspension type?</strong>
                <div>
                    <template v-for="suspension in filteredSuspension">
                        <input type="checkbox" class="favourite-suspension" :key="suspension" :name="suspension" :id="suspension"><label :for="suspension" class="favourite-suspension">{{suspension}}</label>
                    </template>
                </div>
            </div>
        </div>
        <h2>Products:</h2>
        <div v-html="productFeed" class="product-feed"></div>
    </div>
    `,

    data() {
        return {
            "bikes": [
                {
                    id: 1,
                    image: "womens_hybrid_apollo_cosmo_blue.jpg",
                    terrain: "gravel",
                    gender: "womens",
                    type: "hybrid",
                    brand: "apollo",
                    name: "cosmo",
                    colour: "blue",
                    gears: 18,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 205,
                    priceEuros: null
                },
                {
                    id: 2,
                    image: "mens_hybrid_boardman_hyb89_blue.jpg",
                    terrain: "road",
                    gender: "mens",
                    type: "hybrid",
                    brand: "boardman",
                    name: "hyb89",
                    colour: "blue",
                    gears: 20,
                    suspension: "hard tail",
                    brakeType: "hydraulic disc brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: 2,
                    pricePounds: 750,
                    priceEuros: null
                },
                {
                    id: 3,
                    image: "mens_hybrid_carrera_code_orange.jpg",
                    terrain: "gravel",
                    gender: "mens",
                    type: "hybrid",
                    brand: "carrera",
                    name: "code",
                    colour: "orange",
                    gears: 16,
                    suspension: "hard tail",
                    brakeType: "mechanical disc brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 350,
                    priceEuros: null
                },
                {
                    id: 4,
                    image: "mens_mountain_apollo_valier_green.jpg",
                    terrain: "mountain",
                    gender: "mens",
                    type: "mountain",
                    brand: "apollo",
                    name: "valier",
                    colour: "green",
                    gears: 21,
                    suspension: "hard tail",
                    brakeType: "mechanical disc brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: 4,
                    pricePounds: 228,
                    priceEuros: null
                },
                {
                    id: 5,
                    image: "neutral_adventure_voodoo_limba_green.jpg",
                    terrain: "mountain",
                    gender: "neutral",
                    type: "adventure",
                    brand: "voodoo",
                    name: "limba",
                    colour: "green",
                    gears: 16,
                    suspension: "none",
                    brakeType: "mechanical disc brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: 2,
                    pricePounds: 428,
                    priceEuros: null
                },
                {
                    id: 6,
                    image: "neutral_bmx_voodoo_malice_grey.jpg",
                    terrain: "stunt",
                    gender: "neutral",
                    type: "bmx",
                    brand: "voodoo",
                    name: "malice",
                    colour: "black",
                    gears: 0,
                    suspension: "none",
                    brakeType: "u brakes",
                    electricMilesMaxRange: null,
                    inStock: false,
                    stockLeadtimeWeeks: 1,
                    pricePounds: 200,
                    priceEuros: null
                },
                {
                    id: 7,
                    image: "neutral_folding_apollo_tuck_blue.jpg",
                    terrain: "road",
                    gender: "neutral",
                    type: "folding",
                    brand: "apollo",
                    name: "tuck",
                    colour: "blue",
                    gears: 1,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 235,
                    priceEuros: null
                },
                {
                    id: 8,
                    image: "neutral_hybrid_electric_assist_2021_white.jpg",
                    terrain: "gravel",
                    gender: "neutral",
                    type: "electric",
                    brand: "assist",
                    name: "2021",
                    colour: "white",
                    gears: 1,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: 20,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 649,
                    priceEuros: null
                },
                {
                    id: 9,
                    image: "neutral_hybrid_electric_assist_stepthru_silver.jpg",
                    terrain: "gravel",
                    gender: "neutral",
                    type: "electric",
                    brand: "assist",
                    name: "stepthru",
                    colour: "silver",
                    gears: 1,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: 20,
                    inStock: false,
                    stockLeadtimeWeeks: 3,
                    pricePounds: 649,
                    priceEuros: null
                },
                {
                    id: 10,
                    image: "kids_hybrid_apollo_haze_goldwhite.jpg",
                    terrain: "road",
                    gender: "kids",
                    type: "hybrid",
                    brand: "apollo",
                    name: "haze",
                    colour: "gold white",
                    gears: 6,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: 2,
                    pricePounds: 150,
                    priceEuros: null
                },
                {
                    id: 11,
                    image: "kids_hybrid_carrera_saruna_red.jpg",
                    terrain: "road",
                    gender: "kids",
                    type: "hybrid",
                    brand: "carrera",
                    name: "saruna",
                    colour: "red",
                    gears: 7,
                    suspension: "none",
                    brakeType: "v-brakes",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: 2,
                    pricePounds: 260,
                    priceEuros: null
                },
                {
                    id: 12,
                    image: "kids_training_apollo_pawpatrol_blueyellow.jpg",
                    terrain: "road",
                    gender: "kids",
                    type: "training",
                    brand: "apollo",
                    name: "paw patrol",
                    colour: "blue yellow",
                    gears: 0,
                    suspension: "none",
                    brakeType: "caliper",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 130,
                    priceEuros: null
                },
                {
                    id: 13,
                    image: "kids_training_apollo_peppapig_pink.jpg",
                    terrain: "road",
                    gender: "kids",
                    type: "training",
                    brand: "apollo",
                    name: "peppa pig",
                    colour: "pink",
                    gears: 0,
                    suspension: "none",
                    brakeType: "caliper",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 130,
                    priceEuros: null
                },
                {
                    id: 14,
                    image: "kids_training_apollo_batman_black.jpg",
                    terrain: "road",
                    gender: "kids",
                    type: "training",
                    brand: "apollo",
                    name: "batman",
                    colour: "black",
                    gears: 0,
                    suspension: "none",
                    brakeType: "caliper",
                    electricMilesMaxRange: null,
                    inStock: true,
                    stockLeadtimeWeeks: null,
                    pricePounds: 175,
                    priceEuros: null
                }
            ]

        }
    },
    methods: {
        cleanData(dataType, dataToClean) {
            const cleanData = [];
            let multiColouredDuplicate = false;
            // console.log(dataToClean);

            for (let i = 0; i < dataToClean.length; i++) {
                let filteredData = dataToClean[i];
                let spaceFound;

                if (typeof filteredData === "string") {
                    spaceFound = filteredData.indexOf(' ') > 0;
                }
                let formattedData = this.formatData(filteredData);

                if (dataType === "colour") {

                    // If space found but duplicate multicoloured result, skip
                    if (spaceFound && multiColouredDuplicate) {
                        multiColouredDuplicate = false;
                        continue;

                        // If space found and first multicoloured
                    } else if (spaceFound && !multiColouredDuplicate) {
                        formattedData = "multicoloured";
                        multiColouredDuplicate = true;
                    }
                }

                cleanData.push(formattedData);
            }

            return cleanData;
        },
        formatData(dataToFormat) {
            let formattedData = dataToFormat;

            return formattedData;
        },
    },
    computed: {
        filteredGenders() {
            const typeGender = "gender";
            const filteredGenders = Array.from(new Set(this.bikes.map(bike => bike.gender)));

            let formattedGenders = this.cleanData(typeGender, filteredGenders);

            return formattedGenders;
        },
        filteredTerrains() {
            const typeTerrain = "terrain";
            const filteredTerrains = Array.from(new Set(this.bikes.map(bike => bike.terrain)));

            let formattedTerrains = this.cleanData(typeTerrain, filteredTerrains);

            return formattedTerrains;
        },
        filteredBrands() {
            const typeBrand = "brand";
            const filteredBrands = Array.from(new Set(this.bikes.map(bike => bike.brand)));

            let formattedBrands = this.cleanData(typeBrand, filteredBrands);

            return formattedBrands;
        },
        filteredColours() {
            const typeColour = "colour";
            const filteredColours = Array.from(new Set(this.bikes.map(bike => bike.colour)));

            let formattedColours = this.cleanData(typeColour, filteredColours);

            return formattedColours;
        },
        filteredGears() {
            const typeGears = "gears";
            const filteredGearNumbers = Array.from(new Set(this.bikes.map(bike => bike.gears))).sort(function (a, b) {
                return a - b
            });

            let formattedGears = this.cleanData(typeGears, filteredGearNumbers);

            return formattedGears;
        },
        filteredSuspension() {
            const typeSuspension = "suspension";
            const filteredSuspension = Array.from(new Set(this.bikes.map(bike => bike.suspension)));

            let formattedSuspension = this.cleanData(typeSuspension, filteredSuspension);

            return formattedSuspension;
        },
        productFeed() {
            let productFeed = "";

            for (let i = 0; i < this.bikes.length; i++) {
                let productFeedItem = this.bikes[i];

                productFeed += `
                <div class="product-feed__item product-feed__item--${productFeedItem.gender}">
                    <span class="product-feed__item__brand">${productFeedItem.brand}</span> - <span class="product-feed__item__gender">${productFeedItem.gender}</span>
                    <img src="images/${productFeedItem.gender}/${productFeedItem.image}" class="product-feed__item__image" />
                </div>`;
                //console.log(`${this.bikes[i].brand}`)
            }

            return productFeed;
        }
    }

})

// Creates new Vue instance with options
var app = new Vue({

    // Property to connect to div with 'app' ID
    el: '#app',
    data: {
        premium: true
    }
})
.question-section {
    margin-bottom: 2rem;
}

.container__question {
    margin-bottom: 2rem;
}

input[type="checkbox"].favourite-brand {
    display: none;
}

input[type="checkbox"].favourite-brand#apollo+label {
    background-image: url('images/brands/brand_apollo.png')
}

input[type="checkbox"].favourite-brand#assist+label {
    background-image: url('images/brands/brand_assist.png')
}

input[type="checkbox"].favourite-brand#boardman+label {
    background-image: url('images/brands/brand_boardman.png')
}

input[type="checkbox"].favourite-brand#carrera+label {
    background-image: url('images/brands/brand_carrera.png')
}

input[type="checkbox"].favourite-brand#voodoo+label {
    background-image: url('images/brands/brand_voodoo.png')
}

label.favourite-brand {
    display: inline-block;
    margin-right: 1rem;
    width: 200px;
    height: 200px;
    cursor: pointer;
    background-color: #66a1bf;
    background-position: center center;
    background-repeat: no-repeat;
    background-size: 100%;
    transition: background-size 0.1s linear;
}

input.favourite-brand:checked+label {
    background-size: 90%;
    background-color: #244f66;
}

.container__answer--colours{
    display: flex;
}

input[type="checkbox"].colour {
    display: none;
}

label.colour {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    margin-right: 1rem;
    width: 50px;
    height: 50px;
    cursor: pointer;
    border: 1px solid black;
    border-radius: 50%;
    opacity: 1;
}

input#multicoloured+label{
    background: rgb(255,0,0);
    background: -moz-linear-gradient(180deg, rgba(255,0,0,1) 0%, rgba(255,158,0,1) 10%, rgba(255,248,0,1) 25%, rgba(91,255,0,1) 40%, rgba(0,224,255,1) 55%, rgba(0,22,255,1) 70%, rgba(192,0,255,1) 85%, rgba(255,0,219,1) 100%);
    background: -webkit-linear-gradient(180deg, rgba(255,0,0,1) 0%, rgba(255,158,0,1) 10%, rgba(255,248,0,1) 25%, rgba(91,255,0,1) 40%, rgba(0,224,255,1) 55%, rgba(0,22,255,1) 70%, rgba(192,0,255,1) 85%, rgba(255,0,219,1) 100%);
    background: linear-gradient(180deg, rgba(255,0,0,1) 0%, rgba(255,158,0,1) 10%, rgba(255,248,0,1) 25%, rgba(91,255,0,1) 40%, rgba(0,224,255,1) 55%, rgba(0,22,255,1) 70%, rgba(192,0,255,1) 85%, rgba(255,0,219,1) 100%);
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#ff0000",endColorstr="#ff00db",GradientType=1);
}

label.colour::before {
    content: " ";
    position: absolute;
    height: 25px;
    transition-duration: 0.4s;
}

input.colour:checked+label.colour::before {
    content: '✓';
    color: #66a1bf;
}

.product-feed{
    display: grid;
    flex-direction: row;
    grid-template-columns: repeat(4, 0.25fr);
    grid-gap: 1rem;
}

.product-feed__item{
    padding: 1rem;
    outline: 1px solid grey;
    border-radius: 10px;
}

.product-feed__item--womens{
    background-color: lightpink;
}

.product-feed__item--mens{
    background-color: lightblue;
}

.product-feed__item--neutral{
    background-color: lightgray;
}

.product-feed__item--kids{
    background-color: lightgreen;
}

.product-feed__item__brand{
    font-weight: 600;
    text-transform: capitalize;
}

.product-feed__item__image{
    width: 100%;
}

.product-feed__item__gender{
    text-transform: capitalize;
}

Any thoughts would be much appreciated.
Thanks in advance!

not particularly much harder than searching for an equals.
Generic JS:

//Search for Single Value 'y'
let y = "fluff";
let result = data.filter((x) => x.someproperty == y);

//Search for multiple values in array 'y'.
let y = ["fluff","stuff","bort"]
let result = data.filter((x) => y.includes(x.someproperty));
1 Like

Sorry for my late reply, have been unwell.
Thanks so much for your help - will give linking it up to my existing Vue code a shot

Hi there,

After reviewing this further, I realised that what I’m struggling with is how to connect the generated radio buttons/checkboxes to the product feed that I’ve created. I believe that the logic to check the actual values should be relatively simple.

I’ve created a cleaner version of my example above and dropped it into Codepen to make it easier to work with.

When I look up how to filter data based on input, a lot of examples are based around a text input field rather than radio buttons/checkboxes, so it’s quite difficult to find a working example of what I’m trying to achieve.

Thanks in advance.

Hi, taking a step back to look at the broader task you are trying to accomplish it seems you have a list of products and you are trying to filter them based on a) a set of radio buttons (only one choice possible), b) a set of checkboxes (multiple choices possible) and you have to use Vue2.

Did I get that right?

Assuming that is the case, here is a simple example that you can copy and run on your PC.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Bikes</title>
</head>
<body>
  <div id="app">
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Terrain</th>
          <th>Gender</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(bike) in filteredBikes" :key="bike.id">
          <td>{{ bike.name }}</td>
          <td>{{ bike.terrain }}</td>
          <td>{{ bike.gender }}</td>
        </tr>
      </tbody>
    </table>

    <label>
      <input type="radio" name="gender" value="male" v-model="selectedGender"> Male
    </label>
    <label>
      <input type="radio" name="gender" value="female" v-model="selectedGender"> Female
    </label>
    <label>
      <input type="radio" name="gender" value="neutral" v-model="selectedGender"> Neutral
    </label>
  </div>

   <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
   <script>
    const app = new Vue({
      el: '#app',
      data: {
        selectedGender: '',
        'bikes': [
          {
          id: 1,
            name: 'cosmo',
            terrain: 'gravel',
            gender: 'female',
          },
          {
          id: 2,
            name: 'hyb89',
            terrain: 'road',
            gender: 'female',
          },
          {
          id: 3,
            name: 'code',
            terrain: 'gravel',
            gender: 'male',
          },
          {
          id: 4,
            name: 'valier',
            terrain: 'mountain',
            gender: 'male',
          },
          {
          id: 5,
            name: 'limba',
            terrain: 'mountain',
            gender: 'neutral',
          },
        ]
      },
      computed: {
        filteredBikes() {
          if (!this.selectedGender) return this.bikes;
          return this.bikes.filter(bike => bike.gender === this.selectedGender);
        }
      }
    });
   </script>
</body>
</html>

As you can see, I have taken the first five of your bikes and am displaying them in a table. There are three radio buttons beneath the table, allowing you to filter them by gender.

Have a look at that and see if you can apply the principle to the code you provided. I’m happy to answer any questions you may have.

Note: sorry to be nitpicky, but I changed the gender field to ‘male’, ‘female’, ‘neutral’. I removed ‘kids’ as this is not a gender. Assuming you want to keep ‘kids’ you should probably find a better descriptor, such as “riderCategory”.

2 Likes

Hi James,

Thanks so much for your help!
I’ve now taken your great example and applied it to my code as follows.

I took a bit of time as I transferred what I had within my original productFeed function and placed it into the HTML using a v-for (still getting used to coding that way!) and also I don’t get as much time to code in my spare time as I would like unfortunately!

I had a few questions based on the example/my rework:

  • When would I normally inject HTML into a page via computed - presumably for anything that’s going to be static?
  • When I tried to use “:key” on a “template” tag, Vue stated that “it couldn’t be keyed”, so I switched this for another element and it worked?
  • Within your “v-for” you used “(bike)”. I’ve not seen this type of reference before, what do the brackets signify? I presume it must be shorthand for something?
  • The filtering wouldn’t work until I had set up the “:value” attribute of each input, much in the same way you had hardcoded this in your example but I don’t see a reference to “value” being read within the filtering code so how does Vue know to link these up?

And yes you are correct, I am looking to filter the products based on the radio buttons and checkbox inputs (with more options coming later on to sort the results etc potentially). Eventually I want to add more sets of checkboxes to track things like the colour and also base the average miles and compare that to the electric range of a bike (if applicable).

Thanks for the tip regarding the gender/category by the way - I got a bit too into the coding side of things and completely missed this!

Thanks again.

Hey, no worries :slight_smile:

No, not static content. Use a computed property when you want to perform calculations or manipulations on your data and have the result be cached. Computed properties are reactive and only recalculate when their dependencies change. In our example it is the reactivity we are interested in, as we want to automatically update the DOM when selectedGender changes.

The template tag is a “virtual” element and not rendered in the DOM. Vue’s :key is designed to be used on actual DOM elements (like the div you have applied it to) to uniquely identify them.

Oops, sorry. I originally had:

<tr v-for="(bike, index) in filteredBikes" :key="`bike-${index}`">

But then I noticed that you had an ID attribute in your bike data. You can remove the brackets and it will function identically:

<tr v-for="bike in filteredBikes" :key="bike.id">

It’s Vue’s two-way data binding which makes this work. When you specify a v-model on an input, Vue automatically syncs the input’s value with a data property of the same name. :value sets the initial value of the input, and any changes in the input will automatically update the corresponding data property. This updated data property is then used in the computed function for filtering, even if you don’t explicitly see “value” being read.


If you’re looking for a good resource on Vue, I can recommend:

2 Likes

Ah yes I do remember when methods and computed properties should be used now from my initial training; makes sense regarding the “template” tag too - thanks for clarifying!

Thanks for explaining how the two-way data binding works too.

I’ve had a bit of a go at tracking clicks of the checkboxes but I am finding tracking the interactivity of the checkboxes difficult to implement. One way I thought about doing it was to add the selectedTerrains for example into an array which is initially setup as empty. If the length of the array is above 0 then I would loop through and apply the selectedTerrains that way. Probably not the most efficient way to do this but I can’t think of another way to do it?

The other difficulty is that I need to track the individual clicks - at the moment the checkbox clicks are being treated as one group and I’m unsure how to get around that.

Eventually I will want to get to a point where the products are filtered based on a range of values from the form - using an if/else statement doesn’t seem the most efficient way to track inputs from 5-6 sections so any tips on that as well would be appreciated!

You can handle the checkboxes in the same way — by using a computed property.

Here’s an example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Bikes</title>
  <style>
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
    }
    th, td {
      padding: 10px;
      text-align: left;
      border: 1px solid #ddd;
    }
    th {
      background-color: #f2f2f2;
    }
    tr:nth-child(even) {
      background-color: #f2f2f2;
    }
  </style>
</head>
<body>
  <div id="app">
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Terrain</th>
          <th>Gender</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(bike) in filteredBikes" :key="bike.id">
          <td>{{ bike.name }}</td>
          <td>{{ bike.terrain }}</td>
          <td>{{ bike.gender }}</td>
        </tr>
      </tbody>
    </table>

    <h2>Filters</h2>
    <div>
      <label>
        <input type="checkbox" value="mountain" v-model="selectedTerrain"> Mountain
      </label>
      <label>
        <input type="checkbox" value="road" v-model="selectedTerrain"> Road
      </label>
      <label>
        <input type="checkbox" value="gravel" v-model="selectedTerrain"> gravel
      </label>
    </div>
    <div>
      <label>
        <input type="radio" name="gender" value="male" v-model="selectedGender"> Male
      </label>
      <label>
        <input type="radio" name="gender" value="female" v-model="selectedGender"> Female
      </label>
      <label>
        <input type="radio" name="gender" value="neutral" v-model="selectedGender"> Neutral
      </label>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        selectedGender: '',
        selectedTerrain: [],
        'bikes': [
          { id: 1, name: 'ThunderStriker', gender: 'male', terrain: 'mountain' },
          { id: 2, name: 'RoadQueen', gender: 'female', terrain: 'road' },
          { id: 3, name: 'NeutralNimbus', gender: 'neutral', terrain: 'gravel' },
          { id: 4, name: 'MountainMajesty', gender: 'male', terrain: 'mountain' },
          { id: 5, name: 'gravellGoddess', gender: 'female', terrain: 'gravel' },
          { id: 6, name: 'RockRider', gender: 'neutral', terrain: 'mountain' },
          { id: 7, name: 'RoadRascal', gender: 'male', terrain: 'road' },
          { id: 8, name: 'DirtDuchess', gender: 'female', terrain: 'gravel' },
          { id: 9, name: 'AsphaltAce', gender: 'neutral', terrain: 'road' },
          { id: 10, name: 'TrailTitan', gender: 'male', terrain: 'mountain' },
          { id: 11, name: 'gravellGem', gender: 'female', terrain: 'gravel' },
          { id: 12, name: 'NeutralNavigator', gender: 'neutral', terrain: 'road' },
          { id: 13, name: 'SummitSeeker', gender: 'male', terrain: 'mountain' },
          { id: 14, name: 'gravelGazer', gender: 'female', terrain: 'gravel' },
          { id: 15, name: 'RoadRunner', gender: 'neutral', terrain: 'road' }
        ]
      },
      computed: {
        filteredBikes() {
          let filtered = this.bikes;
          if (this.selectedGender) {
            filtered = filtered.filter(bike => bike.gender === this.selectedGender);
          }
          if (this.selectedTerrain.length > 0) {
            filtered = filtered.filter(bike => this.selectedTerrain.includes(bike.terrain));
          }
          return filtered;
        }
      }
    });
  </script>
</body>
</html>
1 Like

Thanks so much James! I was nearly there then - I just needed to read into the array via the “includes”. I realised that I’d neglected to add a “value” attribute to the terrain checkboxes, so they were being treated as one selection rather than individual.

Good call on tidying up the if/else I had.

1 Like

Hi there,

Since my last post I’ve been working on a couple of the fields based on the feedback so far but I’ve hit another roadblock with the prices. What I would like to do is create a series of price ranges which the user can set to filter the bikes accordingly.

So far all I have are the prices being listed out and filtering single products as per the other filtering options. I don’t want to base the price ranges on any data in my “bikes” array at this point, just a simple range filter would be fine.

I came across a price filtering script which uses radio buttons (not the checkboxes I would like to allow for multiple selections) which I attempted to apply but I couldn’t find a way to link it to my data.

I’ve copied the filtering script into my code for reference but here is the original I found on Stack Overflow:

Any help would be much appreciated!

Well first you’ve gotta get the radio buttons to have a legitimate value…

I will talk in some general theory about how to filter things like this.
Let’s assume for the moment that you’re going to use the price points currently in your labels. I’m guessing you arent, but lets rock with it for an example.

 <=£10
 £10 - £100
 £100 - £500
 >=£500

So you’ve got 3 break points for prices (making 4 categories): 10, 100, 500.
Upper bound… who knows. You could have a million pound bike one day. But maybe not. So lets say the upper bound is 1000000.
But your lower bound… well, you’re not going to pay someone to take a bike from you, so our lower bound is 0.

0, 10, 100, 500, 1000000. Simple enough, let’s stick some brackets around that, and call it an array.
[0,10,100,500, 1000000]

How do I filter bikes if I get a value of… 100 (Which corresponds to the range 10-100 in your HTML)?

Well, I can find 100 in my array; indexOf. The value i’ve been given is the upper bound of the search.
I can find the lower bound of my search, because it’s the value at the upper bound’s index minus 1, which will return 10.
filter the bikes by checking to see if their price is between the upper bound and the lower bound inclusively.

So that’s how you filter on a single value. But you said you want to do checkboxes and do multiple spans. How do I do that?

foreach value checked, run a single-value filter. That will give you the bikes that match that value. Add those bikes to your ‘total filter’, and when you get done with checking, return the full set.

1 Like

Hmm agreed - I’m struggling to figure out how to get this from the new array you mention in your follow-up reply, as presumably I should get the index of the price and then get the previous index’s value as well but labelled somehow so that I can grab it later when it’s been selected.

I note from the example I provided that this also does the same…

HI all,

Ok so I’ve now got a new method to get the price from the clicked element called “selectPrice” which I’ve connected to the HTML via an @click.

The price checkboxes are generated based on my priceRanges array. However, I’m unsure how to connect the two so that a comparison can be done.

In normal JavaScript I’d just track the click on the checkbox, pass the value from one function to another and take it from there, but there doesn’t appear to be a way to do that with Vue?

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.