Vue 3 component not rendering but show up on the elements tree - javascript

I'm new to vue and I was trying to change one of my components in to the new syntax to learn vue 3. The component was working just fine before the change so I must be missing something.
this is the mother component:
<template>
<div class="user-profile">
<div>
<div class="user-profile__user-panel">
<h1 class="user-profile__username">#{{ user.username }}</h1>
<div v-if="user.isAdmin" class="user-profile__admin-badge">Admin</div>
<div class="user-profile__follower-count">
<string><b>Followers: </b></string> {{ followers }}
</div>
</div>
<NewTwoot #create-twoot="createNewTwoot" />
</div>
<div class="user-profile__twoots-wraper">
<TwootItem
v-for="twoot in user.twoots"
:username="user.username"
:key="twoot.id"
:twoot="twoot"
#favorit="toggleFavorite(id)"
/>
</div>
</div>
</template>
<script>
import TwootItem from "./TwootItem.vue";
export default {
name: "UserProfile",
components: {
TwootItem,
},
data() {
return {
followers: 0,
user: {
id: 1,
username: "GabrielBG",
firstName: "Gabriel",
lastName: "Gutierrez",
email: "gbg#scienceiscoll.com",
isAdmin: true,
twoots: [
{
id: 2,
content: "This is my second twoot",
},
{
id: 3,
content: "This is my third twoot",
},
{
id: 1,
content: "This is my first twoot",
},
],
},
};
},
watch: {
followers(newFollowerCount, oldFollowerCount) {
if (oldFollowerCount > newFollowerCount) {
console.log("You lost a follower!");
} else {
console.log("You gained a follower!");
}
},
},
computed: {
fullName() {
return this.user.firstName + " " + this.user.lastName;
},
},
methods: {
followUser() {
this.followers++;
},
toggleFavorite(id) {
console.log("Toggling favorite for twoot with id: " + id);
},
createNewTwoot(newTwootContent) {
this.user.twoots.unshift({
id: this.user.twoots.length + 1,
content: newTwootContent,
});
},
},
mounted() {
this.followUser();
},
};
</script>
And this is the component that was refactored but now it does not render:
<template>
<form class="create-twoot" #submit.prevent="createNewTwoot">
<lable for="new-twoot"
><strong>New Twoot</strong> ({{ newTwootCharCount }}/180)
</lable>
<textarea id="new-twoot" rows="5" v-model="state.newTwootContent" />
<div class="create-twoot-type">
<lable for="twoot-type">
<strong>Twoot Type</strong>
</lable>
<select id="twoot-type" v-model="state.selectedTwootType">
<option
:value="option.value"
v-for="(option, index) in state.twootTypes"
:key="index"
>
{{ option.name }}
</option>
</select>
</div>
<button
type="submit"
:disabled="
newTwootContent.length === 0 ||
newTwootContent.length > 180 ||
newTwootType == 'draft'
"
>
Twoot
</button>
</form>
</template>
<script>
import { reactive, computed } from "vue";
export default {
name: "NewTwoot",
setup(_props, ctx) {
const state = reactive({
newTwootContent: "",
selectedTwootType: "instant",
twootTypes: [
{ value: "draft", name: "Draft" },
{ value: "instant", name: "Instant Twoot" },
],
});
const newTwootCharCount = computed(() => state.newTwootContent.length);
function createNewTwoot() {
ctx.emit("create-twoot", state.newTwootContent);
state.newTwootContent = "";
}
return {
state,
newTwootCharCount,
createNewTwoot,
};
},
};
</script>
I can see it on the elements tree but it show up as <newtwoot></newtwoot> as if it was empty.

I see two errors:
you only import TwootItem in the parent but not NewTwoot. This explains why it is not rendered properly.
You don't define the emit in the child component, look at my answer here:
vue 3 emit warning " Extraneous non-emits event listeners"
So importing the missing component import NewTwoot from "./NewTwoot.vue"; and adding it to the components should do the trick:
components: {
NewTwoot,
TwootItem,
}

Related

Vuex - updating data in store from child $emit

