I use vue-property-decorator, it's a simple component and I got error message:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "message"
What this message means? and how can I solve this?
here is my code for example:
<template>
<v-layout row justify-center>
<v-dialog v-model="dialog">........</v-dialog>
</v-layout>
</template>
<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
#Component({})
export default class SomeModal extends ... {
#Prop() public dialog?: boolean;
#Prop() public message?: string;
constructor() {
super();
}
public showError(er) {
this.message = er.message;
this.dialog = true;
}
}
</script>
<style scoped lang="scss">
</style>
I am not used with this syntax for vue, but the message is pretty clear : you need to define a data property, or a computed variable. That means either :
data: {
dialogData: ''
}
constructor() {
super();
this.dialogData = this.dialog;
}
or :
computed: {
dialogData() {
return this.dialog;
}
}
See the vuejs doc on computed properties.
Edit : with vue-property-decorator, it could be :
#Component
export default class YourComponent extends Vue {
// your code here...
private _dialogData: string = '';
constructor() {
super();
this._dialogData = this.dialog;
}
}
Related
I want to create a basic state management using Lit reactive controllers.
The purpose is to share property values accross the application.
The issue occurs when a controller is attached to a view and to a component nested in the view. When value inside controller changes, the value in the view gets updated, but not in the nested component.
Example:
state.js contains the store logic. A view access the store to create a value and show state value. Nested components also show state value.
state.js
export class StateController {
static get properties() {
return {
state: { type: Object },
host: { type: Object }
}
}
constructor(host) {
// Store a reference to the host
this.host = host;
this.state = {};
// Register for lifecycle updates
host.addController(this);
}
_setStoreValue(property, val) {
this.state[property] = val;
this.host.requestUpdate();
}
}
component.js
import { LitElement, html } from 'lit';
import { StateController } from '../state.js';
export class TestComponent extends LitElement {
static get properties() {
return {
stateCtrl: { type: Object },
state: { type: Object },
};
}
constructor() {
super();
this.stateCtrl = new StateController(this);
this.state = this.stateCtrl.state
}
render() {
return html` Value in component: ${this.state?.test} `;
}
}
customElements.define('test-component', TestComponent);
view.js
import { LitElement, html } from 'lit';
import { StateController } from '../state.js';
import './test-component.js';
export class MonTodo extends LitElement {
static get properties() {
return {
stateCtrl: { type: Object },
state: { type: Object },
};
}
constructor() {
super();
this.stateCtrl = new StateController(this);
this.state=this.stateCtrl.state
}
render() {
return html`
<button #click=${() => this.setValueTest()}>Set value to 3</button>
Value in view: ${this.state?.test}
<h3> Component 1</h3>
<test-component></test-component>
<h3> Component 2</h3>
<test-component></test-component>
`;
}
setValueTest() {
this.stateCtrl._setStoreValue("test", 3)
}
}
customElements.define('mon-todo', MonTodo);
A button click in view.js updates this.state.test in view.js but not in component.js
Since you create a new StateController in both MonTodo and TestComponent, they are two different instances of the StateController which only have their specific component as host.
So the StateController in MonTodo only has MonTodo as a host and only updates that and not TestComponent.
You would need to share one controller with both components and call requestUpdate on both.
I have this component in my codebase,
<template>
<v-dialog v-model="modalVisible" content-class="muc-modal" max-width="350" persistent>
<v-card>
<component :is="component"/>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import Vue from 'vue';
import { Component, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class";
const Modal = namespace("Modals");
#Component
export default class AppModal extends Vue {
public component: any = null;
#Modal.State
public modalVisible!: boolean;
#Modal.State
public modalComponent!: string;
#Modal.State
public modalComponentPath!: string|null;
get injectedComponent() {
return this.modalComponent;
}
#Modal.Mutation
public hideModal!: () => void
#Watch('injectedComponent')
onModalComponent(componentName: string) {
if(!componentName) return;
this.component = Vue.component(componentName, () => import(`./${this.modalComponentPath}`));
}
}
</script>
<style scoped lang="scss">
.muc-modal {
width:350px;
max-width:90%;
}
</style>
It is modal component that takes another component, this is run via mutation on the modal store,
import { VuexModule, Module, Mutation, Action } from "vuex-module-decorators";
#Module({ namespaced: true })
export default class Modals extends VuexModule {
//#region "State"
public modalVisible: boolean = false;
public modalComponent: string|null = null;
public modalComponentPath: string|null = null;
//#endregion "State"
//#region "Mutations"
#Mutation
public showModal(modalComponent:string, modalComponentPath: string|null = null) {
this.modalVisible = true;
this.modalComponent = modalComponent
this.modalComponentPath = modalComponentPath ? modalComponentPath : modalComponent
}
#Mutation
public hideModal() {
this.modalVisible = false;
this.modalComponent = null;
this.modalComponentPath = null;
}
//#endregion "Mutations"
//#region "Getters"
get getVisibility(): boolean {
return this.modalVisible
}
//#endregion "Getters"
//#region "Actions"
//#endregion "Actions"
}
I am wanting to write some tests that a) test the modal display correctly when showModal() mutation is run, b) that it gets hidden correctly when hideModal() mutation is run.
This is my current test file,
import { shallowMount, createLocalVue } from '#vue/test-utils';
import Vuex from 'vuex';
import Modals from '#/store/modal';
import AppModal from '#/components/AppModal.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('AppModal.vue', () => {
let store: any = null;
//let mutations = modals.mutations
beforeEach(() => {
store = new Vuex.Store({
modules: {
"modals" : Modals
}
})
});
it('shows modal when modalVisible is set to true', () => {
console.log(store);
const wrapper = shallowMount(AppModal, {store, localVue});
// const modal = wrapper.find('.muc-modal')
// console.log(modal);
})
});
running this test I get the following response,
console.error node_modules/vuex/dist/vuex.common.js:916
[vuex] module namespace not found in mapState(): Modals/
console.error node_modules/vuex/dist/vuex.common.js:916
[vuex] module namespace not found in mapState(): Modals/
console.error node_modules/vue/dist/vue.runtime.common.dev.js:621
[Vue warn]: Error in render: "TypeError: Cannot read property 'template' of null"
found in
---> <AppModal>
and i have no clue why, can anyone help shed some light on this for me?
This error -
TypeError: Cannot read property 'template' of null
Is a direct result of this coding error -
<component :is="null" />
Why?
The component tag should never have a null value for the is attribute, even if it's momentary. null isn't a valid component, and Jest correctly finds this error even when you don't see it in the browser.
In your above code, the initial value for the property component (which is probably a bad variable name) is null, so that's where this is coming from. A simple fix would be to set a valid component as an initial value so that it is never null.
there are two components in div,When the two components were rendered together, I clicked the button to switch properly, but in the case of rendering only one component, the switch becomes abnormal.
this is my code
Base.vue
<template>
<div :id="id">{{msg}}</div>
</template>
<script lang='ts'>
import { Component, Prop } from "vue-property-decorator";
import Vue from "vue";
#Component
export default class Base extends Vue {
id!: string;
msg = "this is Base";
}
</script>
child.vue(no template)
<script lang='ts'>
import Base from "#/components/Base.vue";
import { Prop, Component } from "vue-property-decorator";
#Component
export default class extends Base {
#Prop({ default: "helloWorld" })
childId!: string;
constructor() {
super();
this.id = this.childId;
this.msg = "this is Child " + this.childId;
}
}
</script>
App.vue(display these components)
<template>
<div id="app">
<Child v-show="!show" childId="child1" style="color:#f00;"/>
<button #click="click">change</button>
<Child v-show="show" childId="child2" style="color:#f0f;"/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Child from "#/components/Child.vue";
import Component from "vue-class-component";
#Component({
components:{
Child,
}
})
export default class App extends Vue {
show= false;
click() {
this.show = !this.show;
}
}
</script>
and click the button the result is
These results are expected. But if all the v-show in the app. vue above are changed to v-if, the result is confusing
then click the button the result is
In our expectation it should display child2 here. So why does this happen?
Your first click creates the the show-property which didn't exist because you didn't create your data() properly.
I'll not speculate exactly in the reasons why, but I assume there might be some funny boolean casts, and the property might not be reactive since it's not in data. Either way, just create it and it'll work as you intended:
export default class App extends Vue {
data(){
return {
show: false
}
},
click() {
this.show = !this.show;
}
}
Thanks!!
I solved this problem when I added different keys to the two Child components
<Child v-if="!show" childId="child1" key="hello1" style="color:#f00;" />
<Child v-if="show" childId="child2" key="hello2" style="color:#f0f;" />
I think the reason is Vue's diff algorithm, Vue considers these two components to be the same component
Because when you use v-if, it will use the single same Child component. The this.msg will only change once in the constructor. The msg will not change when the childId props changed, so you need the Watch. When the childId changed, then to update the msg
Child.vue
<script lang='ts'>
import Base from "#/components/Base.vue";
import { Prop, Component, Watch } from "vue-property-decorator";
#Component
export default class extends Base {
#Prop({ default: "helloWorld" })
childId!: string;
#Watch('childId')
onChildIdChanged(val: any) {this.msg = "this is Child " + val}
constructor() {
super();
this.id = this.childId;
this.msg = "this is Child " + this.childId;
}
}
</script>
I want to use this.props.childName in child function that is defined in the parent function.
But it's TypeScript compile error (Property 'name' does not exist...)
If I use this.props.parentName, it's ok.
How can I access this.props of child class?
interface Prop<T> {
parentName: string
}
class Parent<T> extends React.Component<Prop<T>, State<T>> {
constructor(props: Prop<T>) {
super(props)
}
printName() {}
}
interface PropChildren<T> {
childName: string
}
class Child<T> extends Parent<string> {
constructor(props: PropChildren<T>) {
super(props)
}
printName() {
console.log(this.props.childName) // here I want to use children prop but compile error
}
}
Your child component extends the parent component, and the type of props in the parent is Prop<T>, which contains only the property parentName.
In order to have PropChildren as the type of props in the child component you should declare it as:
class Child<T> extends React.Component< PropChildren<T>, State<T>> {
// ...
}
By the way, you don't need to make your props interfaces generic (with <T>). The generics are used only when the interface can be used in different contexts with different data types.
Based on your comment, here is an example of how you can share the behavior of the parent with the child, but still being able to define a different data type for the child's props:
interface PropParent {
parentName: string
}
class Parent<TProp extends PropParent> extends React.Component<TProp, State> {
constructor(props: TProp) {
super(props)
}
printName() {}
}
interface PropChildren extends PropParent {
childName: string
}
class Child<T> extends Parent<PropChildren> {
constructor(props: PropChildren) {
super(props)
}
printName() {
console.log(this.props.childName)
}
}
first, you don't need any Generics in interface unless you need to use it in different places.
second, class Child should also extend from React.Component not from its parent.
so here is what might be a better code
import React from 'react'
interface IParentProps {
readonly parentName: string;
readonly children?: JSX.Element
}
interface IPropsChild {
readonly childName: string;
}
class Parent extends React.Component<IParentProps> {
constructor(props: IParentProps) {
super(props)
}
printName = () => {
}
render() {
return <Child childName={this.props.parentName} />
}
}
class Child extends React.Component<IPropsChild> {
constructor(props:IPropsChild) {
super(props)
}
printName = () => {
console.log(this.props.childName)
}
}
In order to allow both, proper props definition and the child class derive from the parent class you have to include the props type in your definition:
interface ParentProp<T> {
parentName: string;
}
export class Parent<T, P = ParentProp<T>, S = {}, SS = any> extends React.Component<P, S, SS> {
public printName() {
// console.log(this.props.parentName); Doesn't compile, as P can be any prop interface.
}
}
interface ChildProp<T> {
childName: string;
}
export class Child<T> extends Parent<T, ChildProp<T>> {
public printName() {
console.log(this.props.childName);
}
}
I just try to pass a Promise as a Prop to a child component which expect a Promise but I have an error that tells the expected type is Object.
I use TypeScript, vue-property-decorator and vue-class-component for my project.
My code looks like this:
<template>
<child-component :listPromise="listAsPromise"/>
</template>
<script lang="ts">
import { Component, Vue, Prop, Inject } from 'vue-property-decorator'
import { EditableReferentielModel } from '#/api/model/referentiel'
import ReferentielResource from '#/api/resources/referentiel'
import { AxiosResponseExt } from '#/api/resources/index'
export default class ParentComponent extends Vue {
#Inject()
referentielResource: ReferentielResource
listAsPromise: Promise<EditableReferentielModel[] & AxiosResponseExt> = this.referentielResource.getStadeDeveloppement()
}
</script>
<script lang="ts">
import { Component, Vue, Prop, Inject } from 'vue-property-decorator'
import ReferentielResource from '#/api/resources/referentiel'
import { EditableReferentielModel } from '#/api/model/referentiel'
import { AxiosResponseExt } from '#/api/resources/index'
#Component
export default class ChildComponent extends Vue {
#Inject()
referentielResource: ReferentielResource
#Prop({ default: null })
value: EditableReferentielModel | null
#Prop({ default: null })
listPromise: Promise<EditableReferentielModel[] & AxiosResponseExt>
referentiel: EditableReferentielModel[] = []
selected: EditableReferentielModel | null = this.value
async mounted () {
this.referentiel = await this.listPromise
}
}
</script>
With this code, I still have this error: [Vue warn]: Invalid prop: type check failed for prop "listPromise". Expected Object, got Promise. even if the listPromise type is set to any.
What is wrong ?