how to create a dynamic vue component that has a computed template containing another component with Object properties without passing it as string - vue.js

I have a component like this:
Relation.vue
<template>
<div :is="dynamicRelation"></div>
</template>
<script>
import Entry from '#/components/Entry';
import weirdService from '#/services/weird.service';
export default {
name: 'Relation',
data() {
return {
entry1: { type: 'entity', value: 'foo', entity: {id: 4}},
entry2: { type: 'entity', value: 'bar', entity: {id: 5}},
innerText: '#1 wut #2',
}
},
computed: {
dynamicRelation() {
return {
template: `<div>${this.innerText
.replace('#1', weirdService.entryToHtml(this.entry1))
.replace('#2', weirdService.entryToHtml(this.entry2))}</div>`,
name: 'DynamicRelation',
components: { Entry }
};
}
}
}
</script>
wierd.service.js
export default {
entryToHtml(entry) {
[some logic]
return `<entry entry='${JSON.stringify(entry)}'></entry>`;
// unfortunately I cannot return JSX here: <entry entry={entry}></entry>;
// I get 'TypeError: h is not a function'
// unless there is a way to convert JSX to a pure html string on the fly
}
}
Entry.vue
<template>
<div>{{objEntry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: String // I need this to be Object
},
computed: {
objEntry() {
return JSON.parse(this.entry);
}
}
}
</script>
The innerText property decides how the components will be rendered and it can be changing all the time by having its # slots in any position.
In this example the result is:
<div>
<div>foo</div>
wut
<div>bar</div>
</div>
This works since Entry component has as a property entry that is of type String but I have to JSON.stringify() the entry object in weirdService side and then in Entry component I have to JSON.parse() the string to get the real object back.
How can I make the above work so that I pass an object directly to a dynamic template so I avoid serialization and deserialization all the time.
btw for this to work runtimeCompiler needs to be enabled in vue.config.js:
module.exports = {
runtimeCompiler: true
}
I know I can use JSX to return components with objects in them but this is allowed only in render() function it seems and not custom ones like mine.
Thanks!!

I was able to do what I wanted by using JSON.stringify still but pass the entry as object :entry
wierd.service.js
export default {
entryToHtml(entry) {
return `<entry :entry='${JSON.stringify(entry)}'></entry>`;
}
}
Entry.vue
<template>
<div>{{entry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: Object
}
}
</script>

Related

cant set property of undefined vuejs 3

Within my Something.vue:
<template>
<p>{{ codes }}</p>
</template>
export default {
data() {
return {
codes: 'test'
}
},
name: 'Functions',
props: {
msg: String
},
setup() {
this.codes = 'does it work?';
}
</script>
<style>
</style>
Why would I face this issue?
Uncaught TypeError: Cannot set property 'codes' of undefined
In vue3, you can defined variable like this:
import { ref } from "vue";
export default {
name: 'Functions',
props: {
msg: String
},
setup() {
let codes = ref('does it work?');
return { codes }
}
</script>
And you can use codes in your Vue component.
Because, in setup methods, you can not use this keywords. Its execution is earlier than other life cycle function
If you want to change codes, you can defined a function in setup or methods, like this:
setup() {
let codes = ref("does it work?");
function onChangeCodes() {
codes.value = "changed!";
}
return { codes, onChangeCodes };
}
Then, you can use this function in your template:
<template>
<div>{{ codes }}</div>
<button #click="onChangeCodes">change</button>
</template>
When setup is executed, the component instance has not been created yet,
In other words, You can't access 'codes' when setup() execute.
you can check the document here

How to wrap the slot in a scoped slot at runtime

I have an very unusual scenario.
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
The WrapperComponent should manage all instances of the InnerComponent. However I don't know what will be compiled into the wrapper component.
Usually I would do something like this:
<WrapperComponent>
<template scoped-slot="{data}">
<InnerComponent v-for="(propB) in data" prop-b="Static" :prop-b="propB"></InnerComponent>
</template>
</WrapperComponent>
But I cannot do this for reasons!
I need to be able to create multiple instances of the slot content at runtime. I have created a code sandbox with what I got so far.
https://codesandbox.io/s/stupefied-framework-f3z5g?file=/src/App.vue:697-774
<template>
<div id="app">
<WrapperComponent>
<InnerComponent propA="Static"></InnerComponent>
</WrapperComponent>
</div>
</template>
<script>
import Vue from "vue";
const WrapperComponent = Vue.extend({
name: "WrapperComponent",
data() {
return {
valueMap: ["Child 1", "Child 2"]
}
},
render(h) {
return h("div", {}, [
this.valueMap.map(val => {
console.log(val);
for (const slot of this.$slots.default) {
// this is a read only slot. I can not change this.
// However, I want multiple instances of the slot
// => Inject a scoped-slot
slot.componentOptions.propsData.propB = val;
}
return h("div", {}, [this.$slots.default]);
})
])
}
});
const InnerComponent = Vue.extend({
name: "InnerComponent",
props: {
// This is a static configuration value.
propA: String,
// This is a runtime value. The parent component manages this component
propB: String
},
template: "<div>A: {{propA}} B: {{propB}}</div>"
});
export default {
name: "App",
components: {
WrapperComponent,
InnerComponent
}
};
</script>
This works perfectly fine with static information only, but I also have some data that differs per slot instance.
Because VNodes are readonly I cannot modify this.$slot.default.componentOptions.propsData. If I ignore the warning the result will just be the content that was passed to last instance of the slot.
<WrapperComponent>
<WrapperSubComponent v-for="(propB) in data" key="probB" :prop-b="prop-b">
<InnerComponent propA="Static"></InnerComponent>
</WrapperSubComponent>
</WrapperComponent>
Works after wrapping the component in another component and only executing the logic once.

Properly alert prop value in parent component?

I am new to Vue and have been very confused on how to approach my design. I want my component FileCreator to take optionally take the prop fileId. If it's not given a new resource will be created in the backend and the fileId will be given back. So FileCreator acts as both an editor for a new file and a creator for a new file.
App.vue
<template>
<div id="app">
<FileCreator/>
</div>
</template>
<script>
import FileCreator from './components/FileCreator.vue'
export default {
name: 'app',
components: {
FileCreator
}
}
</script>
FileCreator.vue
<template>
<div>
<FileUploader :uploadUrl="uploadUrl"/>
</div>
</template>
<script>
import FileUploader from './FileUploader.vue'
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
uploadUrl: null
}
},
created(){
if (!this.fileId) {
this.fileId = 5 // GETTING WARNING HERE
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId
}
}
</script>
FileUploader.vue
<template>
<div>
<p>URL: {{ uploadUrl }}</p>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
uploadUrl: {type: String, required: true}
},
mounted(){
alert('Upload URL: ' + this.uploadUrl)
}
}
</script>
All this works fine but I get the warning below
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:
"fileId"
What is the proper way to do this? I guess in my situation I want the prop to be given at initialization but later be changed if needed.
OK, so short answer is that the easiest is to have the prop and data name different and pass the prop to the data like below.
export default {
name: 'FileCreator',
components: {
FileUploader
},
props: {
fileId: Number,
},
data() {
return {
fileId_: this.fileId, // HERE WE COPY prop -> data
uploadUrl: null,
}
},
created(){
if (!this.fileId_){
this.fileId_ = 45
}
this.uploadUrl = 'http://localhost:8080/files/' + this.fileId_
}
}
Unfortunately we can't use underscore as prefix for a variable name so we use it as suffix.

