Property not defined on the instance but references during render - vue.js

I am using VueJS 2.5.16, in a sfc way with TypeScript and vue-class-component
My BookingTable.Vue:
<template>
<div>
<button class="refresh-button" v-on:click="refresh()"><img src="../assets/Data-Synchronize.png"></button>
<table>
<tr v-bind:key="booking.id" v-for="booking in bookings">
[...]
</tr>
</table>
</div>
</template>
<script lang="ts">
class Booking {
Id : number = 0;
StartTime : string = '';
EndTime : string = '';
EnergyConsumed : number = 0;
User : number = 0;
Parking : number = 0;
Plug : number = 0;
Charging : boolean = false;
Cancellation : string = '';
}
export default class BookingTable extends Vue {
bookings : any;
[Other properties and methods]
</script>
And this is the Warning that VueJS is throwing :
[Vue warn]: Property or method "bookings" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
I already searched through both stack overflow and github issues section but I could not undertand where my mistake was. So if someone can explain it to me, i'll be very grateful :)
Thank you

data is what is bound in the template scope. It doesn't look like you have data defined anywhere. You can even see this in the Microsoft Vue TypeScript example.
Use this:
export default class BookingTable extends Vue {
data(): {
return {
bookings: [];
}
}
}
Alternatively it looks like you can use decorators to declare properties as well per this example in the same repository from above: HelloDecorator.vue

Related

Find nearest parent Vue component of template ref (Vue 3)

When a Vue template ref is mounted, I want to get the nearest parent Vue component. This should be generic and work for any template ref so I've put it in a composition function (but that's just an implementation detail).
I had this working but my implementation used elem.__vueParentComponent while iteratively searching an element's ancestors. While reading the Vue source code I saw __vueParentComponent was only enabled for dev mode or if dev tools is enabled in production. Thus, I don't want to rely on that flag being enabled.
I thought this might be possible using vnodes but this isn't easily google-able. Here's an example of what I'm trying to do:
function useNearestParentInstance(templateRef) {
function getNearestParentInstance(el) {
// code here
}
onMounted(() => {
const el = templateRef.value;
const instance = getNearestParentInstance(el);
// do something with instance
});
}
<template>
<div>
<SomeComponent>
<div>
<div ref="myElem"></div>
</div>
</SomeComponent>
</div>
</template>
<script>
export default {
setup() {
const myElem = ref();
// nearest would be SomeComponent's instance in this case
useNearestParentInstance(myElem);
...
}
}
</script>
If you want the nearest vue parent you can simply use
ref().$parent // Not sure if syntax is same in vue3
ref().$parent will get the first vuecomponent that is the parent of the ref that you placed.

Why vue need to forceUpdate components when they include static slot

Why vue needs to forceUpdate child component that has a static slot when it update self
It will trigger too much update calculate when a component has lots of child components that has a static slot
// my-button.vue
<template>
<div>
<slot></slot>
</div>
</template>
// my-com.vue
<template>
<div>
<span>{{ foo }}</span>
<template v-for="(item, index) in arr">
<my-button>test</my-button>
</template>
</div>
</template>
<script>
export default {
data() {
return {
foo: 1,
arr: (new Array(10000)).fill(1)
}
}
}
</scirpt>
If run this.foo = 2 will lead update queue include 10000 watcher. When I read source code I found the following code
function updateChildComponent (
...
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
...
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
...
}
I have found this issue on GitHub.
Unfortunately, any child components with static slot content still
need to be forced updated. This means the common use case of
<parent><child></child></parent> doesn't benefit from this change,
unless the default slot is explicitly forced into a scoped slot by
using <parent v-slot:default><child></child></parent>. (We cannot
directly force all slots into scoped slots as that would break
existing render function code that expects the slot to be present on
this.$slots instead of this.$scopedSlots)
Seems like it's fixed in 2.6.
In 2.6, we have introduced an optimization that further ensures parent
scope dependency mutations only affect the parent and would no longer
force the child component to update if it uses only scoped slots.
To solve your problem just update your Vue version to 2.6. Since it's just a minor update nothing will break down. What about the reason to call forceUpdate - only Evan You knows that :)
Hello i solved this problem with this:
export function proxySlots(scopedSlots: any): any {
return Object.keys(scopedSlots).reduce<any>(
(acc, key) => {
const fn = scopedSlots[key];
fn.proxy = true;
return { ...acc, [key]: fn };
},
{ $stable: true },
);
}
const ctx = {
// ...
scopedSlots: proxySlots({ someSlot: () => <span>Hello</span>})
// or from provided slots : this.$scopedSlots or context.slots if using composition api
}
It's a bit hackish but no more forced update.

