petite-vue directive where ctx.get() is array is not reactive - javascript

I need to create a custom 'chart' directive where the directive expression is an 'array'. When I update the array, no reactivity occurs. Is this possible in petite-vue? I see some caveats in Vue proper and using Vue.set() but with petite-vue I don't have access to that.
Javascript
class KatApp {
state;
constructor() {
this.state = PetiteVue.reactive({ chartData: [] });
const vue = PetiteVue.createApp(this.state);
vue.directive("ka-chart", ctx => {
let cnt = 1;
ctx.effect(() => {
ctx.el.innerHTML = "Rebuild chart: " + cnt;
cnt++;
});
});
vue.mount(".myApp");
}
calculate() {
// This is method that would be called when UI button is clicked...
this.state.chartData = [ { "#id": "1", value: 1 }, { "#id": "2", value: 2 } ];
}
}
Markup
<div v-scope class="myApp">
<div v-ka-chart="{ data: chartData }"></div>
</div>
When application.calculate() is called and the chartData is updated, my ctx.effect code does not fire.

Related

Vue.js - watch particular properties of the object and load data on change

I have Vue component with prop named product, it is an object with a bunch of properties. And it changes often.
export default {
props: {
product: {
type: Object,
default: () => {},
},
},
watch: {
'product.p1'() {
this.loadData()
},
'product.p2'() {
this.loadData()
},
},
methods: {
loadData() {
doApiRequest(this.product.p1, this.product.p2)
}
},
}
The component should load new data when only properties p1 and p2 of product are changed.
The one approach is to watch the whole product and load data when it is changed. But it produces unnecessary requests because p1 and p2 may not have changed.
Another idea is to watch product.p1 and product.p2, and call the same function to load data in each watcher.
But it may happen that both p1 and p2 changed in the new version of the product, it would trigger 2 calls.
Will it be a good solution to use a debounced function for data load?
Or rather use single watcher for the whole product and compare new p1 and p2 stringified with their old stringified versions to determine if data loading should be triggered?
There are several approaches to this, each with pros and cons.
One simple approach I do is to use a watch function that accesses each of the properties you want to watch and then returns a new empty object. Vue knows product.p1 and product.p2 were accessed in the watch function, so it will re-execute it any time either of those properties change. Then, by returning a new empty object instance from the watch function, Vue will trigger the watch handler because the watch function returned a new value (and thus what is being watched "changed").
created() {
this.$watch(() => {
// Touch the properties we want to watch
this.product.p1;
this.product.p2;
// Return a new value so Vue calls the handler when
// this function is re-executed
return {};
}, () => {
// p1 or p2 changed
})
}
Pros:
You don't have to stringify anything.
You don't have to debounce the watch handler function.
Cons:
You can't track the previous values of p1 and p2.
Take care if this.product could ever be null/undefined.
It will always trigger when p1 or p2 are changed; even if p1 and p2 are set back to their previous values before the next micro task (i.e. $nextTick()); but this is unlikely to be a problem in most cases.
You need to use this.$watch(). If you want to use the watch option instead then you need to watch a computed property.
Some of these cons apply to other approaches anyway.
A more compact version would be:
this.$watch(
() => (this.product.p1, this.product.p2, {}),
() => {
// changed
}
})
As some of other developers said you can use computed properties to monitor the changing of product.p1 or product.p2 or both of them and then calling loadData() method only once in each case. Here is the code of a hypothetical product.vue component:
<template>
<div>
this is product compo
</div>
</template>
<script>
export default {
name: "product",
watch: {
p1p2: function(newVal, oldVal) {
this.loadData();
}
},
props: {
productProp: {
type: Object,
default: () => {},
},
},
computed: {
p1p2: function() {
return this.productProp.p1 + this.productProp.p2;
}
},
methods: {
loadData() {
console.log("load data method");
}
},
}
</script>
I renamed the prop that it received to productProp and watched for a computed property called p1p2 in that. I supposed that the values of data are in String format (but if they are not you could convert them). Actually p1p2 is the concatenation of productProp.p1 and productProp.p2. So changing one or both of them could fire the loadData() method. Here is the code of a parent component that passes data to product.vue:
<template>
<section>
<product :productProp = "dataObj"></product>
<div class="d-flex justify-space-between mt-4">
<v-btn #click="changeP1()">change p1</v-btn>
<v-btn #click="changeP2()">change p2</v-btn>
<v-btn #click="changeBoth()">change both</v-btn>
<v-btn #click="changeOthers()">change others</v-btn>
</div>
</section>
</template>
<script>
import product from "../components/product";
export default {
name: 'parentCompo',
data () {
return {
dataObj: {
p1: "name1",
p2: "name2",
p3: "name3",
p4: "name4"
}
}
},
components: {
product
},
methods: {
changeP1: function() {
if (this.dataObj.p1 == "name1") {
this.dataObj.p1 = "product1"
} else {
this.dataObj.p1 = "name1"
}
},
changeP2: function() {
if (this.dataObj.p2 == "name2") {
this.dataObj.p2 = "product2"
} else {
this.dataObj.p2 = "name2"
}
},
changeBoth: function() {
if (this.dataObj.p2 == "name2") {
this.dataObj.p2 = "product2"
} else {
this.dataObj.p2 = "name2"
}
if (this.dataObj.p1 == "name1") {
this.dataObj.p1 = "product1"
} else {
this.dataObj.p1 = "name1"
}
},
changeOthers: function() {
if (this.dataObj.p3 == "name3") {
this.dataObj.p3 = "product3"
} else {
this.dataObj.p3 = "name3"
}
}
},
}
</script>
You can test the change buttons to see that by changing dataObj.p1 or dataObj.p2 or both of them the loadData() method only called once and by changing others it is not called.
for you do ontouch event in vuejs while using it with your HTML inline
and you have an object model that house you other variable and you need to validate the any of the variable you will need to put them in watcher and use qoute ('') to tell vuejs that this is from a the model.email
hope this is useful
data() {
return {
model: {},
error_message: [],
}
},
watch: {
'model.EmailAddress'(value) {
// binding this to the data value in the email input
this.model.EmailAddress = value;
this.validateEmail(value);
},
},
methods: {
validateEmail(value) {
if (/^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(value)) {
this.error_message['EmailAddress'] = '';
} else {
this.error_message['EmailAddress'] = 'Invalid Email Address';
}
}
},