I have a Vue app that can either randomize a title and subtitle OR manually edit these two values through a custom input component. When a user decides to edit, their input should then display on save those results on the parent component.
I have the randomizer and child component emitting the updated headings working, but having a troubled time updating the parents and state to display the custom input title and subtitle on save and getting a "undefined" error for both title and subtitle when I placed console logs in updateTitleAndSubtitle() in the actions section of the store.
The objective of this code challenging is to return the new values to the store and be able to display the custom inputs while having the randomizer handy whenever a user decides to use that instead.
Any direction on what I'm doing wrong or missing would be much appreciated. I've been reading article after article around Vuex and Vue2 for 3 days now with 2 months of experience using Vue.
Custom Input Child Component:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
:updateTitle="updateTitle"
v-model="inputTitle"
/>
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle" :updateSubtitle="updateSubtitle"
v-model="inputSubtitle"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
updateTitle() {
console.log('updateTitle: ', this.title);
return this.title;
},
updateSubtitle() {
console.log('updateSubtitle: ', this.subtitle);
return this.subtitle;
},
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
this.$emit('input', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
this.$emit('input', subtitle);
},
},
},
};
</script>
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#update="v => onSave(v)"
/>
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '#/components/CustomInput.vue';
import { mapState, mapActions } from 'vuex';
export default {
name: 'Home',
components: {
CustomInput,
},
data() {
return {
editMode: false,
};
},
computed: {
...mapState(['title', 'subtitle']),
},
methods: {
...mapActions(['randomizeHeadings', 'updateHeadings']),
onEdit() {
this.editMode = true;
},
onCancel() {
this.editMode = false;
},
onSave(v) {
this.editMode = false;
this.title = v.title;
this.subtitle = v.subtitle;
this.updateTitleAndSubtitle(v);
},
},
mounted() {
this.randomizeHeadings();
},
};
Vuex Store:
import randomWords from 'random-words';
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }) {
const payload = {
title: this.title || null,
subtitle: this.subtitle || null,
};
commit('UPDATE_TITLE', payload);
commit('UPDATE_SUBTITLE', payload);
},
},
modules: {
},
});
I tested your code in my local development environment and find out that you need a lot of changes in your codes to work better. Here is the new vuex store code:
vuex store:
export default new Vuex.Store({
state: {
title: '',
subtitle: '',
},
mutations: {
UPDATE_TITLE(state, value) {
state.title = value;
},
UPDATE_SUBTITLE(state, value) {
state.subtitle = value;
},
},
actions: {
randomizeTitle({ commit }) {
const newTitle = randomWords();
commit('UPDATE_TITLE', newTitle);
},
randomizeSubtitle({ commit }) {
const newSubtitle = randomWords();
commit('UPDATE_SUBTITLE', newSubtitle);
},
randomizeTitleAndSubtitle({ dispatch }) {
dispatch('randomizeTitle');
dispatch('randomizeSubtitle');
},
updateTitleAndSubtitle({ commit }, inputUser) {
/* I changed the structure of this action to work correctly */
console.log(inputUser);
commit('UPDATE_TITLE', inputUser.title);
commit('UPDATE_SUBTITLE', inputUser.subtitle);
},
},
modules: {
},
});
Also here is the new Parent component code:
Parent component:
<template>
<main class="home-page page">
<div v-if="!editMode" class="display-information">
<div class="title">
<span class="bold">Title: </span>{{title}}
</div>
<div class="subtitle">
<span class="bold">Subtitle: </span>{{subtitle}}
</div>
<div class="controls">
<button id="randomize-button" class="control-button" #click="randomizeTitleAndSubtitle">
Randomize
</button>
<button id="edit-button" class="control-button" #click="onEdit">Edit</button>
</div>
</div>
<div v-else class="edit-controls">
<CustomInput
:title="title"
:subtitle="subtitle"
#titleEvent = "myFuncTitle"
#subTitleEvent = "myFuncSubTitle"
/>
<!--
I removed this part from your component.
#update="v => onSave(v)"
and also added custom events (titleEvent and subTitleEvent) to the component
-->
<div class="controls">
<button id="cancel-button" class="control-button" #click="onCancel">Cancel</button>
<button id="save-button" class="control-button" #click="onSave">Save</button>
</div>
</div>
</main>
</template>
<script>
// # is an alias to /src
import CustomInput from '../components/CustomInput.vue';
import { mapActions } from 'vuex';
export default {
name: 'Parent',
components: {
CustomInput,
},
data() {
return {
editMode: false,
/* defining new data for handling "cancel" button functionality */
temporaryTitle: "",
temporarySubTitle: ""
};
},
computed: {
/* defining setter and getter for each computed value separately */
title: {
// getter
get: function () {
return this.$store.state.title;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_TITLE', newValue);
}
},
subtitle: {
// getter
get: function () {
return this.$store.state.subtitle;
},
// setter
set: function (newValue) {
this.$store.commit('UPDATE_SUBTITLE', newValue);
}
},
},
methods: {
/* changing the name of actions according to the names defined in "store" */
...mapActions(['randomizeTitleAndSubtitle', 'updateTitleAndSubtitle']),
onEdit() {
this.editMode = true;
this.temporaryTitle = this.$store.state.title;
this.temporarySubTitle = this.$store.state.subtitle;
},
onCancel() {
this.editMode = false;
this.$store.commit('UPDATE_TITLE', this.temporaryTitle);
this.$store.commit('UPDATE_SUBTITLE', this.temporarySubTitle);
},
myFuncTitle(event) {
console.log(event);
/* we could not set values to "computed" properties, if we had not defined "set: function ..." for them above. */
this.title = event;
},
myFuncSubTitle(event) {
this.subtitle = event;
},
onSave(v) {
this.editMode = false;
console.log(v); /* "v" is not related to your data. notice the console */
// this.title = v.title;
// this.subtitle = v.subtitle;
const payload = {
title: this.title,
subtitle: this.subtitle,
};
this.updateTitleAndSubtitle(payload);
},
},
created() {
this.randomizeTitleAndSubtitle();
},
};
</script>
And finally here is the code of new Custom Input component:
Custom Input:
<template>
<div>
<label for="title">Edit Title: </label>
<input
type="text"
id="title"
v-model="inputTitle"
#input="$emit('titleEvent', $event.target.value)"
/>
<!-- emitting event like above code for each input -->
<label for="title">Edit Subtitle: </label>
<input
type="text"
id="subtitle"
v-model="inputSubtitle"
#input="$emit('subTitleEvent', $event.target.value)"
/>
</div>
</template>
<script>
export default {
name: 'CustomInput',
props: {
title: String,
subtitle: String,
},
computed: {
inputTitle: {
get() {
console.log('set title: ', this.title);
return this.title;
},
set(title) {
console.log('set title: ', title);
},
},
inputSubtitle: {
get() {
return this.subtitle;
},
set(subtitle) {
console.log('set subtitle: ', subtitle);
},
},
},
};
</script>
<style scoped>
</style>
I tried to comment some changes to the codes, but the main changes are related to changing the name of mapActions actions according to the names defined in "store" and also provide a setter for computed properties.
I suggest that you read more in vue and vuex documentations, especially the page that is related to custom events and computed setters and vuex actions, if you have problems with my codes.

