VueJS Duplicate components all updating at the same time - javascript

Might be a simple solution, but I'm currently not seeing it. I have an object that describes several configurations.
Object looks like this:
export const fieldSelectionDefault = {
cohort: {
currency_key: null,
salary_key: null,
timeframe_key: null
},
school: {
currency_key: null,
salary_key: null,
timeframe_key: null,
response_count_key: null,
},
}
export const cohortListFieldDefault = {
field_student: { ...fieldSelectionDefault },
field_alum_1: { ...fieldSelectionDefault },
field_alum_2: { ...fieldSelectionDefault },
field_alum_3: { ...fieldSelectionDefault },
}
Now, I have a parent component where I have a form. This form will list each field_* to have a <CohortFieldConfig /> component where we can input the values of the fieldSelectionDefault.
In the parent form, I add them like this:
<h5>Student</h5>
<CohortFieldConfig
:key="'settings.field_student'"
:disabled="settings.active_entities.student"
:selection-fields="settings.field_student"
#update-fields="(val) => test(val, 'stu')"
/>
<h5>Alumnus 1</h5>
<CohortFieldConfig
:key="'settings.field_alum_1'"
:disabled="settings.active_entities.alum_1"
:selection-fields="settings.field_alum_1"
#update-fields="(val) => test(val, 'alum')"
/>
CohortFieldConfig looks like this (example of one inputs, removed js imports):
<template>
<div>
<a-form-item label="Currency input">
<a-input
:disabled="!disabled"
placeholder="Select a currency form key"
v-model="objSelectionFields.cohort.currency_key"
/>
</a-form-item>
<FieldSelector
#select="val => (objSelectionFields.cohort.currency_key = val)"
:user="user"
:disabled="!disabled"
/>
</div>
</template>
<script>
export default {
name: 'CohortFieldConfig',
components: { FieldSelector },
props: {
selectionFields: {
type: [Object, null],
default: () => {
return { ...fieldSelectionDefault }
},
},
disabled: {
type: Boolean,
default: () => false,
},
},
data: function() {
return {
fieldSelectionDefault,
objSelectionFields: { ...this.selectionFields },
}
},
watch: {
objSelectionFields: {
handler(){
this.$emit('update-fields', this.objSelectionFields)
},
deep: true
}
},
methods: {
update() {
// not really used atm
this.$emit('update-fields', this.objSelectionFields)
},
},
}
</script>
When you type in the input, BOTH are updated at the same time. For student & alum_1.
The update-fields event is fired for both (same) components
Whats the reason? I've tried setting different key, doesn't work.
UPDATE
As pointed out in the comments, the issue was I was giving the same object. To correct this, I make a (deep) copy of the object as so:
export const cohortListFieldDefault = {
field_student: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_1: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_2: JSON.parse(JSON.stringify(fieldSelectionDefault)),
field_alum_3: JSON.parse(JSON.stringify(fieldSelectionDefault)),
}

Related

Keep vue component events after moving

I have the following vue component
<template>
<div class="box"
:data-target="dropAreaClass"
:class="{ 'js-draggable': isDraggable }"
:id="id"
:draggable="isDraggable"
#dragstart="dragStart"
#dragend="dragEnd">
{{ id }}
</div>
</template>
<script>
export default {
name: 'ActionBox',
props: {
dropAreaClass: {
default: 'js-droppable--any',
type: String,
},
id: {
default: null,
type: String,
required: true,
},
isDraggable: {
default: true,
type: Boolean,
},
},
data: () => ({
dropAreas: null,
}),
mounted() {
this.dropAreas = document.querySelectorAll(`.${this.dropAreaClass}`);
},
methods: {
dragEnd(event) {
this.dropAreas.forEach(dropArea => {
dropArea.classList.remove('drop');
});
event.currentTarget.classList.remove('dragging');
},
dragStart(event) {
this.dropAreas.forEach(dropArea => {
dropArea.classList.add('drop');
});
event.currentTarget.classList.add('dragging');
event.dataTransfer.setData('text', event.currentTarget.id);
},
},
};
</script>
This is a simple div which I can drag a drop into multiple columns in the parent component - once it is dropped in one of the target columns, the following function is fired to move the component to the column it is dropped in:
drop(event) {
const droppedElement = document.getElementById(event.dataTransfer.getData('text'));
if (event.currentTarget.classList.contains(droppedElement.dataset.target)) {
event.currentTarget.prepend(droppedElement);
event.currentTarget.classList.remove('drop');
}
},
This all works fine, however, once it is dropped, I can no longer drag the component to another column as it seems to have lost all it's event bindings. Is there a way to keep the events after dropping?

