How to force propertyChange event - javascript

I have a large 2 dimensional array where I update values in place and my other polymer elements do not get notified of changes. I have an idea why, and I think my solution is to force a property change event, but I'm not sure.
For a model element item like this...
<polymer-element name="my-model" attributes="rows">
...
<script>
...
this.rows[0][0] = newValue;
</script>
I find when I use it in a view element like this...
<my-view rows="{{model.rows}}"></my-view>
where the view's implementation is like this...
<polymer-element name="my-view" attributes="rows">
<template>
<template repeat="{{row in rows}}">
<template repeat="{{col, i in cols}}">
{{row[i]}}
</template>
</template>
</template>
my view does not get updated unless I reset the array value in the model
this.rows = []
Can I somehow force a property change event. I tried...
this.rows = this.rows
..and...
this.notifyPropertyChanged('rows', this.rows, this.rows)
and no luck. Any ideas would be appreciated. Thanks!

There's a mistake in your second template. Since you're using named scoped, your second template needs to be {{col, i in row}}:
<template repeat="{{row in rows}}">
<template repeat="{{col, i in row}}">
This worked for me: http://jsbin.com/sidagomu/1/edit
(click the element -> data updates -> template re-renders)

Related

Vue3 v-for + component => triggers lots of renders

I have a hard time figuring out a huge performance issue with a component list via v-for.
Here is my typescript code:
<template>
<template v-for="item in list" :key="item.id">
<TestComponent #mouseenter="hoveredItem = item" #mouseleave="hoveredItem = null" />
</template>
<div v-if="hoveredItem">hovered</div>
</template>
<script lang="ts">
import TestComponent from 'TestComponent.vue';
import { Options, Vue } from 'vue-class-component';
interface IItem {id:number, message:string};
#Options({
props:{},
components:{ TestComponent, }
})
export default class TestView extends Vue {
public list:IItem[] = [];
public hoveredItem:IItem|null = null;
public mounted():void {
for (let i = 0; i < 3; i++) {
this.list.push({ id:i, message:"Message "+(i+1), });
}
}
}
</script>
When I roll over an item (see # mouseeenter), a render() is triggered on all the items of the list which shouldn't be necessary.
I checked with Vue Devtools extension that shows these events for every single item of the list :
render start
render end
patch start
patch end
If i remove the following line, no render/patch is triggered:
<div v-if="hoveredItem">hovered!</div>
If instead of storing the item instance to hoveredItem i just raise a flag to display that div, i don't have the issue.
If instead of instantiating the <TestComponent> I use a simple <div> i don't have the issue.
If I don't use a v-for but manually instantiate items, I don't have the issue.
If I $emit a custom event from the instead of using native #mouseover
The <TestComponent> is just that:
<template>
<div>item</div>
</template>
Here is a codesandbox showing the issue of the first example and the fix via an $emit() from the child component
https://dh5ldo.csb.app
Do you have any hint on why the first example triggers a render on all the list items when it's not something we would expect ?
Thank you for reading me :)
Ok i finally figured it out.
After reading this article:
https://codeburst.io/5-vue-performance-tips-98e184338439
..that links to this github answer:
https://github.com/vuejs/core/issues/3271#issuecomment-782791715
Basically, when using a component on a v-for, Vue needs to know when it has to update it.
To achieve that it looks for any prop used on the DOM and builds up a cache to make further updates faster.
But when using a prop on an event handler, Vue cannot build that cache.
If that prop is updated, Vue will know it is linked to your component but it won't know if it actually should trigger a render or not. It will trigger it just in case.
If you use a prop on every instances of the list, any update of that prop will trigger a render on all the items.
What's unclear to me though is why this does not happen if I simply remove this line from the example of my first post:
<div v-if="hoveredItem">hovered!</div>
The hoverItem is still used on my event handlers so it should still trigger a render.
TLDR; don't use any property/var within a component event handler
(disclaimer: i may not have understood things properly, appologies if I'm wrong on some points)
Your :key="item.id" should be on the <TestComponent>, it does not work on <template>
<template>
<template v-for="item in list" :key="item.id">
<TestComponent #mouseenter="hoveredItem = item" #mouseleave="hoveredItem = null" />
</template>
<div v-if="hoveredItem">hovered</div>
</template>
I also recommend you do get a linter so it will show your such errors

How to access "this" component in Vue 3 #click event?

I'm using Vue 3 and in a v-for loop, I'm creating multiple button elements. The buttons are being made in another component named wb-button. So I call wb-button in every v-for loop.
I add #click event listener to the wb-button that calls a method inside the current component, simple:
<div v-for="(item,key) in items" :key="key">
<span>{{item.name}}</span>
<wb-button #click="deleteItem(item)">
Delete item!
</wb-button>
</div>
This works how I want, the problem starts when I want to pass the wb-button just like a ref to the deleteItem method. The purpose is to make changes to another ref inside the wb-button. So what I basically want to do is this:
export default{
name: 'listComponent',
methods:{
async deleteItem(item,wbButtonRef){
// The next line is what I want to do
wbButtonRef.$refs.btnElement.putInLoadingStatus()
// do the delete item action
}
}
}
I know I can create a ref on each wb-button and pass an ID or something to the method, but I don't think it is the cleanest way to do it.
If there was something to just pass the wb-button element as the method parameter it would be great. Something like this:
<!-- I want to know if there is something I can put in
the second argument of the 'wb-button' #click event -->
<wb-button #click="deleteItem(item, /* what to put here?*/)">
<!-- "this","$this","$event.target" etc ... -->
I have tried $event.target but it returns the root element of wb-button, what I need is the wb-button itself just like a $ref.
Simply put, you can't. And since this logic is relevant only for the button component itself, it's best to keep this logic within it. Adding a prop and render something based on that, like you suggested yourself in the comments, is a good way to go about it.
Considering Other Options
Although I used #paddotk 's answer, 'the props way' to solve my problem, I'm just adding this answer so anyone who reads this question afterward would have a complete answer.
As far as I have found out, there are two more ways of doing this:
1- As #MrFabio_25 mentioned in the comments, I can create a custom event on the child component and $emit with 'this' as a parameter, so I can handle that in the parent:
// wbButton.vue file
<inside-wb-component ref="btnElement" #click="handleClick">
<button>
<slot></slot>
</button>
</inside-wb-component>
//and in the script tag
//...
methods:{
handleClick(){
this.$emit('buttonClick',this)
}
}
//...
And simply in the parent component:
<wb-button #buttonClick="handleButtonClick">
A text here
</wb-button>
// in the script tag
methods:{
handleButtonClick(elem){
elem.$refs.btnElement.putInLoadingStatus()
}
}
2- The second way to do so, without the child component being involved, is to use an array of refs, as explained here:
Vue 3 Documentation - refs inside v-for
<div v-for="(item,key) in items" :key="key">
<wb-button ref="wbButtonRefs" #click="handleButtonClick(item,key)">
A text here
</wb-button>
</div>
// and in scripts tag
//...
methods:{
handleButtonClick(item,index){
const buttonRef=this.$refs.wbButtonRefs[index]
// Now do whatever with buttonRef
buttonRef.$refs.btnElement.putInLoadingStatus()
}
}
//...
<template>
<button #click="test($event)">Test</button>
</template>
methods:{
test(e){
const comp = e.target.__vueParentComponent
const props = comp.props
}
}

Dynamically creating polymer element inside another custom tag

A polymer element offices-list needs to be created dynamically inside another polymer element's script as so:
<dom-module id="contacts-tag">
<template>
<iron-ajax ... on-response = "handleResponse"></iron-ajax>
</template>
<script>
Polymer({
is: "contacts-tag",
handleResponse: function(request){
var response = request.detail.response;
this.officesRegions = response.officesRegions;
this.officesCities = response.officesCities;
var dynamicEl = document.createElement("offices-list");
dynamicEl.setAttribute("regions", this.officesRegions);
dynamicEl.setAttribute("cities", this.officesCities);
document.body.appendChild(dynamicEl);
}
});
</script></dom-module>
However as soon as it reaches "document.createElement("offices-list");" the element inside this new tag starts rendering, and it's on ready method is already called, while I was expecting them to happen after I set attributes. How can I do it?
Edit: Seems that problem is of different nature. I'm setting objects to attributes and "offices-list" tag is not recognizing them, hence isn't able to access it or loop through it. Then, my question will change to, "How to bind objects?"
I think the right answer is - it depends on what you are trying to achieve.
If you are looking to imperatively data-bind i.e. you want to dynamically add something like this into the template,
<offices-list offices-regions="{{regions}}"
offices-cities="{{cities}}">
</offices-list>
you are out of luck because imperative data-binding is not supported in Polymer 1.0.
If you simply want to pass in parameters (NOT data-bind) into a dynamically-created element, both el.setAttribute() and el.myAttribute = this.myAttribute should work.
Since you are using Polymer, I think you should try to contain DOM manipulations within the framework (inside a Polymer template instance) instead of directly adding to document.body, something like this.
<dom-module id="contacts-tag">
<template>
<iron-ajax ... on-response="handleResponse"></iron-ajax>
<div id="insertion_point"></div>
</template>
<script>
Polymer({
is: "contacts-tag",
handleResponse: function(request) {
...
Polymer.dom(this.$.insertion_point).appendChild(dynamicEl);
}
});
</script>
</dom-module>
Lastly, if your intention is to simply display a <contacts-tag> after the ajax request is completed, I think you can achieve this declaratively.
<dom-module id="contacts-tag">
<template>
<iron-ajax ... on-response="handleResponse"></iron-ajax>
<template is="dom-if" if="{{_unveilOfficesListWhenReady}}">
<offices-list offices-regions="{{_regions}}"
offices-cities="{{_cities}}">
</offices-list>
</template>
</template>
<script>
Polymer({
is: "contacts-tag",
properties: {
_regions: String,
_cities: String,
_unveilOfficesListWhenReady: { type: Boolean, value: false }
},
handleResponse: function(request) {
var response = request.detail.response;
this._regions = response.officesRegions;
this._cities = response.officesCities;
this._unveilOfficesListWhenReady = true;
}
});
</script>
</dom-module>
No dynamic element needed.
You should not use setAttribute but you should set the corresponding property directly.
var dynamicEl = document.createElement("offices-list");
dynamicEl.regions = this.officesRegions;
dynamicEl.cities = this.officesCities;
document.body.appendChild(dynamicEl);
You can use dom api provided by polymer team.
Polymer.dom(parent).appendChild(polymerChild)

Binding an array of objects to template containing google-map-marker elements

I have this <dom-module> with a property locations of type Array that get updated externally every once in a while:
<dom-module id="my-map">
<template>
<google-map api-key="MY_API_KEY" id="map" libraries="visualization,geometry,drawing">
<template is="dom-repeat" items="{{locations}}" as="user">
<template is="dom-repeat" items="{{user.trace}}">
<google-map-marker latitude="{{item.latitude}}" longitude="{{item.longitude}}">
</google-map-marker>
</template>
</template>
</google-map>
</template>
</dom-module>
<script>
Polymer({
is : 'my-map',
properties : {
locations : {
type : Array,
value : [],
observer : '_locationsChanged'
}
},
ready : function(){
var self = this;
this.map = this.querySelector('google-map');
this.map.addEventListener('google-map-ready',function(){
// some styling happening here.
});
},
_locationsChanged : function(newLocations, oldLocations){
// this fires correctly! ...
}
});
</script>
In another module, I send an AJAX request to retrieve the data to be shown as markers on the map.
Once the request finishes, I update the locations property and the _locationsChanged callback fires perfectly fine.
The data in that case looks something like this:
[
{
"_id":"someID1",
"trace":[
{
"latitude":55.629215086862,
"longitude":10.640099246067,
}
]
},
{
"_id":"someID2",
"trace":[
{
"latitude":50.14743798944287,
"longitude":18.52913082363756,
}
]
}
]
Something weird happens.
Whenever locations is an empty array, the newLocations are bound without problems to the <template dom-repeat="{{locations}}"> element. However, if the map already had shown a couple of markers, the old <google-map-marker> objects are still there, the new ones are simply added.
So if I do this in the dev console document.querySelectorAll('google-map-marker') after an update to locations, I see both the newLocations and the oldLocations.
To test that the data-binding works correctly if applied outside the <google-map> element, I added a similar template. Here, everything works perfectly as expected:
<template is="dom-repeat" items="{{locations}}" as="user">
<ul>
<li>
<span>{{user._id}}</span>
<ul>
<template is="dom-repeat" items="{{user.trace}}" as="trace">
<li><span>Lat:</span><span>{{trace.latitude}}</span></li>
</template>
</ul>
</li>
</ul>
</template>
Here's what did not help so far:
Calling clear() on the GoogleMap object.
creating a second property that is bound to the dom-repeat template, but accessed this way:
this.splice('_locations',0,this._locations.length);
newLocations.forEach(function(location){
self.push('_locations',location);
});
using one-way data binding.
manually removing DOM nodes via googleMap.removeChild(marker) and adding them. Okay, this actually worked to some degree but isn't the whole point of data-binding that you don't have to do this?
So, to sum up: The <template is="dom-repeat"> inside the google map does not get notified correctly about changes to the locations property.
Can anybody tell me what I'm doing wrong or if data-binding does not work inside a <google-map> element? Am I mixing things up with shady / shadow DOM? Am I using the dom-repeat thing incorrectly? Am I going to lose my mind? I'll appreciate any hint towards a solution. Thanks!
I am not sure if it makes a difference for your specific problem but default values for arrays and objects should be specified in this format (see value attribute) :
properties : {
locations : {
type : Array,
value : function() { return [];},
observer : '_locationsChanged'
}
},
I also think you have to call clear() on the google-map element because the _updateMarkers() function does not do this for you (see here)

Meteor to not rerender when certain fields change

Meteor re renders a view when a document changes.
Template.story.data = function() {
var storyID = Session.get('storyID');
var story = Stories.findOne({
_id: storyID
})
if (!story)
return;
return story;
};
Here's a template helper for the story template, getting a story from the Stories document.
When fields like story.title changes I want the template to rerender. But when fields like story.viewingusers change I don't want to rerender the template. Anyway to make that happen?
The specific problem that triggered the question was solved by setting the publish function to not publish those fields. However that solution doesn't work for every usecase and a general solution is needed.
What you're looking for is the #constant template helper.
What i'd do in your case is wrap the popover markup in a constant block, then update the content in that block manually in the story.rendered function.
So something like this:
story.html:
<template name="story">
...
{{#constant}}
<!-- Popover markup here -->
{{/constant}}
...
</template>
client.js:
Template.story.rendered = function(){
//Update logic here
}

Categories