AlpineJs dynamically changing nested component - javascript

What are the options to keep nested components reactive.
I have very little experience with alpineJs and need some advice.
There is a filter in which items are dynamically changed during the search.
I bypass these elements with x-for.
This is where I run into a problem. For example, for the item.range position, I need to use a different library (for example, Ion.RangeSlider), the problem is that initialization occurs only once and, accordingly, with subsequent changes in items, the item.range position that is in x-for will not change and remain static .
I see several options here:
It is trivial to wrap the rendering of this position in the isLoading condition, and on each load, the initialization will occur anew and, accordingly, the actual data will be displayed. But this is clearly not the right decision, for a number of reasons.
<template x-if="item.hasRange && !isLoading" >
<div x-data="item(index)">
<input x-ref="range" type="text" />
<template x-data="range($refs.range)"></template>
</div>
</template>
Implement a wrapper on top of this library with its api methods, and try to pass data already in them.
Below is a slightly truncated version of the code.
document.addEventListener('alpine:init', () => {
Alpine.data('filter', () => ({
items: [],
item_: (index) => {
return {
item: this.items[index];
range: () => {
return {
range: null,
init() {
this.range = range(element) {
$(element).ionRangeSlider({
type: "double",
//...
onFinish: (data) => {},//...
});
}
},
update: () => {
this.range.update(/*...*/);
},
//... other api
};
}
}
}
})
})
Implement an event listener in a component that implements rendering using an external library
I would be very grateful for an example or information on how best to implement such dynamism.

You can try using x-show instead of x-if, with this you can then manually hide or show the component using CSS.
<div x-data="{ items: [], activeIndex: null }">
<template x-for="(item, index) in items">
<div x-show="activeIndex === index" x-bind:key="index">
<input x-ref="range" type="text" x-bind:value="item.range" x-on:input="item.range = $event.target.value" />
<template x-init="initRange($refs.range, item.range)">
<!-- initialization code here -->
</template>
</div>
</template>
</div>

The best way to include an external library is to write a custom directive or a custom magic component. This way we can have an Alpine.js-like API for the external library with two-way data binding and we can reuse our component easily elsewhere.
Here I show a minimal example that implements a double RangeSlider with min/max modifiers as e.g. x-range.min.0.max.10="item.range" custom directive. I added further explanations as inline comments, so see the live example and source code:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs#3.x.x/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<div x-data="{items: [
{id: 1, range: {from: 2, to: 3, updating: false}},
{id: 2, range: {from: 3, to: 4, updating: false}},
{id: 3, range: {from: 4, to: 5, updating: false}},
]}">
<template x-for="item in items" :key="item.id">
<div style="margin-bottom: 20px;">
<input type="text" x-range.min.0.max.10="item.range" />
<div><b x-text="`Item #${item.id} Range: ${item.range.from}-${item.range.to}`"></b>,
Set manually: from: <input type="number" x-model="item.range.from" min="0" max="10" size="1">
to: <input type="number" x-model="item.range.to" min="0" max="10" size="1">
<span x-show="item.range.updating">Changing...</span>
</div>
</div>
</template>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('range', (el, { expression, modifiers }, { evaluate, evaluateLater, effect }) => {
// Set RangeSlider config from directive modifiers
const config = {
type: "double",
onChange(data) {
// Disable Alpine.js->RangeSlider data flow during RangeSlider change event
evaluate(`${expression}.updating = true;`)
// Update Alpine.js data after range change
evaluate(`${expression}.from = ${data.from}; ${expression}.to = ${data.to}`)
},
onFinish(data) {
// Enable Alpine.js->RangeSlider data flow again after a RangeSlider change event
evaluate(`${expression}.updating = false;`)
},
}
// Source: https://github.com/ryangjchandler/alpine-tooltip
const getModifierArgument = (modifier) => {
return modifiers[modifiers.indexOf(modifier) + 1]
}
if (modifiers.includes('min')) {
config.min = getModifierArgument('min')
}
if (modifiers.includes('max')) {
config.max = getModifierArgument('max')
}
if (!el.__x_range) {
// Init RangeSlider on the element
el.__x_range = $(el).ionRangeSlider(config)
// Set RangeSlider data for updating it from Alpine.js
el.__x_range_data = el.__x_range.data("ionRangeSlider")
}
// Set Alpine.js data binding that updates RangeSlider
let rangeData = evaluateLater(expression)
effect(() => {
// Update RangeSlider data from Alpine.js data when RangeSlider is inactive
rangeData(val => {
if (!val.updating) el.__x_range_data.update({ from: val.from, to: val.to })
})
})
})
})
</script>

