Vue.js add property to bound object - javascript

I have an list like this one
<div class="list-group entity-list">
<a v-for="entity in master.data"
:class="['entity list-group-item', entity.selected ? 'selected' : '']"
#click="itemselect(entity)"
>
#{{entity.name}}
</a>
</div>
The list is displayed as expected. The Bound entity is added as an prop to an outer compontent like this.
<my-compontent :entityname="customers" :master="{{json_encode($customers->toArray())}}" inline-template>
As you can see it should be a list of customers. To indicate if a customer is selected i want to set an "selected" attribute on click. However it does not seem to work if the default bound object does not have a select property.
methods: {
itemselect: function(item) {
this.master.data.forEach(function(el) {
el.selected = false;
});
item.selected = true;
}
}
The shown method does not seem to work / trigger a change

If you want to modify data passed in by a prop then you need to copy it, vue will throw out a warning if you try to directly modify a prop. I would probably handle this by doing something like:
new Vue({
el: '#app',
created() {
// Assume this is passed in by a prop
var myData = {master: {
data: [{
name: 'foo'
}, {
name: 'bar'
}, {
name: 'baz'
}]
}};
// Copy data from prop
myData.master.data.forEach(el => {
var obj = {
name: el.name,
selected: false
};
this.master.data.push(obj);
});
},
methods: {
itemselect(item) {
this.master.data.forEach(el => {
// if this element is selected set to true
el['selected'] = (el.name == item.name);
});
}
},
data() {
return {
master: {
data: []
}
}
}
});
I just mocked the actual data coming in, but obviously this would add this to a prop and pass it to your component as usual. Here's the jsFiddle: https://jsfiddle.net/xswtfkaf/

Instead of altering your master data, track selection in model, using $index assuming you don't reorder your master.data
<div class="list-group entity-list">
<a v-for="entity in master.data"
:class="eClass[$index]"
#click="itemselect($index)"
>
#{{entity.name}}
</a>
</div>
methods: {
itemselect: function(idx) {
if ( 0 > this.selected.indexOf(idx) ) {
this.selected.push(idx)
}
}
}
computed : {
eClass : function () {
var r=[];
for (var i= 0;c=this.master.data[i];++i) {
r[i] = {'selected': -1 < this.selected.indexOf(i),'entity list-group-item':true};
}
return r;
}
}
if entity has am unique id
<div class="list-group entity-list">
<a v-for="entity in master.data"
:class="eClass[entity.id]"
#click="itemselect(entity.id)"
>
#{{entity.name}}
</a>
</div>
methods: {
itemselect: function(idx) {
if ( 0 > this.selected.indexOf(idx) ) {
this.selected.push(idx)
}
}
}
computed : {
eClass : function () {
var r={};
for (var i= 0;c=this.master.data[i];++i) {
r[c.id] = {'selected': -1 < this.selected.indexOf(c.id),'entity list-group-item':true};
}
return r;
}
}

Related

bootstrap-vue b-table: keep expanded rows expanded on table reload