VUE - prop binding for "disabled" not working on select field

I am trying to make a drop down "select" field disabled under certain conditions in my app.
I have successfully done this with buttons already using a prop (disableBtn) that i pass up from the root of my app to the button component I made.
I try to do the EXACT same thing on a select component and it refuses to pass the prop (disableOption) back into the child component, even though its passing back many other binded props that work fine and build the options out in the drop down correctly.
I am logging the values on screen right now and can see they are updating in the main app component, but its not passing that back to the child for some reason.
Where am I off here? My understanding is you store the values you want to change in data() in the app.vue, then create a prop for them in the child component and bind them in the HTML. This has been working fine in all my other use cases.
app.vue
<template>
<div class="container">
<img alt="logo" src="./assets/logo.png">
<Header title="Provider Manager" :custTZ="custTZ" :shippingState="shippingState" :patientID="patientID" :custName="custFullName" :custEmail="custEmail" :custPhone="custPhone" />
<div v-if="providerVisibility" class="providerInfo">
<Button #btn-click="providerPicked" id="first-avail" text="First Available" />
<br />
<Select #dd-select="providerPickedSelect" :disabled="disableOption" :selectName="selectName" :id="id" :eligibleProviders="eligibleProviders" :labelFor="labelFor" :labelText="labelText" />
{{ disableOption }}
</div>
<div v-if="providerSelected" >
<hr>
<br />
<h2>Provider: {{ chosenProvider }} </h2>
<br />
</div>
<div v-if="providerSelected" >
<BookingSlots #selectSlot="removeUnselected" :slots="slots" />
<br />
<Button #btn-click="bookMeeting" text="Confirm Request" />
</div>
</div>
</template>
<script>
import { ZOHO } from "./assets/ZohoEmbededAppSDK.min.js";
import Header from './components/Header'
import Button from './components/Button'
import BookingSlots from './components/BookingSlots'
import Select from './components/Select'
const axios = require('axios');
export default {
name: 'App',
components: {
Header,
Button,
BookingSlots,
Select
},
data() {
return{
slots: [],
providerVisibility: true,
providerSelected: false,
currentProvider: 'None Assigned',
chosenProvider: '',
custFullName: '',
custEmail: '',
custPhone: '',
shippingState: '',
patientID: '',
custTZ: '',
providerZID: '',
eligibleProviders: [],
disableBtn: false,
disableOption: true,
}
},
methods: {
removeUnselected(id) {
console.log('id', id)
this.slots = this.slots.filter((slot) => slot.id === id)
},
providerPicked(id) {
console.log("id" + id)
console.log("currentProvider",this.currentProvider)
//Toggle front end visibility
this.providerSelected = true;
this.providerVisibility = false;
if(id === "first-avail"){
// hit booking engine, get name of first available
console.log("FIRST AVAIL")
this.chosenProvider = "Need to Hit Booking App";
}
if(id === "current-provider"){
// hit zoho values and get contact assigned provider
console.log("CURRENT PROVIDER")
this.chosenProvider = this.currentProvider;
}
},
providerPickedSelect(id, selectValue) {
if(this.id === "provider-select"){
// Get values from our DB for the provider selected
console.log("Provider-Select")
this.providerSelected = true;
this.providerVisibility = false;
this.chosenProvider = selectValue;
}
},
bookMeeting() {
//Book the meeting
console.log("book meeting called")
}
},
created() {
//Hit zoho and get customer info back
ZOHO.embeddedApp.on("PageLoad",(data) =>
{
console.log(data);
//Custom Business logic goes here
let entity = data.Entity;
let recordID = data.EntityId[0];
ZOHO.CRM.API.getRecord({Entity:entity,RecordID:recordID})
.then((data) => {
console.log(data.data[0])
// Set values scraped from CRM Contact profile
if(data.data[0].provider !== null && data.data[0].provider !== "None Assigned" ){
this.currentProvider = data.data[0].provider.name;
this.providerZID = data.data[0].provider.id;
}else{
//need to disable button if no doc assigned
this.disableBtn = true;
}
this.custEmail = data.data[0].Email;
this.custFullName = data.data[0].Full_Name;
this.custPhone = data.data[0].Phone;
this.patientID = data.data[0].Patient_ID;
this.shippingState = data.data[0].Mailing_State;
this.custTZ = data.data[0].GTM;
// getEligibleProviders(this.shippingState);
var data = JSON.stringify({
"state":this.shippingState,
});
axios(config)
.then((res) => {
console.log(res.data)
//this.eligibleProviders = res.data;
if(this.eligibleProviders && !this.eligibleProviders.length){
console.log("empty array")
this.eligibleProviders = [{
first_name: "None Avilable in Svc. State",
last_name: ""
}
];
this.disableOption = true;
}else{
console.log("full array")
}
console.log(this.eligibleProviders)
})
.catch((e) => {
console.log(e);
});
});
});
ZOHO.embeddedApp.init();
this.slots = [
{
id: 1,
text: 'Time Slot 1',
providerFname: 'James',
providerLname: "Appleton"
},
{
id: 2,
text: 'Time Slot 2',
providerFname: 'James',
providerLname: "Johnson"
}
];
this.selectName = "provider-select";
this.id = "provider-select";
this.labelFor = "provider-select";
this.labelText = "Choose a Provider: ";
}
}
</script>
select.vue
<template>
<br />
<label :for="labelFor">{{ labelText }} {{ disableOption }}</label>
<select v-on:change="onSelect($event, id)" class="select" :name="selectName" :id="id" :disabled="disableOption" >
<option :value="'none'" selected disabled hidden >Select One</option>
<option :key="provider.id" v-for="provider in eligibleProviders" :value="provider.first_name + ' ' + provider.last_name" >{{ provider.first_name +" "+ provider.last_name }}</option>
</select>
<br /><br />
</template>
<script>
export default {
name: 'Select',
props: {
selectValue: String,
selectName: String,
id: String,
labelFor: String,
labelText: String,
eligibleProviders: Array,
disableOption: Boolean,
},
methods: {
onSelect($event, id) {
console.log($event.target.value)
this.$emit('dd-select', id, $event.target.value);
}
},
emits: ['dd-select']
}
</script>
button.vue
<template>
<button #click="onClick(id)" class="btn" :id="id" :disabled="disableBtn" >{{ text }}</button>
</template>
<script>
export default {
name: 'Button',
props: {
text: String,
id: String,
disableBtn: Boolean,
},
methods: {
onClick(id) {
this.$emit('btn-click', id);
}
}
}
</script>
in select.vue, the props says it wants "disableOption", but you're passing disabled="disableOption"
so you can try updating app.vue with:
<Select
#dd-select="providerPickedSelect"
:disable-option="disableOption"
:select-name="selectName"
:id="id"
:eligible-providers="eligibleProviders"
:label-for="labelFor"
:label-text="labelText"
/>