Related

Vue 2: Best way to use v-model on custom checkboxes

Say, I have a custom checkbox (input) component, and I plan to use it with v-model in the component that is supposed to use it.
What I'm doing is, to support multiple checkboxes, I create an array state, and upon checking/unchecking, I add/remove the checkbox's value from the array.
Is the best approach. I'm using Vue2.
// CheckBox.vue
<script>
export default {
model: { // Customising the option for model
prop: "checked", // Array of checked values
event: "check" // Firing this event with new array of checked values
},
props: ["name", "label", "value", "checked"],
emits: ["check"]
};
</script>
<template>
<label :for="`input-${name}`">
{{ label }}
<input :id="`input-${name}`" :value="value" type="checkbox" :checked="checked.includes(value)"
#change="$emit('check', $event.target.checked ? [...checked, value] : checked.filter((val) => val !== value))" /> // I'm just appending/removing value from the array state. Is this the best approach?
</label>
</template>
// SomeComponent.vue
<script>
import CheckBox from "../atoms/checkbox.vue";
export default {
data: () => ({
elements: [1, 2, 3, 4, 5, 6, 7, 8, 9],
checked: [], // Storing the checked values here
}),
watch: {
checked: function (newSelected) { console.log(newSelected); }
},
components: {
CheckBox
}
}
</script>
<template>
<ul>
<li v-for="num in elements">
<CheckBox :name="num" :label="num" :value="num" v-model="checked" />
</li>
</ul>
</template>

How to display dynamically array on input value change?

I have a div in which I am displaying components from array of objects. Above the div I have an input[type=text] which will be filtering the displayed data depending what I will insert. It looks like that:
<div class="filters">
<input type="search" placeholder="Szukaj specjalisty"
:onchange="filterAllSpecialists">
<FilterData/>
</div>
<div class="all-fields">
<SpecialistPreview class="specialist-preview"
v-for="(talk, index) in newTalks"
:key="index"
:name="talk.name"
:speciality="talk.speciality"
:hourly-price="talk.hourlyPrice"
:half-hour-price="talk.halfHourPrice"
:avatar="talk.avatar"
:rating="talk.rating"
:is-available="talk.isAvailable"
>{{ talk }}
</SpecialistPreview>
</div>
I have created a method that takes the value of input and compares with the name from newTalks array:
<script>
export default {
data() {
return {
talks: [
{
name: 'Małgorzata Karwacka-Lewandowska',
speciality: 'Coach',
hourlyPrice: 150,
halfHourPrice: 80,
avatar: 'patrycja.jpg',
rating: 2.5,
isAvailable: true
},
... more objects
],
newTalks: []
}
},
methods: {
filterAllSpecialists(e) {
let input = e.target.value;
if (input !== '') {
this.newTalks = this.talks.filter(el =>
el.name.toLowerCase().includes(input.toLowerCase()))
console.log(this.newTalks);
return this.newTalks;
} else {
return this.newTalks;
}
}
},
mounted() {
this.newTalks = this.talks;
}
}
All I get is Uncaught SyntaxError: Function statements require a function name in the console
Since nobody answered and I have spent more time figuring out what was wrong with my code I found out that the first mistake I did was instead of using # I used : before onchange (I decided that for my purpose I needed #keyup instead of #change). Also there was an easier way of targeting the input value - simply by adding a v-model='allSpecialistInput.
So now in the template it should look like that:
<div class="filters">
<input type="search" placeholder="Szukaj specjalisty" v-model="allSpecialistsInput"
#keyup="filterAllSpecialists">
<FilterData/>
</div>
<div class="all-fields" v-cloak>
<SpecialistPreview class="specialist-preview"
v-for="(coach, index) in newCoaches"
:key="index"
:name="coach.name"
:speciality="coach.speciality"
:hourly-price="coach.hourlyPrice"
:half-hour-price="coach.halfHourPrice"
:avatar="coach.avatar"
:rating="coach.rating"
:is-available="coach.isAvailable"
>{{ coach }}
</SpecialistPreview>
</div>
In the methods all I needed to change was the first line in the filterAllSpecialists function like that:
filterAllSpecialists() {
let input = this.allSpecialistsInput; <--- here
if (input !== '') {
this.newCoaches = this.coaches.filter(el => el.name.includes(input.toLowerCase()))
return this.newCoaches;
} else {
return this.newCoaches;
}
},