the expand/collapse part of this works just fine.
Right now I am using javascript startInterval() to reload the table every 2 seconds. Eventually this will be moving to web sockets.
In general, as part of the table load/reload, the system checks to see if it should display the icon " ^ " or " v " in the details column by checking row.detailsShowing, this works fine.
getChevron(row, index) {
if (row.detailsShowing == true) {
return "chevronDown";
}
return "chevronUp";
}
When the user selects the " ^ " icon in the relationship column, #click=row.toggleDetails gets called to expand the row and then the function v-on:click="toggleRow(row)" is called to keep track of which row the user selected. This uses a server side system generated guid to track.
Within 2 seconds the table will reload and the row collapses. On load/reload, in the first column it loads, relationship, I call a function checkChild(row), to check the row guid against my locally stored array, to determine if this is a row that should be expanded on load.
<template #cell(relationship)="row"> {{checkChild(row)}} <\template>
if the row guid matches one in the array I try setting
checkChild(row){
var idx = this.showRows.indexOf( row.item.id);
if(idx > -1){
row.item.detailsShowing = true;
row.rowSelected = true;
row.detailsShowing == true
row._showDetails = true;
}
}
and I am able to see that i have found match, but none of those variables set to true keeps the expanded row open, the row always collapses on reload
anyone have any ideas as to how i can make the row(s) stay open on table reload?
The issue with your code is because of a Vue 2 caveat. Adding properties to objects after they've been added to data will not be reactive. To get around this you have to utilize Vue.set.
You can read more about that here.
However, calling a function like you are doing in the template seems like bad practice.
You should instead do it after fetching your data, or use something like a computed property to do your mapping.
Here's two simplified examples.
Mapping after API call
{
data() {
return {
items: [],
showRows: []
}
},
methods: {
async fetchData() {
const { data } = await axios.get('https://example.api')
foreach(item of data) {
const isRowExpanded = this.showRows.includes(item.id);
item._showDetails = isRowExpanded;
}
this.items = data;
}
}
}
Using a computed
{
computed: {
// Use `computedItems` in `<b-table :items="computedItems">`
computedItems() {
const { items, showRows } = this;
return items.map(item => ({
...item,
_showDetails: .showRows.includes(item.id)
}))
}
},
data() {
return {
items: [],
showRows: []
}
},
methods: {
async fetchData() {
const { data } = await axios.get('https://example.api')
this.items = data;
}
}
}
For a more complete example, check the snippet below.
const {
name,
datatype,
image
} = faker;
const getUser = () => ({
uuid: datatype.uuid(),
personal_info: {
first_name: name.firstName(),
last_name: name.lastName(),
gender: name.gender(),
age: Math.ceil(Math.random() * 75) + 15
},
avatar: image.avatar()
});
const users = new Array(10).fill().map(getUser);
new Vue({
el: "#app",
computed: {
computed_users() {
const {
expanded_rows,
users
} = this;
return users.map((user) => ({
...user,
_showDetails: expanded_rows[user.uuid]
}));
},
total_rows() {
const {
computed_users
} = this;
return computed_users.length;
}
},
created() {
this.users = users;
setInterval(() => {
users.push(getUser());
this.users = [...users];
}, 5000);
},
data() {
return {
per_page: 5,
current_page: 1,
users: [],
fields: [{
key: "avatar",
class: "text-center"
},
{
key: "name",
thClass: "text-center"
},
{
key: "personal_info.gender",
label: "Gender",
thClass: "text-center"
},
{
key: "personal_info.age",
label: "Age",
class: "text-center"
}
],
expanded_rows: {}
};
},
methods: {
onRowClicked(item) {
const {
expanded_rows
} = this;
const {
uuid
} = item;
this.$set(expanded_rows, uuid, !expanded_rows[uuid]);
}
}
});
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.js"></script>
<script src="https://unpkg.com/faker#5.5.3/dist/faker.min.js"></script>
<div id="app" class="p-3">
<b-pagination v-model="current_page" :per-page="per_page" :total-rows="total_rows">
</b-pagination>
<h4>Table is refresh with a new item every 5 seconds.</h4>
<h6>Click on a row to expand the row</h6>
<b-table :items="computed_users" :fields="fields" bordered hover striped :current-page="current_page" :per-page="per_page" #row-clicked="onRowClicked">
<template #cell(avatar)="{ value }">
<b-avatar :src="value"></b-avatar>
</template>
<template #cell(name)="{ item: { personal_info: { first_name, last_name } }}">
{{ first_name }} {{ last_name }}
</template>
<template #row-details="{ item }">
<pre>{{ item }}</pre>
</template>
</b-table>
</div>

How to validate child component using vue ant design?