Vue: Using input value in function

I am using Single File Components and I have a modal component that has an
input box but I can't get the value of the input in a function below using the v-modal name. It keeps coming back as 'name is not defined'. Am I using the v-model attribute incorrectly?
<template>
<input v-model="name" class="name"></input>
</template>
<script>
export default {
methods: {
applyName() {
let nameData = {{name}}
}
}
}
</script>
You're right, you're using the v-model property incorrectly.
First off you need to define a piece of state in your component, using data:
export default {
data: () => ({
name: '',
}),
methods: {
log() {
console.log(this.name);
}
}
}
You can then bind this piece of data in your component using v-model="name", just like you did. However, if you want to access this piece of state in your method, you should be using this.name in your applyName() method.
Your {{name}} syntax is used to get access to the data in your template, like so:
<template>
<span>
My name is: {{name}}!
</span>
</template>
You have to use this pointer to access the model:
<template>
<input v-model="inputName" class="name"></input>
</template>
<script>
export default {
data() {
return {
inputName: '',
}
},
methods: {
applyName() {
// Notice the use of this pointer
let nameData = { name: this.inputName };
}
}
}
</script>
Look at the doc https://v2.vuejs.org/v2/guide/forms.html#v-model-with-Components
In the template, you are referring by name to data, computed or methods. In this case, it refers to data. When the input changes the name then the data is updated.
It is possible to use in a function referring to this.
<template>
<input v-model="name" class="name"></input>
</template>
<script>
export default {
data() {
return { name: '' }
},
methods: {
applyName() {
let nameData = this.name
}
}
}
</script>

Vue.js single file component 'name' not honored in consumer

Please pardon my syntax, I'm new to vue.js and may not be getting the terms correct.
I've got a single file component (SFC) named CreateTodo.vue. I've given it the name 'create-todo-item' (in the name property). When I import it in my app.vue file, I can only use the component if I use the markup <create-todo>. If I use <create-todo-item>, the component won't render on the page.
I've since learned that I can do what I want if I list the component in my app.vue in the format components: { 'create-todo-item': CreateTodo } instead of components: { CreateTodo }.
My question is this: is there any point to giving the component a name in the name property? It's not being honored in the consumer, and if I leave it empty, the app runs without error.
Also, am I correct in my belief that vue-loader is assigning the kebab-case element name for template use based on the PascalCase import statement?
Bad - component name property
Here's the code where I try to name the SFC (CreateTodo.vue)
<script>
export default {
name: 'create-todo-item',
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
The name as listed in the component is ignored by my App.vue. The html renders fine even though I have the element <create-todo> instead of <create-todo-item>:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo v-on:make-todo="addTodo"></create-todo>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
Good - don't use component name property at all
Here's my CreateTodo.vue without using the name property:
<script>
export default {
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
And here's my App.vue using the changed component:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo-item v-on:make-todo="addTodo"></create-todo-item>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
'create-todo-item': CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
First note that the .name property in a SFC module is mostly just a convenience for debugging. (It's also helpful for recursion.) Other than that, it doesn't really matter when you locally register the component in parent components.
As to the specific details, in the first example, you're using an ES2015 shorthand notation
components: {
TodoList,
CreateTodo,
},
is equivalent to
components: {
'TodoList': TodoList,
'CreateTodo': CreateTodo
},
so that the component that is imported as CreateTodo is given the name 'CreateTodo' which is equivalent to <create-todo>.
In the second example, you give the name explicitly by forgoing the shorthand notation
components: {
TodoList,
'create-todo-item': CreateTodo,
},
That's equivalent, btw to
components: {
TodoList,
'CreateTodoItem': CreateTodo,
},
So you can see, in that case, that you're giving the component the name 'CreateTodoItem' or, equivalently, <create-todo-item>