Dynamics inputs, the v-model update all values in v-for

I try this following code with Vue.js 2:
<div id="app">
<div v-for="(item, index) in items">
<div>
<input type="text" v-model="items[index].message">
<input type="text" v-model="items[index].surface">
</div>
</div>
<button #click="addNewfield">Add</button>
</div>
var app = new Vue({
el: '#app',
data: {
item: {
message: 'test',
surface: 45
},
items: [],
},
mounted() {
this.items.push(this.item)
},
methods: {
addNewfield() {
this.items.push(this.item);
}
}
})
The goal is to create news input when user click on Add button. I tried different ways like :
<input type="text" v-model="item.message">
But it doesn't work. If you write in "message" input, all "message" inputs will be updated.
How can I only updated the concerned value ?
Thanks for help !
This is happening because objects in Javascript are stored by reference. This means that every time you push this.item onto the array, it's adding a reference to the exact same object as the last.
You can avoid this by creating a new object each time:
methods: {
addNewfield() {
const obj = {
message: 'test',
surface: 45
}
this.items.push(obj);
}
}
Another option would be to clone the original object each time like:
methods: {
addNewfield() {
const clone = Object.assign({}, this.item);
this.items.push(clone);
}
}

Why is the Vue.js input value not updating?

I have a Vue.js text-input component like the following:
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: this.value
};
},
watch: {
inputValue: function () {
eventBus.$emit('inputChanged', {
type: 'text',
name: this.name,
value: this.inputValue
});
}
}
};
</script>
And I am using that text-input in another component as follows:
<ul>
<li v-for="row in rows" :key="row.id">
<text-input :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
Then, within the JS of the component using text-input, I have code like the following for removing li rows:
this.rows = this.rows.filter((row, i) => i !== idx);
The filter method is properly removing the row that has an index of idx from the rows array, and in the parent component, I can confirm that the row is indeed gone, however, if I have, for example, two rows, the first with a value of 1 and the second with a value of 2, and then I delete the first row, even though the remaining row has a value of 2, I am still seeing 1 in the text input.
Why? I don't understand why Vue.js is not updating the value of the text input, even though the value of value is clearly changing from 1 to 2, and I can confirm that in the parent component.
Maybe I'm just not understanding how Vue.js and v-model work, but it seems like the value of the text input should update. Any advice/explanation would be greatly appreciated. Thank you.
You cannot mutate values between components like that.
Here is a sample snippet on how to properly pass values back and forth. You will need to use computed setter/getter. Added a button to change the value and reflect it back to the instance. It works for both directions.
<template>
<div>
<input type="text" :id="name" v-model="inputValue" />
<button #click="inputValue='value2'">click</button>
</div>
</template>
<script>
export default {
props: ['name', 'value'],
computed: {
inputValue: {
get() {
return this.value;
},
set(val) {
this.$emit('updated', val);
}
}
}
}
</script>
Notice that the "#updated" event updates back the local variable with the updated value:
<text-input :name="row.name" :value="row.value" #updated="item=>row.value=item"></text-input>
From your code you are trying to listen to changes.. in v-model data..
// Your Vue components
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
};
</script>
If You really want to listen for changes..
<ul>
<li v-for="row in rows" :key="row.id">
<text-input #keyup="_keyUp" :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
in your component file
<template>...</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
methods : {
_keyUp : () => {// handle events here}
};
</script>
check here for events on input here
To bind value from props..
get the props value, then assign it to 'inputValue' variable
it will reflect in tthe input element

Input field unable to change model when component and model are dynamically created

I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>

Categories