I have this component DynamicSelect (child compoenent) and I'm using it in an another component (parent) but when I try to validate my childe component, it deliver always the value as null so the validation is always false
DynamicSelect Component:
<template>
<a-select
:showSearch="true"
:placeholder=placeholder
:value="selectedValue"
#search="searchRegex($event)"
#change="$emit('changed-item', setChangedItem($event))"
#select="$emit('selected-item', setSelectedItem($event))"
:filterOption="filterOption"
>
<a-select-option
v-for="(item,idx) in dropdownData"
:value="idx"
:key="idx"
>{{item.text}}</a-select-option>
</a-select>
</template>
<script>
export default {
name: "DynamicSelect",
data(){
return{
dropdownData: [],
copyDropdownData:[],
selectedValue: undefined
}
},
props:{
//Input data collection
dataSrc:Array,
//Placeholder for input field
placeholder: String,
//if true the dropdown will be automatically cleared after element selected
resetAfterSelect: false,
// List of id to filter the dropdown list
lookFor:Array,
// Data to display in the dropdown if not set, lookFor list will be displayed
displayedValues: Array,
//Default Value
defaultValues:String,
},
beforeMount(){
this.checkDefaultVariable();
},
watch:{
dataSrc:function(newVar,oldVar) { // watch it
this.checkDefaultVariable()
}
},
methods:{
//Search for search term in the data collection 'lookFor' elements to set the dropdown list
async searchRegex(term){
if(term.length>2) {
let searchTerm = new RegExp(term.toUpperCase());
this.dropdownData = await this.filterData(searchTerm);
this.copyDropdownData = JSON.parse(JSON.stringify(this.dropdownData));
}
else
{
this.dropdownData = [];
this.copyDropdownData = [];
}
},
filterData(searchTerm){
return this.dataSrc.filter(x => {
let filtered= [];
for (let i=0; i<this.lookFor.length;i++){
if(x[this.lookFor[i]])
{
if(searchTerm.test(x[this.lookFor[i]].toUpperCase()))
{
let text = '';
if(this.displayedValues !== undefined)
{
for (let k=0; k<this.displayedValues.length;k++)
{
text += x[this.displayedValues[k]];
if(k < this.displayedValues.length-1)
text += ', '
}
}
else {
for (let k=0; k<this.lookFor.length;k++)
{
text += x[this.lookFor[k]];
if(k < this.lookFor.length-1)
text += ', '
}
}
x.text = text;
filtered.push(x);
}
}
}
return filtered.length>0
});
},
// just a logger
logger(event){
console.log(event);
},
async checkDefaultVariable(){
if (this.defaultValues !== '' && this.defaultValues !== undefined && this.dataSrc.length>0 ){
// console.log('DATA',this.dataSrc);
await this.searchRegex(this.defaultValues);
let selected = await this.setSelectedItem(0);
this.$emit('selected-item', selected)
}
},
// return the selected Item as an Object
setSelectedItem(id){
// console.log('ON SELECT');
let temp = JSON.parse(JSON.stringify(this.dropdownData[id]));
delete temp.text;
if(this.resetAfterSelect)
{
this.dropdownData = [];
this.selectedValue = undefined;
}
else {
this.selectedValue = id;
}
return temp
},
setChangedItem(id){
let temp = JSON.parse(JSON.stringify(this.copyDropdownData[id]));
delete temp.text;
if(this.resetAfterSelect)
{
this.copyDropdownData = [];
this.selectedValue = undefined;
}
else {
this.selectedValue = id;
}
return temp
},
// search in the dropdown list
filterOption(input, option) {
let searchTerm = new RegExp(input.toUpperCase());
if(searchTerm.test(this.dropdownData[option.key].text.toUpperCase()))
return true;
else {
for(let i=0;i<this.lookFor.length;i++){
if(searchTerm.test(this.dropdownData[option.key][this.lookFor[i]].toUpperCase()))
return true;
else if(i >= this.lookFor.length)
return false;
}
}
}
}
}
</script>
parent component:
<template>
<dynamic-select
:dataSrc="users"
placeholder="Lastname, Firstname"
#selected-item="onSelectUser($event)"
#changed-item="onSelectUser($event)"
:lookFor="['lastname','firstname']"
v-decorator="['contact', {valuePropName:'selectedValue',
rules: [{ required: true,
validator: userExists,
message: 'Error'}]}]"
>
</dynamic-select>
</template>
<script>
.
.
.
methods: {
userExists(rule, value, callback) {
console.log('VALUE', value); //always undefined
console.log('RULES',rule);
console.log('CALLBACK',callback)
return value !== null && value !== undefined && value.length > 2;
},
onSelectUser(user) {
console.log("user: " , user); // set with the selected value
}
},
.
.
.
</script>
I expect that the child component returns the selected value like when emitting an event, I also tried with models but it hasn't helped
thanks :)
You can easily communicate between components
Vue.config.debug = true;
// Parent
let App = new Vue({
el: "#the-parent",
data(){
return{ msg: "Nothing.." };
},
methods:{
receivedFromChild(request){
this.msg = request;
}
},
// Children
components: {
'children': {
template: `
<div><button #click="request">Send to parent!</button>` + `<input type="text" v-model="text"></div>`,
props: [ 'childrenRequest' ],
data() {
return {
text: 'this is value'
}
},
methods: {
request(){
console.log('work!');
this.$emit('received', this.text);
}
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="the-parent">
<h3>The children want me to say: {{ msg }}</h3>
<children #received="receivedFromChild"></children>
</div>

Vue push unique values to a list

var vue_app = new Vue({
el: '#id1',
data: {
v1:[],
},
methods:{
pushUnique: function() {
this.v1.push({'id':1,'name':'josh'});
this.v1.push({'id':1,'name':'josh'}); //this should not work.
},
},
});
In above code the second push should not execute. I would like to keep id unique. How can this be done in Vue.
THanks
I would move to storing data in an object (keyed by id) and use a computed property to produce your v1 array. For example
data: {
v1obj: {}
},
computed: {
v1 () {
return Object.keys(this.v1obj).map(id => ({ id, name: this.v1obj[id] }))
}
}
Then you can use methods like Object.prototype.hasOwnProperty() to check for existing keys...
methods: {
pushUnique () {
let id = 1
let name = 'josh'
if (!this.v1obj.hasOwnProperty(id)) {
this.v1obj[id] = name
}
}
}

vue.js - recursive components doesn't get updated when using nested array from raw json

I am trying to create a tree with an add function using computed data. I used the tree-view example in vuejs official homepage and combined it with the computed function that I created but found no luck in implementing it. I've been trying to solve this for 4 days already and still no luck so I am here looking for help.
When you click the "+" in the end of the list it will trigger a call to the addChild function and it will successfully append the data. The data gets appended but the recursive component is not reactive.
https://jsfiddle.net/znedj1ao/9/
var data2 = [{
"id": 1,
"name":"games",
"parentid": null
},
{
"id": 2,
"name": "movies",
"parentid": null
},
{
"name": "iron-man",
"id": 3,
"parentid": 2
},
{
"id": 4,
"name": "iron-woman",
"parentid": 3
}
]
// define the item component
Vue.component('item', {
template: '#item-template',
props: {
model: Object
},
data: function () {
return {
open: false
}
},
computed: {
isFolder: function () {
return this.model.children &&
this.model.children.length
}
},
methods: {
toggle: function () {
if (this.isFolder) {
this.open = !this.open
}
},
changeType: function () {
if (!this.isFolder) {
Vue.set(this.model, 'children', [])
this.addChild()
this.open = true
}
},
addChild: function () {
this.model.children.push({
name: 'My Tres',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
})
}
}
})
// boot up the demo
var demo = new Vue({
el: '#demo',
data: {
treeData2: data2
},
computed: {
nestedInformation: function () {
var a= this.nestInformation(data2);
return a;
}
},
methods:
{
nestInformation: function(arr, parent){
var out = []
for(var i in arr) {
if(arr[i].parentid == parent) {
var children = this.nestInformation(arr, arr[i].id)
if(children.length) {
arr[i].children = children
}
out.push(arr[i])
}
}
return out
}
}
})
<!-- item template -->
<script type="text/x-template" id="item-template">
<li>
<div
:class="{bold: isFolder}"
#click="toggle"
#dblclick="changeType">
{{model.name}}
<span v-if="isFolder">[{{open ? '-' : '+'}}]</span>
</div>
<ul v-show="open" v-if="isFolder">
<item
class="item"
v-for="model in model.children"
:model="model">
</item>
<li class="add" #click="addChild">+</li>
</ul>
</li>
</script>
<p>(You can double click on an item to turn it into a folder.)</p>
<!-- the demo root element -->
<ul id="demo">
<item
class="item"
:model="nestedInformation[1]">
</item>
</ul>
The Vue.js documentation that Abdullah Khan linked to in a comment above says:
Again due to limitations of modern JavaScript, Vue cannot detect property addition or deletion.
However, property addition is exactly what you are doing in your nestInformation method:
if(children.length) {
arr[i].children = children
}
The result is that the children property of each object is not reactive, so when this Array is pushed to in addChild, no re-render is triggered in the UI.
The solution will be to use Vue.set when creating the children Arrays so that they will be reactive properties. The relevant code in the nestInformation method must be updated to the following:
if (children.length) {
Vue.set(arr[i], 'children', children);
}
I have forked and modified your fiddle for reference.

While passing data from parent to child, it passes but view doesn't get updated

I have a a root and a child component. I am trying to pass data using the props from parent to child.
<div id="root">
<my-component v-bind:account-types='accountTypes'>
</my-component>
</div>
Vue.component('my-component', {
props: ['accountTypes'],
data() {
return { accounts: Object.assign({},this.accountTypes) }
},
template: `
<div v-for="x in accounts"></div> // I kept it short for this line. It returns 0
`
}
Through Vue debugger extension, I can see my-component has the accountTypes but id doesn't show.
The object structure is like this:
var obj = {
1 : [ "a", "z", "k", "m" ]
2 : [ "a", "b", "c", "d" ]
}
And in my main app,
var app = new Vue({
el: '#root',
data: {
accountTypes : {},
},
methods : {
// selecting & pushing
accountTypeSelected(clientIndex, formName, action) {
if (action == 'add') {
this.pushValue(clientIndex, formName)
} else {
this.removeFromArray(clientIndex, formName)
}
},
// And these are what I use for push and delete:
pushValue(key, value) {
var obj = this.accountTypes
if (obj.hasOwnProperty(key)) {
var idx = $.inArray(value, obj[key]);
if (idx == -1) {
obj[key].push(value);
}
} else {
obj[key] = [value];
}
},
removeFromArray(key, val) {
var idx = $.inArray(val, this.accountTypes[key]);
if (idx != -1) {
this.accountTypes[key].splice(idx, 1);
}
}
}
You are initializing accounts from accountTypes, but initialization is a one-time thing. Updates to accountTypes will not re-initialize accounts.
If you want the child to stay up-to-date with the parent, then you should get rid of accounts and just use accountTypes.

Categories

Resources