Vue.js: Passing props to data to use in v-model

I've been trying to pass props to data like this, as I saw on another post:
Child Component:
props: {
idInput: { type: String, required: false },
nameInput: { type: String, required: false },
},
data() {
return {
id: this.idInput,
name: this.nameInput,
}
}
So I can use it here:
<t-input v-model="name" type="text" />
Parent Component:
data() { return { game: {} } },
beforeCreated() { this.game = { name: "myName", id: "myID" }
<ChildComponent :name-input="game.name" :id-input="game.id" />
The problem is that "name" appears to be undefined, while if I do the same but changing "name" to "nameInput" it works, but I get the Vue error telling me not to use props like that. Any ideas?
Here is a functional example I created to demonstrate this case:
const comp = Vue.component('comp', {
template: '#myComp',
props: {
idInput: { type: String, required: false },
nameInput: { type: String, required: false },
},
data() {
return {
id: this.idInput,
name: this.nameInput,
}
}
});
new Vue({
el: "#myApp",
data () {
return {
game: {}
}
},
created() {
this.game = { name: 'myName', id: 'myID' };
},
components: { comp }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="myApp">
<comp :name-input="game.name" :id-input="game.id" />
{{game}}
</div>
<template id="myComp">
<div>
{{idInput}}
<br>
<input v-model="name" type="text" />
</div>
</template>
EDIT:
After checking the code, I think the problem is that you're setting the game atrribute in the data of the parent component on beforeCreated. Set it on created instead.
EDIT
OP found another way to do it. Instead of passing props one by one, pass just 1 prop with the object and use its attributes on the v-model.

vuejs: vue-select component not updating values

I am trying to use the vue-select component for a dropdown list. So far I have written.
<template>
<div>
<v-select label="name" key="id" :v-model="selected" :reduce="data => data.id" :options="items" #input="update()" />
</div>
</template>
<script>
export default {
props: {
initial: {
type: [String, Number],
default: 0,
},
api_call: {
type: String,
required: true,
},
},
data(){
return {
value: this.initial,
items: [],
}
},
computed: {
selected: {
get() {
return this.value;
},
set(val) {
return this.value = val;
}
},
},
methods:{
update() {
console.log('selected', this.selected, this.value);
this.$emit('input', this.selected);
},
getData: function(){
axios.get('/api/' + this.api_call)
.then(function (response) {
this.items = response.data;
}.bind(this));
},
},
created(){
this.getData();
}
}
The dropdown list populates as intended and selecting an Item inserts it in the input filed. The two problems I have are
Neither the value nor the selected variables change when something is selected.
I am also passing in an initial value which I would like to be selected as the default in the list.
Remove the binding sign : from v-model directive
<v-select label="name" key="id" v-model="selected" :reduce="data => data.id" :options="items" #input="update()" />
and init your value like :
data(vm){//vm refers to this
return {
value: vm.initial,
items: [],
}
},
or :
data(){
return {
value: null,
items: [],
}
},
mounted(){
this.value=this.initial
}

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.

click event on div and filter vuex data --vue.js

I am stuck at this very point: exporting filter function and using it in vuex store. No problem'till here. Now am trying to put #click event on divs. And when I click, for example. Audi the filter needs to show just "audi" And if I click "audi" again then it needs remove it from the filter.
Here is the sandbox: https://codesandbox.io/s/filtering-bzphi
filter.js
export const carFilter = car => allcars => {
if (car.length > 0) {
if (allcars.name.includes(car)) {
return true;
} else {
return false;
}
} else {
return true;
}
};
Store
export const store = new Vuex.Store({
state: {
cars: [
{ name: "AUDI" },
{ name: "BMW" },
{ name: "MERCEDES" },
{ name: "HONDA" },
{ name: "TOYOTA" }
],
carBrand: []
},
mutations: {
updateCarsFilter(state, carBrand) {
state.carBrand = carBrand;
}
},
getters: {
filteredCars: state => {
return state.cars.filter(carFilter(state.carBrand));
}
}
});
and App.js
<template>
<div id="app">
<div class="boxes" :key="index" v-for="(item, index) in cars">{{item.name}}</div>
<List/>
</div>
</template>
<script>
import List from "./List.vue";
export default {
name: "App",
components: {
List
},
computed: {
selectBrand: {
set(val) {
this.$store.commit("updateCarsFilter", val);
},
get() {
return this.$store.state.carBrand;
}
},
cars() {
return this.$store.getters.filteredCars;
}
}
};
</script>
I also created a sandbox for this. You can check it for better understanding. https://codesandbox.io/s/filtering-bzphi
In the store.js
changed the carBrand default to ''
added Mutation clearFilter
added Getter isActiveFilter
update
remove carBrand from state
replaced by selectedCars that is an array
removed mutation about carBrand
added mutation addCarSelection removeCarSelection
filteredCars return selectedCars array if contains cars, otherwise cars state
added isSelectedCar to check if a car is in the selection
carFilter function from filter.js is no longer needed.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
cars: [
{ name: "AUDI" },
{ name: "BMW" },
{ name: "MERCEDES" },
{ name: "HONDA" },
{ name: "TOYOTA" }
],
selectedCars: []
},
mutations: {
addCarSelection(state, car) {
state.selectedCars.push(car);
},
removeCarSelection(state, car) {
state.selectedCars = state.selectedCars.filter(r => r.name !== car.name);
}
},
getters: {
filteredCars: state => {
if (state.selectedCars.length !== 0) {
// There's selected cars, return filtered
return state.selectedCars;
} else {
return state.cars;
}
},
isSelectedCar: state => car => {
return state.selectedCars.some(r => r.name === car.name);
}
}
});
In the App.vue
added method filterCars (moved from computed property searchText)
added method clearFilter
update
removed filterCars and 'clearFilter' method and mapped new mutation and getters from store
methods: {
addCarSelection(car) {
this.$store.commit("addCarSelection", car);
},
removeCarSelection(car) {
this.$store.commit("removeCarSelection", car);
},
isSelectedCar(car) {
return this.$store.getters.isSelectedCar(car)
},
}
added isFilterActive() computed property
update
removed isFilterActive() and searchText from computed property
computed: {
cars() {
return this.$store.getters.filteredCars;
},
},
update
Changed the Template code to manage #click event to add car or remove car from selection
boxes always show cars available, if isSelectedCar toggle between add or remove function.
List show selected cars if presents otherwise the full car catalog.
<template>
<div id="app">
<div class="boxes" :key="index" v-for="(item, index) in cars">
<div
v-if="!isSelectedCar(item)"
style="cursor:pointer"
#click="addCarSelection(item)"
>{{item.name}}</div>
<div v-else style="cursor:pointer;" #click="removeCarSelection(item)">
{{item.name}}
<small>[x]</small>
</div>
</div>
<List/>
</div>
</template>
Updated version is available in this sandbox
https://codesandbox.io/s/filtering-3ej7d

Categories