Vue component prop change does not trigger rerender

In my Vue 2 application I have a big object that is passed from the root component down several levels to some child components as a prop. When I change some property of the object the child components should update and rerender. In some cases they do, in other cases they don't. I need some help spotting why it does not work.
Here is a child component, which does not update:
<template>
<div class="upgradeBar">
{{level}}
<div
v-for="lvlNum in maxLevel + 1"
class="level"
v-bind:class="{reached: isLevelReached(lvlNum - 1)}"
></div>
<button
class="btnUpgrade"
#click="onLevelUp()"
v-if="!isLevelReached(maxLevel)"
>
+
</button>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import Upgradable from "../../../models/Upgradable";
#Component()
export default class UpgradeBar extends Vue {
name: 'UpgradeBar';
#Prop() entity: Upgradable;
get level(): number {
return this.entity.level;
}
get maxLevel(): number {
return this.entity.MAX_LEVEL;
}
onLevelUp() {
this.entity.levelUp();
}
isLevelReached(level: number): Boolean {
return this.entity.level >= level;
}
}
</script>
The component is called like this:
<UpgradeBar :entity="entity" />
All the code works. When I click the btnUpgrade button entity.level is indeed changed, but I need to close and reopen the component to see the changes. Also the browser dev tool does not show the change instantly. I need to click on the component to refresh the values in the debugger.
EDIT:
The entity class looks basicly like this (excerpt):
class Entity {
name: string = 'some name';
level: number = 1;
}
I searched deeper and it seems to boils down to this: Some properties of the object are reactive (they have getters / setters created by vue) and some don't. entity.name has a setter, so changing it updates the component. entity.level does not. Here's the question: Why are they treated differently? Here is a log:
Can't tell for sure without seeing the code for entity.levelUp, but it seems like a reactivity issue, that may be solved by using Vue.$set inside that function.
You can confirm this being the case by adding this.$forceUpdate(); after this.entity.levelUp();
update
this._level = this._level + 1;
can be changed to
Vue.$set(this, _level, this._level + 1);
You will need to import Vue in that component/file to access the $set function
You don't show (or I can't find) the code that changes the object, but are you using $set() or Vue.set() instead of simply changing the object's properties directly? Changing a property directly generally doesn't work because of reactivity limitations
Edited to add:
I see now. I think you want something like:
this.$set(this, '_level', this._level + 1);

Dynamically add properties to Vue component

I am loading data from the database which drives what type of component I display
An AJAX call goes off and returns me some data (this can be restructured if needed)
{
component_type: 'list-item',
props: {
name: 'test',
value: 'some value'
}
}
This is accessible on my parent object a variable called component
Within the template of my parent object I have the following
<component :is="component.component_type" ></component>
This works fine and loads the component as expected.
Next I want to add the properties from my data object into this tag too
<component :is="component.component_type" {{ component.props }} ></component>
This doesn't work and rejects writing a tag with {{ in it. I presume this is an error thrown by the browser rather than Vue, although I'm unsure.
For reference I want the output to actually look like:
<component :is="component.component_type" name='test' value='some value' ></component>
How can I go about passing in these properties? Ideally I'd like these to be tied to data / props of the parent as I'm showing so that they can easily be changed in database and the UI will change accordingly.
At worst I will generate it all on server side, but I'd rather do it via ajax as I'm currently trying to do.
In case anyone is wondering how to do this using Vue 2 you can just pass an object to v-bind:
<template>
<component :is="componentType" v-bind="props"></component>
</template>
<script>
export default {
data() {
return {
componentType: 'my-component',
props: {
foo: 'foofoo',
bar: 'barbar'
}
}
}
}
</script>
Following this thread, I see two options to do this.
one is to pass a single prop which is an object in itself, and pass all the relevant key values in it which can be used by the child component, something like following:
<component :is="component. component_type"
:options="component.props"
</component>
Other solution mentioned is to have a directive, where you pass the object and it will set the attributes which are keys in that object to corresponding values, you can see this in work here.
Vue.directive('props', {
priority: 3000,
bind() {
// set the last component child as the current
let comp = this.vm.$children[this.vm.$children.length - 1];
let values = null;
if(this._scope && this._scope.$eval) {
values = this._scope.$eval(this.expression);
} else {
values = this.vm.$eval(this.expression);
}
if(typeof values !== 'object' || values instanceof Array) {
values = { data: values };
}
// apply properties to component data
for(let key in values) {
if(values.hasOwnProperty(key)) {
let hkey = this.hyphenate(key);
let val = values[key];
if(typeof val === 'string') {
comp.$options.el.setAttribute(hkey, values[key]);
} else {
comp.$options.el.setAttribute(':' + hkey, values[key]);
}
}
}
console.log(comp.$options.el.outerHTML);
comp._initState();
},
/*
* Hyphenate a camelCase string.
*/
hyphenate(str) {
let hyphenateRE = /([a-z\d])([A-Z])/g;
return str.replace(hyphenateRE, '$1-$2').toLowerCase();
}
});
and use it like this:
<div class="app">
<component is="component.component_type" v-props="component.props"></component>
</div>
As far as I know, there is no rest props (spread props) syntax in Vue.js template.
A possible solution is render functions. You have full power of JavaScript when using render functions, so you can do something like this:
render (h) {
return h('foo', {
props: {
...yourData
}
})
}
I created a simple example here: http://codepen.io/CodinCat/pen/bgoVrw?editors=1010
both component type (like foo) and props can be dynamic (but you still need to declare all the possible fields of prop (like a, b and c) in your child components)
There is another solution is JSX.
Use the JSX babel plugin: https://github.com/vuejs/babel-plugin-transform-vue-jsx
then you can use the spread props syntax:
return <foo {...{yourData}}></foo>

