Replace tag dynamically returns the object instead of the contents - vue.js

I'm building an chat client and I want to scan the messages for a specific tag, in this case [item:42]
I'm passing the messages one by one to the following component:
<script>
import ChatItem from './ChatItem'
export default {
props :[
'chat'
],
name: 'chat-parser',
data() {
return {
testData: []
}
},
methods : {
parseMessage(msg, createElement){
const regex = /(?:\[\[item:([0-9]+)\]\])+/gm;
let m;
while ((m = regex.exec(msg)) !== null) {
msg = msg.replace(m[0],
createElement(ChatItem, {
props : {
"id" : m[1],
},
}))
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return msg
},
},
render(createElement) {
let user = "";
let msg = this.parseMessage(this.$props.chat.Message, createElement)
return createElement(
'div',
{
},
[
// "hello",// createElement("render function")
createElement('span', '['+ this.$props.chat.Time+'] '),
user,
msg,
]
)
}
};
</script>
I thought passing createElement to the parseMessage method would be a good idea, but it itsn't working properly as it replaces the tag with [object object]
The chatItem looks like this :
<template>
<div>
<span v-model="item">chatITem : {{ id }}</span>
</div>
</template>
<script>
export default {
data: function () {
return {
item : [],
}
},
props :['id'],
created() {
// this.getItem()
},
methods: {
getItem: function(){
obj.item = ["id" : "42", "name": "some name"]
},
},
}
</script>
Example :
if the message looks like this : what about [item:42] OR [item:24] both need to be replaced with the chatItem component

While you can do it using a render function that isn't really necessary if you just parse the text into a format that can be consumed by the template.
In this case I've kept the parser very primitive. It yields an array of values. If a value is a string then the template just dumps it out. If the value is a number it's assumed to be the number pulled out of [item:24] and passed to a <chat-item>. I've used a dummy version of <chat-item> that just outputs the number in a <strong> tag.
new Vue({
el: '#app',
components: {
ChatItem: {
props: ['id'],
template: '<strong>{{ id }}</strong>'
}
},
data () {
return {
text: 'Some text with [item:24] and [item:42]'
}
},
computed: {
richText () {
const text = this.text
// The parentheses ensure that split doesn't throw anything away
const re = /(\[item:\d+\])/g
// The filter gets rid of any empty strings
const parts = text.split(re).filter(item => item)
return parts.map(part => {
if (part.match(re)) {
// This just converts '[item:24]' to the number 24
return +part.slice(6, -1)
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="part in richText">
<chat-item v-if="typeof part === 'number'" :id="part"></chat-item>
<template v-else>{{ part }}</template>
</template>
</div>
If I were going to do it with a render function I'd do it pretty much the same way, just replacing the template with a render function.
If the text parsing requirements were a little more complicated then I wouldn't just return strings and numbers. Instead I'd use objects to describe each part. The core ideas remain the same though.

Related

Vuex: How can I pass a variable to a getter?

In a vue app I get an elementId from the URL to pass in to a vuex getter:
<template>
<p>elementId: {{ elementId }}</p>
<p>loadedElement: {{ loadedElement }}</p>
</template>
<script>
export default {
data() {
return {
elementId: this.$route.params.id,
};
},
computed: {
loadedElement() {
return this.$store.getters.getElementById(this.elementId);
},
},
};
</script>
Getter:
getElementById: (state) => (id) => {
return state.elements.find(ele => ele.id === id)
},
Right now the page gets rendered like this:
However, when hardcode an elementId, it works:
<template>
<p>elementId: {{ elementId }}</p>
<p>loadedElement: {{ loadedElement }}</p>
</template>
<script>
export default {
data() {
return {
elementId: 3,
};
},
computed: {
loadedElement() {
return this.$store.getters.getElementById(this.elementId);
},
},
};
</script>
I don't understand what I'm doing wrong since getting the elementId from the route seems to work, but it is not passed into the getter function.
What am I doing wrong?
Most likely this.$route.params.elementId is a string but your element IDs are numbers. Using === to compare a string and number will not match.
Try using == instead, or converting this.$route.params.elementId to a number:
data() {
return {
elementId: +this.$route.params.id,
};
},
(Of course, maybe you want to do more error checking as part of this.)

Vue.js Send an index with #input event

Vue version : 3.1.1
Hey guys,
I'm working with dynamic Creation Component, which means a user can add whatever of component he wants.I create it base on this documentation dynamic component creation.
And I use this component vue image uploader.
I need to send an index when the user wants to upload the image, like this :
<div v-for="(line, index) in lines" v-bind:key="index">
{{index}}//if i log the index its 0,1,2,3 and its ok
...
<image-uploader
:preview="true"
:class-name="['fileinput', { 'fileinput--loaded': line.hasImage }]"
:capture="false"
:debug="0"
:auto-rotate="true"
output-format="blob"
accept="image/*"
#input="setImage(output , index)"
:ref="'fileUpload'+index"
>
...
And the setImage funciton :
setImage: function(output,index) {
console.log(index);
console.log(output);
return ;
this.lines[index].hasImage = true;
this.lines[index].image = output;
let formData = new FormData();
formData.append("file", output);
Ax.post(upload_route, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
// upload successful
})
.catch(error => console.log(error));
}
And the log result is:
The index always is 0 :(
How can i send an index when i want to upload it?
I read this passing event and index and test it but it's not working on component.
Because This is a custom event not a DOM event.
what should I do?
thanks.
Because you're actually passing the return value of setImage to the #input, not the method.
You can't just add extra parameters to setImage, as ImageUploader component just emit an image to the setImage. If you need to add extra parameters to that method, you need to create custom element that wrap ImageUploader.
It's something like this:
ImageUpload.vue
<template>
<image-uploader
:debug="0"
:autoRotate="true"
outputFormat="blob"
:preview="true"
:className="['fileinput', { 'fileinput--loaded' : hasImage }]"
:capture="false"
accept="image/*"
doNotResize="['gif', 'svg']"
#input="setImage"
v-on="listeners" />
</template>
<script>
export default {
props: {
index: {
required: true,
type: Number
}
},
data() {
return {
hasImage: false,
image: null
};
},
computed: {
listeners() {
const listeners = { ...this.$listeners };
const customs = ["input"];
customs.forEach(name => {
if (listeners.hasOwnProperty(name)) {
delete listeners[name];
}
});
return listeners;
}
},
methods: {
setImage(image) {
this.hasImage = true;
this.image = image;
this.$emit("input", this.index, image); // here, we emit two params, as index for the first argument, and the image at the second argument
}
}
};
</script>
Then, you can use that component something like this:
<template>
<div class="container">
<div v-for="(line, index) in lines" :key="index">
<image-upload :index="index" #input="setImage"/>
</div>
</div>
</template>
<script>
import ImageUpload from "./ImageUpload";
export default {
components: {
ImageUpload
},
data() {
return {
lines: ["1", "2", "3", "4"]
};
},
methods: {
setImage(index, image) {
console.log("Result", index, image);
}
}
};
</script>
See the working example: https://codesandbox.io/s/vue-template-ccn0e
Just use $event like this...
#input="setImage($event, index)"
...and you're done!

Dynamically apply search filter on js data using vuejs

I have a array list which contains column names. I want to write a search filter which will dynamically pick the value from the array and display all the rows and column. Search Filter should be with array contains column names.
Column name can be set dynamically
I have written code like below
Inside script tag
Its not working.. udata value is comming as array
export default {
el: '#apicall',
mixins: [Vue2Filters.mixin],
data()
{
return {
toggle: false,
userData: [],
search: "",
apidata: ['Id', 'Name', 'Version'],
currentvalue: '',
}
computed:
{
filteruserData: function(){
var self = this;
var list =[];
return this.userData.filter((udata) => {
for(var i in self.apidata)
{
list.push(udata[self.apidata[i]])
}
return list.toLowerCase().indexOf(self.search.toLowerCase()) != -1
});
},
I didn't really get exactly what you need. From what I understood you need to search for column name in apidata array using a filter. If your search filter find an element of your apidata array, you want to diplay the row values corresponding to column name your search filter found.
From elements you gave in your post, I don't understand what value you need to diplay and where are they supposed to come from.
Anyway, here is my try to help you.
Lets suppose that you have a data object that contains the row value to display.
The code below search inside the apidata array and diplay the value of the corrsponding attribute of the data object if anything has been found.
App.vue
<template>
<div id="app"><searchCompoenent :data="data"></searchCompoenent></div>
</template>
<script>
import searchCompoenent from "./components/searchComponent.vue";
export default {
name: "App",
data() {
return {
data: {
Id: [1, 9, 2, 3, 4, 5],
Name: ["name1", "name2"],
Version: ["2.0", "1.0.0", "1.1-beta"]
}
};
},
components: {
searchCompoenent
}
};
</script>
searchComponent.vue
<template>
<div>
<input v-model="search" placeholder="Search..." type="text" />
<button type="button" #click="searchColumn">Search!</button>
</div>
</template>
<script>
export default {
data() {
return {
search: "",
apidata: ["Id", "Name", "Version"]
};
},
props: ["data"],
methods: {
searchColumn() {
let result = this.apidata.find(element => {
return element === this.search;
});
if (result !== undefined) {
console.log(result);
console.log(this.data);
}
}
}
};
</script>

Filtering a list of objects in Vue without altering the original data

I am diving into Vue for the first time and trying to make a simple filter component that takes a data object from an API and filters it.
The code below works but i cant find a way to "reset" the filter without doing another API call, making me think im approaching this wrong.
Is a Show/hide in the DOM better than altering the data object?
HTML
<button v-on:click="filterCats('Print')">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
Javascript
export default {
data() {
return {
assets: {}
}
},
methods: {
filterCats: function (cat) {
var items = this.assets
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === cat)) {
result[key] = item
}
})
this.assets = result
}
},
computed: {
filteredData: function () {
return this.assets
}
},
}
Is a Show/hide in the DOM better than altering the data object?
Not at all. Altering the data is the "Vue way".
You don't need to modify assets to filter it.
The recommended way of doing that is using a computed property: you would create a filteredData computed property that depends on the cat data property. Whenever you change the value of cat, the filteredData will be recalculated automatically (filtering this.assets using the current content of cat).
Something like below:
new Vue({
el: '#app',
data() {
return {
cat: null,
assets: {
one: {cat_names: ['Print'], title: {rendered: 'one'}},
two: {cat_names: ['Two'], title: {rendered: 'two'}},
three: {cat_names: ['Three'], title: {rendered: 'three'}}
}
}
},
computed: {
filteredData: function () {
if (this.cat == null) { return this.assets; } // no filtering
var items = this.assets;
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === this.cat)) {
result[key] = item
}
})
return result;
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="cat = 'Print'">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
</div>

Vue : Limit characters in text area input, truncate filter?

<textarea name="" id="" cols="30" rows="10" v-model="$store.state.user.giftMessage | truncate 150"></textarea>
I tried creating a custom filter :
filters: {
truncate(text, stop, clamp) {
return text.slice(0, stop) + (stop < text.length ? clamp || '...' : '')
}
}
but that didn't broke the build when I put it on the v-model for the input...
Any advice?
This is one of those cases, where you really want to use a component.
Here is an example component that renders a textarea and limits the amount of text.
Please note: this is not a production ready, handle all the corner cases component. It is intended as an example.
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
This component implements v-model and only emits a change to the data if the length of the text is less than the specified max. It does this by listening to keydown and preventing the default action (typing a character) if the length of the text is equal to or more than the allowed max.
console.clear()
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
data:{
text: ""
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<limited-textarea v-model="text"
:max="10"
cols="30"
rows="10">
</limited-textarea>
</div>
Another issue with the code in the question is Vuex will not allow you set a state value directly; you have to do it through a mutation. That said, there should be a Vuex mutation that accepts the new value and sets it, and the code should commit the mutation.
mutations: {
setGiftMessage(state, message) {
state.user.giftMessage = message
}
}
And in your Vue:
computed:{
giftMessage:{
get(){return this.$store.state.user.giftMessage},
set(v) {this.$store.commit("setGiftMessage", v)}
}
}
Technically the code should be using a getter to get the user (and it's giftMessage), but this should work. In the template you would use:
<limited-textarea cols="30" rows="10" v-model="giftMessage"></limited-textarea>
Here is a complete example using Vuex.
console.clear()
const store = new Vuex.Store({
state:{
user:{
giftMessage: "test"
}
},
getters:{
giftMessage(state){
return state.user.giftMessage
}
},
mutations:{
setGiftMessage(state, message){
state.user.giftMessage = message
}
}
})
Vue.component("limited-textarea", {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
template: `
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
`,
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
if (evt.keyCode >= 48 && evt.keyCode <= 90) {
evt.preventDefault()
return
}
}
}
}
})
new Vue({
el: "#app",
store,
computed:{
giftMessage:{
get(){ return this.$store.getters.giftMessage},
set(v){ this.$store.commit("setGiftMessage", v)}
}
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.4.0/vuex.js"></script>
<div id="app">
<limited-textarea v-model="giftMessage"
:max="10"
cols="30"
rows="10">
</limited-textarea>
Message: {{giftMessage}}
</div>
Sorry to break in. Was looking for a solution. Looked at all of them.
For me they look too complicated. I'm always looking for symplicity.
Therefor I like the answer of #Даниил Пронин. But it has the by #J. Rambo noted potential problem.
To stay as close as possible to the native html textelement. The solution I came up with is:
Vue Template
<textarea v-model="value" #input="assertMaxChars()">
JavaScript
let app = new Vue({
el: '#app',
data: {
value: 'Vue is working!',
maxLengthInCars: 25
},
methods: {
assertMaxChars: function () {
if (this.value.length >= this.maxLengthInCars) {
this.value = this.value.substring(0,this.maxLengthInCars);
}
}
}
})
Here is a REPL link: https://repl.it/#tsboh/LimitedCharsInTextarea
The upside I see is:
the element is as close as possible to the native element
simple code
textarea keeps focus
delete still works
works with pasting text as well
Anyway
happy coding
While I agree with the selected answer. You can also easily prevent the length using a keydown event handler.
Vue Template
<input type="text" #keydown="limit( $event, 'myModel', 3)" v-model="myModel" />
JavaScript
export default {
name: 'SomeComponent',
data () {
return {
myModel: ''
};
},
methods: {
limit( event, dataProp, limit ) {
if ( this[dataProp].length >= limit ) {
event.preventDefault();
}
}
}
}
Doing this way, you can also use regular expression to event prevent the type of keys accepted. For instance, if you only wanted to accept numeric values you can do the following.
methods: {
numeric( event, dataProp, limit ) {
if ( !/[0-9]/.test( event.key ) ) {
event.preventDefault();
}
}
}
I have improved on #J Ws answer. The resulting code does not have to define how to react on which keypress, which is why it can be used with any character in contrast to the accepted answer. It only cares about the string-length of the result. It also can handle Copy-Paste-actions and cuts overlong pastes to size:
Vue.component("limitedTextarea", {
props: {
value: {
type: String,
default: ""
},
max: {
type: Number,
default: 25
}
},
computed: {
internalValue: {
get: function () {
return this.value;
},
set: function (aModifiedValue) {
this.$emit("input", aModifiedValue.substring(0, this.max));
}
}
},
template: '<textarea v-model="internalValue" #keydown="$forceUpdate()" #paste="$forceUpdate()"></textarea>'
});
The magic lies in the #keydown and #paste-events, which force an update. As the value is already cut to size correctly, it assures that the internalValue is acting accordingly.
If you also want to protect the value from unchecked script-changes, you can add the following watcher:
watch: {
value: function(aOldValue){
if(this.value.length > this.max){
this.$emit("input", this.value.substring(0, this.max));
}
}
}
I just found a problem with this easy solution: If you set the cursor somewhere in the middle and type, transgressing the maximum, the last character is removed and the cursor set to the end of the text. So there is still some room for improvement...
My custom directive version. Simple to use.
<textarea v-model="input.textarea" v-max-length="10"></textarea>
Vue.directive('maxlength',{
bind: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
var handler = function(e) {
if (e.target.value.length > el.dataset.maxLength) {
e.target.value = e.target.value.substring(0, el.dataset.maxLength);
var event = new Event('input', {
'bubbles': true,
'cancelable': true
});
this.dispatchEvent(event);
return;
}
};
el.addEventListener('input', handler);
},
update: function(el, binding, vnode) {
el.dataset.maxLength = Number(binding.value);
}
})
Event() has browser compatibility issue.
Unfortunately for me, keydown approach seems not working good with CJK.
there can be side effects since this method fires input event double time.
Best way is to use watch to string length and set old value if string is longer than you want:
watch: {
'inputModel': function(val, oldVal) {
if (val.length > 250) {
this.inputModel = oldVal
}
},
},
Simply use maxlength attribute like this:
<textarea v-model="value" maxlength="50" />
I used your code and broke it out into a .Vue component, thanks!
<template>
<textarea v-model="internalValue" #keydown="onKeyDown"></textarea>
</template>
<script>
export default {
props:{
value:{ type: String, default: ""},
max:{type: Number, default: 250}
},
computed:{
internalValue: {
get() {return this.value},
set(v){ this.$emit("input", v)}
}
},
methods:{
onKeyDown(evt){
if (this.value.length >= this.max) {
evt.preventDefault();
console.log('keydown');
return
}
}
}
}
I did this using tailwind 1.9.6 and vue 2:
<div class="relative py-4">
<textarea
v-model="comment"
class="w-full border border-gray-400 h-16 bg-gray-300 p-2 rounded-lg text-xs"
placeholder="Write a comment"
:maxlength="100"
/>
<span class="absolute bottom-0 right-0 text-xs">
{{ comment.length }}/100
</span>
</div>
// script
data() {
return {
comment: ''
}
}