How to filter a data object? - javascript

Context: I receive from Elasticsearch the result of a search (example below) which I put into a Vue.js data object. I then list the data via <div v-for="result in results">{{result.name}}</div>.
var vm = new Vue({
el: "#root",
data: {
results: [{
'name': 'john',
'big': true
},
{
'name': 'jim',
'tall': true
},
{
'name': 'david'
}
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.js"></script>
<div id="root">
<div v-for="result in results">{{result.name}}</div>
</div>
I now would like to filter the results. To do so, I will have switches which will be bound via v-model.
Question: what is the correct way to handle filtering in Vue.js?
I would like to render (via a v-if, I guess) only elements from results which match a filter (say, big is checked - so only johnshould be visible), or a concatenation of filters (logical AND).
The part I have a hard time turning into Vue.js philosophy is "display the element if all active switches are present (value true) in that element).
Since I am sure that having a chain of v-ifs is not the right approach, I prefer to ask before jumping into that (and I would probably rather rerun a search with parameters than go this way - but I would prefer to avoid the search way).

Create a computed property which returns only the filtered results:
computed: {
filteredResults() {
return this.results.filter((result) => {
// your filter logic, something like this:
// return result.big || result.tall
});
}
}
And use it in the v-for instead:
<div v-for="result in filteredResults">{{result.name}}</div>

Related

For sibling communication between many identical components, how should I store the data in the lowest-common ancestor?

Background:
I'm a Python/Vue developer; I've been using Vue since 2016.
I have a client who runs a weight loss / meal planning business: clients pay her to prepare weekly single-page PDF menus that tell them (the clients) exactly what to eat for breakfast, lunch, and dinner of every day of the week. (image of an example menu)
Each meal is shown as a list of ingredients.
Right now she's preparing these menus in Excel, and she hired me to reproduce and extend the functionality of what she has in Excel, but in a Python/Vue app.
The app I'm building for her has many "pages" ("top-level" components) to allow her to add/modify/delete objects like clients, ingredients, and recipes (image), but the most complicated part of the UI is the component in which she can define the meals for every meal of every day of the week (image). That component is named WeeklyMenu.vue.
WeeklyMenu.vue itself contains seven DailyMenu.vue children, one for each day of the week (Monday, Tuesday, etc.). (image)
Each DailyMenu.vue component itself contains four Meal.vue components, one for each of four meal types: Breakfast, Lunch, Dinner, and Snacks. (image)
Important: At the moment, the DailyMenu.vue and Meal.vue components themselves contain their data rather than accessing it from the Vuex store.
For example, the list of ingredients for each meal is contained within the Meal.vue component as a mealIngredients variable within the component's data attribute. (image)
Side-note: This means that there are lots of HTTP requests being sent to the back-end when the page loads as all of the meals are requesting their own data, rather than a single request being sent via a Vuex action (for example). This seems like it can't be best practice.
The problem:
The problem is that she is now asking me to add features in which a change to the data in one subcomponent should update the data in a different subcomponent.
For example, she wants the app to work so that when she has the same recipe in several different Meals of the week, then a change to an ingredient in one of the meals will propagate to the other meals that have the same recipe. (image explanation)
My question:
What is the best practice for handling a situation like this? Should I move the ingredient data into the Vuex store or (in the same vein) the lowest-common-ancestor WeeklyMenu.vue component? If so, how exactly should it work? Should there be a separate variable for each meal? Or should I have an object that contains data for all of the different meals? If I use a single object, do I need to worry that a watcher on that object in the Meal.vue component would be triggering even when a change was made to a different meal's data?
If I store all the meal ingredients in separate variables, I would need to pass all of those to every meal (so every meal would need to receive every other meal's ingredients as separate props). So that doesn't seem like the right way to go.
If a user is making a particular change to a particular meal, how would I only have the other meals with the same name react?
Related links:
Communication between sibling components in VueJs 2.0
I'm looking into whether it would make sense to move the ingredient data up to the level of the WeeklyMenu.vue component as described in the "Lowest Common Ancestor" approach (here and here).
Simplified example of the situation I'm trying to handle:
Without Vuex: https://codepen.io/NathanWailes/pen/zYBGjME
Using Vuex: https://codepen.io/NathanWailes/pen/WNxWxWe
With everything working (including the state being kept in Vuex) except the propagation: https://codepen.io/NathanWailes/pen/KKMYNVZ
Yes, problem domain seems complex enough to more than justify use of Vuex. I would not go with keeping data in components and sharing by props - that doesn't scale well
Keep each Recipe as an object in single object recipes - you don't need to worry about watchers. If one particular Recipe object will change, Vue will re-render only components using same Recipe object (and if done properly you don't even need watchers for that)
Create a "weekly menu" object inside the store
In leaf nodes (Meals) of that object just use some kind of reference (by name or unique ID if you have one) into recipes. As a result multiple Meal.vue components on a menu will use same object in the store and update automatically
I ended up getting it working in a simple example in CodePen, which I'm going to use as a guide when trying to get it working on the actual site.
The summary of my findings with this solution is, "Vue will actually update when the nested entries of a Vuex state object are updated; you don't need to worry about it not detecting those changes. So it's OK to just keep all the data in a single big Vuex store object when you have many duplicate sibling components that need to react to each other."
Here's the CodePen: https://codepen.io/NathanWailes/pen/NWRNgNz
Screenshot
Summary of what the CodePen example does
The data used to populate the menu all lives in the Vuex store in a single weeklyMenu object, which has child objects to break up the data into the different days / meals.
The individual meals have computed properties with get and set functions so that it can both get changes from the store and also update the store.
The DailyMenu and WeeklyMenu components get their aggregate data by simply having computed properties that iterate over the Vuex weeklyMenu object, and it "just works".
I have same-named meals update to match each other by iterating over the meals in the Vuex mutation and looking for meals with the same "Ingredient Name".
The code
HTML
<html>
<body>
<div id='weekly-menu'></div>
<h3>Requirements:</h3>
<ul>
<li>Each row should have all the numbers in it summed and displayed ('total daily calories').</li>
<li>The week as a whole should have all the numbers summed and displayed ('total weekly calories').</li>
<li>If two or more input boxes have the same text, a change in one numerical input should propagate to the other same-named numerical inputs.</li>
<li>Ideally the data (ingredient names and calories) should be stored in one place (the top-level component or a Vuex store) to make it more straightforward to populate it from the database with a single HTTP call (which is not simulated in this example).</li>
</ul>
</body>
</html>
JavaScript
const store = new Vuex.Store(
{
state: {
weeklyMenu: {
Sunday: {
Breakfast: {
name: 'aaa',
calories: 1
},
Lunch: {
name: 'bbb',
calories: 2
},
},
Monday: {
Breakfast: {
name: 'ccc',
calories: 3
},
Lunch: {
name: 'ddd',
calories: 4
},
}
}
},
mutations: {
updateIngredientCalories (state, {dayOfTheWeekName, mealName, newCalorieValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['calories'] = newCalorieValue
const ingredientNameBeingUpdated = state.weeklyMenu[dayOfTheWeekName][mealName]['name']
for (const dayOfTheWeekName of Object.keys(state.weeklyMenu)) {
for (const mealName of Object.keys(state.weeklyMenu[dayOfTheWeekName])) {
const mealToCheck = state.weeklyMenu[dayOfTheWeekName][mealName]
const ingredientNameToCheck = mealToCheck['name']
if (ingredientNameToCheck === ingredientNameBeingUpdated) {
mealToCheck['calories'] = newCalorieValue
}
}
}
},
updateIngredientName (state, {dayOfTheWeekName, mealName, newValue}) {
state.weeklyMenu[dayOfTheWeekName][mealName]['name'] = newValue
}
}
}
)
var Meal = {
template: `
<td>
<h4>{{ mealName }}</h4>
Ingredient Name: <input v-model="ingredientName" /><br/>
Calories: <input v-model.number="ingredientCalories" />
</td>
`,
props: [
'dayOfTheWeekName',
'mealName'
],
computed: {
ingredientCalories: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['calories']
},
set (value) {
if (value === '' || value === undefined || value === null) {
value = 0
}
this.$store.commit('updateIngredientCalories', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newCalorieValue: value
})
}
},
ingredientName: {
get () {
return this.$store.state.weeklyMenu[this.dayOfTheWeekName][this.mealName]['name']
},
set (value) {
this.$store.commit('updateIngredientName', {
dayOfTheWeekName: this.dayOfTheWeekName,
mealName: this.mealName,
newValue: value
})
}
}
}
};
var DailyMenu = {
template: `
<tr>
<td>
<h4>{{ dayOfTheWeekName }}</h4>
Total Daily Calories: {{ totalDailyCalories }}
</td>
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Breakfast" />
<meal :day-of-the-week-name="dayOfTheWeekName" meal-name="Lunch" />
</tr>
`,
props: [
'dayOfTheWeekName'
],
data: function () {
return {
}
},
components: {
meal: Meal
},
computed: {
totalDailyCalories () {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[this.dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[this.dayOfTheWeekName][mealName]['calories']
}
return totalDailyCalories
}
}
};
var app = new Vue({
el: '#weekly-menu',
template: `<div id="weekly-menu" class="container">
<div class="jumbotron">
<h2>Weekly Menu</h2>
Total Weekly Calories: {{ totalWeeklyCalories }}
<table class="table">
<tbody>
<daily_menu day-of-the-week-name="Sunday" />
<daily_menu day-of-the-week-name="Monday" />
</tbody>
</table>
</div>
</div>
`,
data: function () {
return {
}
},
computed: {
totalWeeklyCalories () {
let totalWeeklyCalories = 0
for (const dayOfTheWeekName of Object.keys(this.$store.state.weeklyMenu)) {
let totalDailyCalories = 0
for (const mealName of Object.keys(this.$store.state.weeklyMenu[dayOfTheWeekName])) {
totalDailyCalories += this.$store.state.weeklyMenu[dayOfTheWeekName][mealName]['calories']
}
totalWeeklyCalories += totalDailyCalories
}
return totalWeeklyCalories
}
},
components: {
daily_menu: DailyMenu
},
store: store
});

Forcing v-validate to update rules (with Vue)

I'm using v-validate with Vue. I'm trying to figure out how to force v-validate to update rules. For example, I have something like this:
<template>
<div v-for="field in fields">
<input :name="field.name" v-validate="field.rules">
</div>
</template>
<script>
export default {
data() {
fields: [
{
name: "city",
rules: {
included: []
}
}
]
}
}
</script>
As you can see, my "included" array is empty on page load. I get the array from an AJAX request, and then I update my data:
this.fields[0].rules.included = cities
But v-validate doesn't seem to acknowledge the newly-added array. It only works if I hardcode the cities into my data. How can I force v-validate to respond to the updated rules?
Vue.js is unable to track updates on nested reference types.
Try:
let fields = [...this.fields]
fields[0].rules = cities
this.fields = fields
Use Vue.set to track changes : https://v2.vuejs.org/v2/guide/reactivity.html
Vue.set(this.fields[0], 'rules', cities);

How to loop through a list of components in VueJS

I may have gone about this completely the wrong way from the beginning so all advise is welcomed.
I am trying to create a basic page with inputs on the right and hints for the inputs on the left and when you focus on the inputs the appropriate hint is highlighted on the left.
There is a JSFiddle here: https://jsfiddle.net/eywraw8t/210693/
This will not work as I do not know how to find the appropriate hint to highlight (and set isHighlighted to false on all the other hints).
I managed to get a working example by adding a highlighted prop on the field object and not using a hint component. However in reality the fields data will come from the database so it won't have a highlighted parameter so a hint component seemed more sensible.
To put my question in simple terms: How can I find the relevant hint component when focused on an input?
JS Fiddle showing functionality without a component: https://jsfiddle.net/as2vxy79/
Broken JS Fiddle trying to use a component: https://jsfiddle.net/eywraw8t/210693/
Here is the JS outside JS Fiddle:
Vue.component('hint', {
template: `
<div class="hint" :class="{'highlight-hint': isHighlighted }">
<slot></slot>
</div>
`,
data() {
return {
isHighlighted: false
}
}
});
new Vue({
el: "#app",
data: {
fields: [
{
'id': 'name',
'hint': 'Put the name here'
},
{
'id': 'description',
'hint': 'Put the description here'
},
{
'id': 'more',
'hint': 'Put more here'
}
]
},
methods: {
onFocus(focusedField) {
// Somehow loop through the hints
// I am aware there is no "hints" property this is an example
this.hints.forEach(function(field) {
field.isHighlighted = (focusedField == field.id)
})
}
}
})
Short answer: you don't need a direct reference to the components displaying the hint because you can solve this problem using reactivity.
Only thing you need to do is add a data field which stores the field id you want to highlight and then conditionally add a class to the hint you want to highlight based on the selected id (or render the components conditionally).
new Vue({
el: "#app",
data: {
highlightedFieldId: '',
fields: [
{
'id': 'name',
'hint': 'Put the name here' },
{
'id': 'description',
'hint': 'Put the description here' },
{
'id': 'more',
'hint': 'Put more here' }
]
},
methods: {
onFocus(focusedFieldId) {
this.highlightedFieldId = focusedFieldId;
}
}
})
Here's an update to your Fiddle.
NOTES:
If you really need direct references you can use the ref directive (this also works in a list of components generated by v-for).
You should probably think about using tooltips for the hints. E.g. using Element UI's Tooltip.
UPDATE:
So, here's a solution using the ref directive to obtain a reference to the highlighted hint component. You can see that I used the field id in :ref but you still get an array from this.$refs[focusedFieldId] since there is a surrounding v-for. Other than that, it's pretty simple.
I also updated the hint component to accept the is-highlighted prop so it can change its class on its own (you previously used a data property for this which does not result in a component update).

Vue.js 2 - retrieve form elements from server and render CRUD

I'm Vue.js newbie and my task is:
make an ajax call (GET) to server, using RESTful API (Laravel on background)
retrieve a (JSON) list of Form CRUD items in array (like checkbox, input text, textarea...) with their properties (value, checked, custom classes...)
render CRUD form with these form items maybe using Vue's loop
I'm wondering if it could be rendered using components somehow. But I don't know the correct way.
Frankly, I exactly don't know how to solve this problem with Vue.js - rendering items from array and each item has it's own markup and properties (checkbox has it's own, textbox, select, textarea...).
I'm building a web application based on CRUD operations and I'm trying to write universal components. The easiest way is to do a special component with hard-written sub-components for each subpage, but I don't like this way if not needed.
Thank you!
EDIT: I don't have much code yet, but this is where I am...
<script>
// ./components/CrutList.vue
export default {
mounted() {},
data() {
return {
items: []
}
},
props: ['resource'],
methods: {
getItems() {
var resource = this.$resource('api/'+this.resource+'{/id}');
resource.get({}).then(function(items){
if(items.body.status == 'success'){
this.items = items.body.items;
}
}).bind(this);
},
deleteItem(item) {
// perform CRUD operation DELETE
alert('delete action');
}
}
}
</script>
My idea is using CrudList component to CRUD listing...
<crud-list resource="orders">
In laravel I do something like this:
return response()->json([
'status' => 'success',
'items' => [
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'label' => "Checkbox č.1",
'name' => 'checkbox1'
]
],
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'label' => "Checkbox č.2",
'name' => 'checkbox2'
]
],
[
'itemComponent' => 'checkbox',
'props' => [
'checked' => true,
'name' => 'checkbox3'
]
],
],
]);
...it's very simplified, but it's just example of what I'm doing.
Now the problem is:
take the 'itemComponent' part from the returned array item (this is in a loop),
if it's a checkbox, take (for example) Checkbox.vue component, fill it with properties ('props' part of the array item)
I read about slots, but it's not what I'm looking for. Is there something I can use for dynamic components?
Check out this jsFiddle working example for dynamic forms:
https://jsfiddle.net/mani04/kr8w4n73/1/
You can do it easily by using a lot of v-ifs for each and every form element type you might get from server. It is a bit cumbersome but I can't find any other way.
In the above example, I have the form structure as follows:
var formItems = [{
input_type: "text",
input_label: "Login",
values: {
value: "your_name#example.com"
}
},
{...},
{...}];
Once you have that data, then it is a matter of iterating through formItems, checking input_type and activating the relevant form control.
Here is how my dynamic form template looks like, for the above input:
<div v-for="formItem in formValues">
<div v-if="formItem.input_type == 'text'">
<input type="text" v-model="formItem.values.value">
</div>
<div v-if="formItem.input_type == 'password'">
<input type="password" v-model="formItem.values.value">
</div>
<div v-if="formItem.input_type == 'checkbox'">
<input type="checkbox" v-model="formItem.values.checked">
{{formItem.values.label}}
</div>
</div>
My jsFiddle example uses form-horizontal from bootstrap, and I am also able to display the labels well. If I put that in the example above, it will get cluttered and will not let you see how it works.
Hope it helps! You can change the formItems data structure to meet your needs, and modify the template accordingly.