Getting element height

I was curious if I can get element properties form component template.
So I have made simple div with class and I've made this class:
export class viewApp{
elementView: any;
viewHeight: number;
myDOM: Object;
constructor() {
this.myDOM = new BrowserDomAdapter();
}
clickMe(){
this.elementView = this.myDOM.query('div.view-main-screen');
this.viewHeight = this.myDOM.getStyle(this.elementView, 'height');
}
}
getStyle(), query() are from BrowserDomAdapter.
My problem is when I try to get height it is null, but when I set some height by setStyle() and then I get it by getStyle() it returns proper value.
After checking DOM and styles in browser I discovered that is because of two CSS elements. One is: .view-main-screen[_ngcontent-aer-1]{} and second one is element{}.
.view-main-screen has some stylings, but element is empty. When I add styles by setStyle() it appears in element{}. Why is that? How can I get element properties by using Angular2?
The correct way is to use #ViewChild() decorator:
https://angular.io/docs/ts/latest/api/core/index/ViewChild-decorator.html
Template:
<div class="view-main-screen" #mainScreen></div>
Component:
import { ElementRef, ViewChild } from '#angular/core';
export class viewApp{
#ViewChild('mainScreen') elementView: ElementRef;
viewHeight: number;
constructor() {
}
clickMe(){
this.viewHeight = this.elementView.nativeElement.offsetHeight;
}
}
That should do it but obviously you need to add your Component decorator.
Edit:
For Angular 8 or later you need to provide the 2nd parameter in ViewChild
#ViewChild('mainScreen', {read: ElementRef, static:false}) elementView: ElementRef;
update2
constructor(private elementRef:ElementRef) {}
someMethod() {
console.log(this.elementRef.nativeElement.offsetHeight);
}
Accessing nativeElement directly is discouraged but current Angular doesn't provide other ways as far as I know.
update
https://github.com/angular/angular/pull/8452#issuecomment-220460761
mhevery commented 12 days ago
We have decided to remove Ruler service, and so it is not part of the public API.
original
As far as I know the Ruler class should provide that functionality
https://github.com/angular/angular/blob/master/modules/angular2/src/platform/browser/ruler.ts if this isn't enought you probably need to access elementRef.nativeElement and use direct DOM access and functions provided by the elements.
new Ruler(DOM).measure(this.elRef).then((rect: any) => {
});
Rules service is safe in WebWorker.
See also the comments on https://github.com/angular/angular/issues/6515#issuecomment-173353649
<div #getElementHeight>
Height
</div>
Height of element is {{ getElementHeight.offsetHeight }}
<div *ngFor="let item of items" (click)="itemClick($event.currentTarget)"></div>
itemClick(dom){
var height=dom.clientHeight;
// ...
}