Vue JS - dynamically created components rendered with innerHTML cannot be bound to events

I've just recently started to dive in into Vue JS - loved it so far.
I'm facing an issue now where I'm trying to create a (non-trivial) table (using vue-good-table plugin) in which each cell is a component by it's own.
Having read the documentation of the plugin, it's being mentioned that it's possible to create an HTML column types where you can just use, well, a raw HTML (I guess):
https://xaksis.github.io/vue-good-table/guide/configuration/column-options.html#html
To simplify things, here's what I have - a Vue component (called Dashboard2.vue) that holds the table and the child component called Test.vue
I'm creating the Test components dynamically per each relevant cell and assigning it to the relevant row cell.
since I've defined the columns to be HTML types, I'm using the innerHTML property to extract the raw HTML out of the Vue component. (following this article https://css-tricks.com/creating-vue-js-component-instances-programmatically/)
It all goes very well and the dashboard looks exactly how I wanted it to be, but when clicking the button inside each Test component, nothing happens.
I suspect that since I've used the innerHTML property it just skips Vue even handler mechanism somehow, so I'm kinda stuck.
Here's the relevant components section:
Dashboard2.vue:
<template>
<div>
<vue-good-table
:columns="columns"
:rows="rows"
:search-options="{enabled: true}"
styleClass="vgt-table condensed bordered"
max-height="700px"
:fixed-header="true"
theme="black-rhino">
</vue-good-table>
</div>
</template>
<script>
import axios from 'axios';
import Vue from 'vue';
import { serverURL } from './Config.vue';
import Test from './Test.vue';
export default {
name: 'Dashboard2',
data() {
return {
jobName: 'team_regression_suite_for_mgmt',
lastXBuilds: 7,
builds: [],
columns: [
{
label: 'Test Name',
field: 'testName',
},
],
rows: [],
};
},
methods: {
fetchResults() {
const path = `${serverURL}/builds?name=${this.jobName}&last_x_builds=${this.lastXBuilds}`;
axios.get(path)
.then((res) => {
this.builds = res.data;
this.builds.forEach(this.createColumnByBuildName);
this.createTestsColumn();
this.fillTable();
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
});
},
createBaseRow(build) {
return {
id: build.id,
name: build.name,
cluster: build.resource_name,
startTime: build.timestamp,
runtime: build.duration_min,
estimatedRuntime: build.estimated_duration_min,
result: build.result,
};
},
addChildRows(build, children) {
const row = this.createBaseRow(build);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < build.sub_builds.length; i++) {
const currentBuild = build.sub_builds[i];
if (currentBuild.name === '') {
this.addChildRows(currentBuild, children);
} else {
children.push(this.addChildRows(currentBuild, children));
}
}
return row;
},
createColumnByBuildName(build) {
this.columns.push({ label: build.name, field: build.id, html: true });
},
addRow(build) {
const row = this.createBaseRow(build);
row.children = [];
this.addChildRows(build, row.children);
this.rows.push(row);
},
createTestsColumn() {
const build = this.builds[0];
const row = this.createBaseRow(build);
row.children = [];
this.addChildRows(build, row.children);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < row.children.length; i++) {
this.rows.push({ testName: row.children[i].name });
}
},
fillBuildColumn(build) {
const row = this.createBaseRow(build);
row.children = [];
this.addChildRows(build, row.children);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < row.children.length; i++) {
const childBuild = row.children[i];
const TestSlot = Vue.extend(Test);
const instance = new TestSlot({
propsData: {
testName: childBuild.name,
result: childBuild.result,
runTime: childBuild.runtime.toString(),
startTime: childBuild.startTime,
estimatedRunTime: childBuild.estimatedRuntime.toString(),
},
});
instance.$mount();
this.rows[i] = Object.assign(this.rows[i], { [build.id]: instance.$el.innerHTML });
}
},
fillTable() {
this.builds.forEach(this.fillBuildColumn);
},
},
created() {
this.fetchResults();
},
};
</script>
<style scoped>
</style>
Test.vue
<template>
<div>
<b-card :header="result" class="mb-2" :bg-variant="variant"
text-variant="white">
<b-card-text>Started: {{ startTime }}<br>
Runtime: {{ runTime }} min<br>
Estimated: {{ estimatedRunTime }} min
</b-card-text>
<b-button #click="sayHi" variant="primary">Hi</b-button>
</b-card>
</div>
</template>
<script>
export default {
name: 'Test',
props: {
id: String,
testName: String,
build: String,
cluster: String,
startTime: String,
runTime: String,
estimatedRunTime: String,
result: String,
},
computed: {
variant() {
if (this.result === 'SUCCESS') { return 'success'; }
if (this.result === 'FAILURE') { return 'danger'; }
if (this.result === 'ABORTED') { return 'warning'; }
if (this.result === 'RUNNING') { return 'info'; }
return 'info';
},
},
methods: {
sayHi() {
alert('hi');
},
},
};
</script>
<style scoped>
</style>
I know this is a lot of code.
The specific relevant section (in Dashboard2.vue) is fillBuildColumn
Again - I'm a newbie to Vue JS - that being said my hunch tells me I'm
doing many things wrong here.
Any help will be greatly appreciated.
EDIT:
by loosing the innerHTML property and the html type I'm ending up with a:
"RangeError: Maximum call stack size exceeded" thrown by the browser.
Not sure what's causing it
I have made a CodeSandbox sample. I might have messed with the data part. But it gives the idea.
fillBuildColumn(build) {
const row = this.createBaseRow(build);
row.children = [];
this.addChildRows(build, row.children);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < row.children.length; i++) {
const childBuild = row.children[i];
// i might have messed up with the data here
const propsData = {
testName: childBuild.name,
result: childBuild.result,
runTime: childBuild.runtime.toString(),
startTime: childBuild.startTime,
estimatedRunTime: childBuild.estimatedRuntime.toString()
};
this.rows[i] = Object.assign(this.rows[i], {
...propsData
});
}
}
createColumnByBuildName(build) {
this.columns.push({
label: build.name,
field: "build" + build.id //guessable column name
});
}
<vue-good-table :columns="columns" :rows="rows">
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field.startsWith('build')">
<Cell
:testName="props.row.testName"
:build="props.row.build"
:cluster="props.row.cluster"
:startTime="props.row.startTime"
:runTime="props.row.runTime"
:estimatedRunTime="props.row.estimatedRunTime"
:result="props.row.result"
></Cell>
</span>
<span v-else>{{props.formattedRow[props.column.field]}}</span>
</template>
</vue-good-table>
The idea is rendering a component inside a template and do it conditionally. The reason giving guessable column name is to use condition like <span v-if="props.column.field.startsWith('build')">. Since you have only 1 static field the rest is dynamic you can also use props.column.field !== 'testName'. I had problems with rendering i had to register table plugin and Cell component globally.