How to fetch data from collections when the route/template consist on more than collection?

Example:
routes.js:
this.route("chapterPage", {
path: "/books/:bookId/chapters/:_id",
data: function() {
var chapter = Chapters.findOne(this.params._id);
var book = Books.findOne(this.params.bookId);
var chapters = Chapters.find({
bookId: this.params.bookId
}, {
sort: {
position: 1
}
});
return {
chapter: chapter,
book: book,
chapters: chapters
};
}
});
As you can see this template/route has two collections Book and Chapter. Previously, I used to call the collections individually like this:
chapter_form.js:
Template.chapterForm.events({
"input #input-content": function() {
var currentChapter = Session.get("currentChapter");
Chapters.update(currentChapter, {
$set: {
content: $("#input-content").html();
}
});
}
});
But now in my new route/template I can't do that since it isn't based on any collection:
chapter_page.js:
Template.chapterPage.events({
"input #input-content": function() {
console.log(chapter._id); // this returns is not defined
console.log(this._id); // this one too
}
});
How to get around this?
EDIT:
I also tried calling the chapter_form.html template:
<template name="chapterPage">
{{> chapterForm}}
</template>
But it doesn't display and shows stuff like: Cannot read property 'content' of undefined so it isn't recognizing the template.
There are two problems in your code.
First in the data function of the chapterPage route, you do not return the object containing your data.
// no return here in your question, need to do :
return {
chapter: chapter,
book: book,
chapters: chapters
};
Then in your event handler, you can access the data context using this, so the correct syntax to access the chapter or book id is this.chapter._id or this.book._id.
EDIT :
Inside templates route helpers and event handlers, this refers to the current data context assigned to the template.
There are several ways to assign a data context to a template.
You can use attribute="value" syntax along with template inclusion syntax.
{{> myTemplate param1="value1" param2="value2"}}
Template.myTemplate.helpers({
paramsJoined:function(){
return [this.param1,this.param2].join(",");
}
});
You may also use a helper value coming from the parent template data context :
<template name="parent">
{{> myTemplate someHelper}}
</template>
Template.parent.helpers({
someHelper:function(){
return {
param1:"value1",
param2:"value2"
};
}
});
If you don't specify a data context when using the template inclusion syntax, it is assumed to be inherited from the parent data context.
You can also use {{UI.dynamic}} (http://docs.meteor.com/#ui_dynamic) to specify a dynamic template name along with a dynamic data context.
{{> UI.dynamic template=Router.template data=Router.data}}
This is this kind of approach that iron:router is using to set dynamically the route data context of the route template (implementation is slightly more complex though).
Meteor provides utilities to access current data contexts as well as parent data contexts, which can be useful :
http://docs.meteor.com/#template_currentdata
http://docs.meteor.com/#template_parentdata

Categories

Resources