How to control multiple checkbox in Vue

I have a checkbox component in Vue:
<template>
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" v-model="checkbox">
</div>
</template>
<script>
export default {
data(){
return {
checkbox: false
};
},
};
</script>
So in the parent component I want to control these checkbox. So here is my parent component:
<div class="card">
<div class="card-header">
<CheckBox />
</div>
<div class="card-body" v-for="item in cart" :key="item.product.id">
<div class="checkbox-area">
<CheckBox />
</div>
</div>
So checkbox in card-body can be added when user clicks to add. So if a user clicks 3 times, 3 checkbox are being added inside of card-body. What I am trying to achieve is, as you see in card-header there is another checkbox, and when this checkbox is clicked, I want to check all the checkboxes inside card-body, and when it is unchecked in card-header, I want to unchcecked everything inside card-body.
So do you have any idea how to achieve this?
Thanks...
You can try like this :
Vue.component('checkbox', {
template: `
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" #change="getCheck" v-model="value">
</div>
`,
props: {
checked: {
type: Boolean,
default: false
}
},
data(){
return {
value: this.checked
};
},
methods: {
getCheck() {
this.$emit("set-checked", this.value)
}
},
watch: {
checked(){
this.value = this.checked
}
}
})
new Vue({
el: '#demo',
data(){
return {
all: false,
cart: [
{id: 1, check: false},
{id: 2, check: false},
{id: 3, check: true},
{id: 4, check: false}
]
};
},
watch: {
cart() {
this.cart.find(c => c.check === false) ? this.all = false : this.all = true
}
},
methods: {
checkAll(val) {
this.all = val
this.cart = this.cart.map(c => {
c.check = val
return c
})
},
checkItem(id) {
this.cart = this.cart.map(c => {
if(c.id === id) {
c.check = !c.check
}
return c
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div class="card">
<div class="card-header">
<p>All</p>
<checkbox :checked="all" #set-checked="checkAll" />
</div>
<br/>
<div class="card-body" v-for="item in cart" :key="item.id">
<div class="checkbox-area">
<checkbox :checked="item.check" #set-checked="checkItem(item.id)" />
</div>
</div>
</div>
</div>
First of all you need to add some props to your Component. than a wachter to emit a sync when the Value of the checkbox changes.
<template>
<div class="checkbox">
<input class="checkbox-input" name="input" type="checkbox" v-model="value">
</div>
</template>
<script>
export default {
props: {
checked: {
type: Boolean,
default: false
}
}
data(){
return {
value: this.checked
};
},
watch: {
value(){
this.$emit("update:checked", this.value)
}
}
};
</script>
on the cart you need to watch for theses changes an than you can check/uncheck all the items.
<template>
<div class="card">
<div class="card-header">
<CheckBox :checked.sync="global"/>
</div>
<div class="card-body" v-for="item in cart" :key="item.product.id">
<div class="checkbox-area">
<CheckBox :checked.sync="item"/>
</div>
</div>
</div>
</template>
<script>
export default {
data(){
return {
global: false,
cart: [
false,
false,
false
]
};
},
watch: {
global(){
for (let i = 0; i < this.cart.length; i++) {
this.chart[i] = this.global
}
}
}
};
</script>
I have not tested this code, but this should work...
Using checkbox input as custom components can be a little tricky see if this code can help you:
code sandbox
Vue.component('checkbox', {
template: `
<label>
<input type="checkbox" :value="value" v-model="deltaValue" />
{{ label }}
</label>
`,
name: "checkbox",
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
label: String,
value: [Object, Boolean, Array],
modelValue: {
type: [Array, Boolean],
default: () => [],
},
},
computed: {
deltaValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
},
});
new Vue({
el: '#app',
data() {
return {
areAllSelected: false,
checkedItems: [],
cart: [
{ id: 1, name: "tablet"},
{ id: 2, name: "phone"},
{ id: 3, name: "PC" },
],
};
},
watch: {
areAllSelected(areAllSelected) {
if (areAllSelected) {
this.checkedItems = [...this.cart];
return;
}
this.checkedItems = [];
},
checkedItems(items){
if (items.length === this.cart.length) {
this.areAllSelected = true;
return;
}
if (items.length === 0) {
this.areAllSelected = false;
}
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="card">
{{ checkedItems }}
<div class="card-header">
<checkbox label="check all" v-model="areAllSelected" />
</div>
<hr />
<div class="card-body" v-for="product in cart" :key="product.id">
<checkbox
:label="product.name"
:value="product"
v-model="checkedItems"
/>
</div>
</div>
</div>

Component not being initialized initially vuejs

In
https://codesandbox.io/s/rmh2n?file=/src/components/NestedDraggable.vue
, in:
<template>
<draggable class="dragArea" tag="ul" :list="tasks" :group="{ name: 'g1' }">
<li v-for="el in tasks" :key="el.name">
<generic-item :taskItem="el"> </generic-item>
<p>{{ el.name }}</p>
<nested-draggable :tasks="el.tasks" />
</li>
</draggable>
</template>
why isn't generic-item rendering el?
GenericItem:
<template>
<div v-if="taskItemLocal['itemSectionCategoryId']">
<p>
Section: {{taskItemLocal['itemSectionCategoryName']}}
</p>
</div>
<div v-else-if="taskItemLocal['itemSectionCategoryItemId']">
<p>
Item: {{taskItemLocal['itemSectionCategoryItemName']}}
</p>
</div>
<div v-else>
Not set
</div>
</template>
<script>
export default {
name: "generic-item",
props: {
taskItem: {
required: true,
type: Object,
default() {
return {};
},
},
},
components: {},
watch: {
taskItem: {
deep: true,
handler(val) {
this.taskItemLocal = Object.assign({}, val);
},
},
},
computed: {
getTaskItemLocal: {
get() {
if (!this.taskItemLocal) {
return Object.assign({}, this.taskItem);
}
return Object.values(this.taskItemLocal);
},
set(value) {
this.taskItemLocal = value;
},
},
},
data() {
return {
taskItemLocal: {
type: Object,
default: {},
},
};
},
};
</script>
<style scoped></style>
It looks like it isn't computing taskItemLocal. This component will have to modify props (they can't be used directly in the template). How can I "directly" copy props to a local variable and use it in the template "on the first run"?

How do I render a child component within an iframe in Vue?

So I want to show the user a preview of what an email will look like before it's sent out. To avoid styles from leaking from the parent page into the preview, I've decided to use an iframe. I want the preview to update in real time as the user enters form details.
How would I render a component within an iframe so that its props update automatically when the parent form is updated? This is the code I have so far:
this is the html:
<template>
<div id="confirmation">
<h2>Give a gift</h2>
<form #submit.prevent="checkout()">
<div class="date-section">
<label class="wide">Send</label>
<input type="radio" name="sendLater" v-model="sendLater" required :value="false">
<span>Now</span>
<input type="radio" name="sendLater" v-model="sendLater" required :value="true">
<span style="margin-right: 5px;">Later: </span>
<date-picker :disabled="!sendLater" v-model="date" lang="en" />
</div>
<div>
<label>Recipient Email</label>
<input type="email" class="custom-text" v-model="form.email" required>
</div>
<div>
<label>Recipient Name</label>
<input type="text" class="custom-text" v-model="form.name" required>
</div>
<div>
<label>Add a personal message</label>
<textarea v-model="form.message" />
</div>
<p class="error" v-if="error">Please enter a valid date.</p>
<div class="button-row">
<button class="trumpet-button" type="submit">Next</button>
<button class="trumpet-button gray ml10" type="button" #click="cancel()">Cancel</button>
</div>
</form>
<iframe id="preview-frame">
<preview-component :form="form" :sender-email="senderEmail" :term="term" />
</iframe>
</div>
</template>
here is the js (note: PreviewComponent is the actual preview that will be rendered in the iframe):
export default {
name: 'ConfirmationComponent',
components: {
DatePicker,
PreviewComponent
},
props: {
term: {
required: true,
type: Object
}
},
data() {
return {
form: {
name: null,
email: null,
message: null,
date: null
},
date: null,
sendLater: false,
error: false
}
},
computed: {
senderEmail() {
// utils comes from a separate file called utils.js
return utils.user.email || ''
}
},
watch: {
'form.name'(val) {
this.renderIframe()
},
'form.email'(val) {
this.renderIframe()
}
},
methods: {
renderIframe() {
if (this.form.name != null && this.form.email != null) {
console.log('rendering iframe')
// not sure what to do here......
}
}
}
}
I've tried all sorts of things but what seems to be the hardest is setting the props of the preview-component properly. Any help you all can give would be appreciated.
So as posted in one of the comments, Vuex works perfectly for this.
I also ended up creating a custom "IFrame" component that renders whatever you have inside its slot in an iframe.
Here is my Vuex store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
form: {
name: null,
email: null,
message: null
},
senderEmail: null,
term: null,
styles: null
},
mutations: {
updateForm(state, form) {
state.form = form
},
updateEmail(state, email) {
state.senderEmail = email
},
updateTerm(state, term) {
state.term = term
},
stylesChange(state, styles) {
state.styles = styles
}
}
})
my IFrame component:
import Vue from 'vue'
import { store } from '../../store'
export default {
name: 'IFrame',
data() {
return {
iApp: null,
}
},
computed: {
styles() {
return this.$store.state.styles
}
},
render(h) {
return h('iframe', {
on: {
load: this.renderChildren
}
})
},
watch: {
styles(val) {
const head = this.$el.contentDocument.head
$(head).html(val)
}
},
beforeUpdate() {
this.iApp.children = Object.freeze(this.$slots.default)
},
methods: {
renderChildren() {
const children = this.$slots.default
const body = this.$el.contentDocument.body
const el = document.createElement('div') // we will mount or nested app to this element
body.appendChild(el)
const iApp = new Vue({
name: 'iApp',
store,
data() {
return {
children: Object.freeze(children)
}
},
render(h) {
return h('div', this.children)
}
})
iApp.$mount(el)
this.iApp = iApp
}
}
}
finally here is how data is passed to the PreviewComponent from the ConfirmationComponent:
export default {
name: 'ConfirmationComponent',
mounted() {
this.$store.commit('updateEmail', this.senderEmail)
this.$store.commit('updateTerm', this.term)
},
watch: {
'form.name'(val) {
this.updateIframe()
},
'form.email'(val) {
this.updateIframe()
}
},
methods: {
updateIframe() {
this.$store.commit('updateForm', this.form)
}
}
}
then lastly the actual PreviewComponent:
import styles from '../../../templates/styles'
export default {
name: 'PreviewComponent',
mounted() {
this.$store.commit('stylesChange', styles)
},
computed: {
redemption_url() {
return `${window.config.stitcher_website}/gift?code=`
},
custom_message() {
if (this.form.message) {
let div = document.createElement('div')
div.innerHTML = this.form.message
let text = div.textContent || div.innerText || ''
return text.replace(/(?:\r\n|\r|\n)/g, '<br>')
}
return null
},
form() {
return this.$store.state.form
},
term() {
return this.$store.state.term
},
senderEmail() {
return this.$store.state.senderEmail
}
}
}
hopefully this will help somebody.

Categories