How to get the checked tree structure values using angular2-tree plugin

Here I am generating a dynamic tree structure using my json and angular2 - tree component and till every thing is fine now, I am unable to generate the selection events ad when ever we select the events that particular names have to be selected as objects if child is there and I tried this URL and in the documentation also I didn't find any methods for getting the selected valules so please, suggest me on that.
https://angular2-tree.readme.io/docs
below is my code
options = {
useCheckbox: true
};
nodes;
data = {
"info": {
"laptop": {
},
"config": {
"properties": {
"ram": {
},
"processor": {
},
"hdd": {
}
}
},
"link": {
},
"name": {
},
"company": {
"properties": {
"model": {
},
"maker": {
"type": "integer"
},
"country": {
"type": "text"
},
"enterprise": {
}
}
}
}
};
check(){
const results = Object.keys(this.data.info).map(k => ({
name: k,
children: this.data.info[k].properties
? Object.keys(this.data.info[k].properties).map(kk => ({ name: kk }))
: []
}));
this.nodes = results;
}
.html code
<button type="button" (click)="check()">click</button>
<hr>
<input id="filter" #filter (keyup)="tree.treeModel.filterNodes(filter.value)" placeholder="filter nodes" />
<button (click)="tree.treeModel.clearFilter()">Clear Filter</button>
<tree-root #tree [focused]="true" [options]="options" [nodes]="nodes"></tree-root>
stackblitz link
https://stackblitz.com/edit/angular-kh28sg
I don't know if there a better way You can access to the selected nodes using tree.treeModel.selectedLeafNodeIds. You must before check is isSelected or not
See the docs/API
For example if you has a button
<!--I like pass as argument the property "treeModel" of #tree ("reference variable")-->
<button (click)="click(tree.treeModel)">sendData</button>
<tree-root #tree [focused]="true" [options]="options" [nodes]="nodes"></tree-root>
Your function click can be like
click(tree:TreeModel)
{
console.log(tree.activeNodes);
Object.keys(tree.selectedLeafNodeIds).forEach(x=>{
let node:TreeNode=tree.getNodeById(x);
if (node.isSelected)
{
console.log("Selected:",node.data.name,
"Parent:",node.parent.data.name);
}
})
}
NOTE: I forked your stackblitz
Updated create an object with the response
click(tree: TreeModel) {
console.log(tree.activeNodes);
let result: any = {} //<--declare a variable
Object.keys(tree.selectedLeafNodeIds).forEach(x => {
let node: TreeNode = tree.getNodeById(x);
if (node.isSelected) {
console.log("Selected:", node.data.name,
"Parent:", node.parent.data.name);
if (node.parent.data.name) //if the node has parent
{
if (!result[node.parent.data.name]) //If the parent is not in the object
result[node.parent.data.name] = {} //create
result[node.parent.data.name][node.data.name] = true;
}
else {
if (!result[node.data.name]) //If the node is not in the object
result[node.data.name] = {} //create
}
}
})
console.log(result);
}
I couldn't get the accepted answer to work, my tree.getNodeById(x) would always return null
So I used the action mapping:
Add IActionMapping to your Tree component
actionMapping: IActionMapping = {
// ...
checkboxClick: (tree, node) => {
node.data.checked = !node.data.checked;
this.setCheckedNodes(node.id, node.data.checked);
}
}
Method to store the values in a service:
private setCheckedNodes(id: string, checked: boolean) {
if (!this.treeService.selectedIds) {
this.treeService.selectedIds = new Array<[string, boolean]>();
}
const checkedNode = this.treeService.selectedIds.find(cn => cn[0] === id);
if (checkedNode) {
if (checkedNode[1] !== checked) {
checkedNode[1] = checked;
this.treeService.selectedIds[id] = checkedNode;
}
} else {
this.treeService.selectedIds.push([id, checked]);
}
}
Then on some select event get or emit the values stored in the service:
export class treeService {
// ...
public selectedIds: Array<[string, boolean]>;
}
Probably want to clear them afterwards

Vue props data not updating in child component

Hi everyone I just want some explanation about vue props data. So I'm passing value from parent component to child component. The thing is when parent data has data changes/update it's not updating in child component.
Vue.component('child-component', {
template: '<div class="child">{{val}}</div>',
props: ['testData'],
data: function () {
return {
val: this.testData
}
}
});
But using the props name {{testdata}} it's displaying the data from parent properly
Vue.component('child-component', {
template: '<div class="child">{{testData}}</div>',
props: ['testData'],
data: function () {
return {
val: this.testData
}
}
});
Thanks in advance
Fiddle link
This is best explained with a very simple example
let a = 'foo'
let b = a
a = 'bar'
console.info('a', a)
console.info('b', b)
When you assign...
val: this.testData
you're setting the initial value of val once when the component is created. Changes to the prop will not be reflected in val in the same way that changes to a above are not reflected in b.
See https://v2.vuejs.org/v2/guide/components.html#One-Way-Data-Flow
I resolve with! this.$set(this.mesas, i, event);
data() {
return { mesas: [] }
},
components: {
'table-item': table_l,
},
methods: {
deleteMesa: function(event) {
let i = this.mesas.map(item => item.id).indexOf(event.id);
console.log("mesa a borrare", i);
this.mesas.splice(i, 1);
},
updateMesa: function(event) {
let i =this.mesas.map(item => item.id).indexOf(event.id);
console.log("mesa a actualizar", i);
/// With this method Vue say Warn
//this.mesas[i]=event;
/// i Resolve as follow
this.$set(this.mesas, i, event);
},
// Adds Useres online
addMesa: function(me) {
console.log(me);
this.mesas.push(me);
}
}

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.

Categories

Resources