I am currently creating a search box in VueJS.
Now my question is how to make letters that are searched bold in the results.
This is the part of code I have now:
<div v-for="result in searchResults.merk" :key="result.uid">
<inertia-link :href="result.url">
<strong v-if="result.name.toLowerCase().includes(searchInput.toLowerCase())">{{result.name}}</strong>
<span v-else>{{result.name}}</span>
</inertia-link>
</div>
This sort of does what I want, but not really, now it makes the whole word bold if it contains these letters. I would like to only give those specific letters bold, and the rest of the word normal styling.
See example below. It contains 2 solutions
Very simple but with the BIG warning as it is using v-html. v-html should not be used with user input for safety reasons (see the link). Luckily in this case if user enters some html into search box, it will be rendered only if it is also contained in searched text. So if searched text (source in example) is safe (produced by trusted source), it is perfectly save
Second solution is little bit involved - simply split the text into multiple segments, mark segments which should be highlighted and then render it using v-for and v-if. Advantage of this solution is you can also render Vue components (for example Chips) and use other Vue features (wanna bind a click handler on highlighted text?) which is not possible with v-html solution above...
const vm = new Vue({
el: '#app',
data() {
return {
source: 'foo bar baz baba',
search: 'ba',
}
},
computed: {
formatedHTML() {
const regexp = new RegExp(this.search, "ig")
const highlights = this.source.replace(regexp, '<strong>$&</strong>')
return `<span>${highlights}</span>`
},
highlights() {
const results = []
if (this.search && this.search.length > 0) {
const regexp = new RegExp(this.search, "ig")
let start = 0
for (let match of this.source.matchAll(regexp)) {
results.push({
text: this.source.substring(start, match.index),
match: false
})
start = match.index
results.push({
text: this.source.substr(start, this.search.length),
match: true
})
start += this.search.length
}
if(start < this.source.length)
results.push({ text: this.source.substring(start), match: false})
}
if (results.length === 0) {
results.push({
text: this.source,
match: false
})
}
return results
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input type="text" v-model="search" />
<div>As HTML:
<span v-html="formatedHTML"></span>
</div>
<div>Safe:
<span>
<template v-for="result in highlights">
<template v-if="result.match"><strong>{{result.text}}</strong></template>
<template v-else>{{result.text}}</template>
</template>
</span>
</div>
</div>
You can try splitting the word into a collection of in-line spans.
So you'd first iterate over the results. Then you'd iterate over each letter of the result.name, putting each individual letter in its own span.
Lastly, you'd toggle a bold class on the spam using the object class syntax.
Consider the following example:
<span
v-for="letter in result.name.toLowerCase()"
:key="letter"
:class={ isBold: searchInput.toLowerCase().includes(letter) }
>
{{letter}}
</span>
The object syntax for classes will toggle the class on or off if the condition to the right of the class name is true.
This isn't an ideal solution as it contains a reasonable amount of computational complexity (2x lowercase, and searching the search string for each letter). However, the use case is very simple so modern devices shouldn't struggle.
If you find it worth optimising, you can debounce the input and generate a map of the different letters lowercase search letters then search that map as map access is an O(1) operation.
Related
I am new to ReactJS and building an app that highlights to the user whether a letter in a string is a consonant or a vowel by changing the letter's colour and adding a small 'c' or 'v' beneath the relevant letter.
I am struggling with implementing this and wondering how I add css styling and the 'c' or 'v' to a particular letter (grapheme) as the user types depending on whether it is a consonant or vowel.
Any advice would be very welcome, thanks!
Here is what I have so far:
const TextArea = () => {
const registerKeyPresses = (e) => {
let consonants = [
"b",
"d",
[...]//consonant list
];
let grapheme = e.key;
for (let i = 0; i < consonants.length; i++) {
if (grapheme === consonants[i]) {
console.log("consonant");
}
}
};
return (
<form className="textinputframe">
<div className="textinputframe">
<textarea
className="textinput"
type="text"
onKeyDown={registerKeyPresses}
/>
</div>
</form>
);
};
export default TextArea;
I think what you're trying to build is a text-rich based editor, something like what draft.js can achieve (note that this library is a HUGE one)
What you're trying to do can't be achieved with a normal text area. I can see 2 options:
Use a library like draft.js to achieve what you want.
"Fake" the display, so you'll have an element that where it tracks your key input (mimics a text area/input field), then style it on display.
So you can have a textarea like one above, but style it so the output doesn't show color: rgba and make the alpha 0 or any style that suits your need. Then on top of that is a div that display whatever you typed and you can style it line by line or even by letters.
Our QA team uses protractor 5.4.2 for feature / integration tests on an angular web app. Part of the app is receiving data from our API, then using just CSS applying a "sentence-case" class to it to force the text into desired capitalization: first character upper case, all following characters lowercase--regardless of input format.
This works fine for the app being displayed in the browser. But when our tests extract the element text with element.getText(), the returned string has had the 'lowercase' text transform done, but not the uppercasing of the first character.
CSS style:
/* convert something to sentence case */
.sentence-case {
text-transform: lowercase;
}
.sentence-case::first-letter {
text-transform: uppercase;
}
Sample HTML
<section id="testSect">
<h2>Test Section</h2>
<br/>
<div class='sentence-case'>ABCDEF</div>
<div class='sentence-case'>aBcDeF</div>
<div class='sentence-case'>Abcdef</div>
<div class='sentence-case'>ABCDEF</div>
</section>
Protractor test code:
describe('sentence-case transform', () => {
it('with GetText', () => {
let foo: ElementArrayFinder = element.all(by.css('#testSect div.sentence-case'));
browser.get('http://localhost:4200/testPage');
foo.each((bar: ElementFinder) => {
expect(bar.getText()).toBe('Abcdef');
})
})
})
Test results:
sentence-case transform
× with GetText
- Expected 'abcdef' to be 'Abcdef'.
- Expected 'abcdef' to be 'Abcdef'.
- Expected 'abcdef' to be 'Abcdef'.
- Expected 'abcdef' to be 'Abcdef'.
Is this a limitation in protractor's support for pseudo-selectors?
Is there any workaround in protractor to get the final transformed text value?
I have seen the same behavior recently and while we could work around it there are some options you may be able to work with. You should be able to get the text of an element as it appears (after the text-tranformations) by sending some javascript commands to the browser using executeScript.
let textDiv = await element(by.css('div[class]')).get(0);
browser.executeScript("return arguments[0].innerText;", textDiv);
outerText should also work in this context.
If you really want to get the pseudo-selectors you can run a command like the following also.
//returns uppercase
browser.executeScript("return window.getComputedStyle(arguments[0], ':first-letter').getPropertyValue('text-transform');", textDiv);
Code you can try
let foo = element.all(by.css('#testSect div.sentence-case'));
foo.each((bar) => {
browser.executeScript("return arguments[0].innerText;", bar).then(innerText => {
console.log(innerText)
expect(innerText).toBe('Abcdef');
})
})
I have a React component with parsed text:
The html structure is like:
<div>
<span>It's a </span>
<span className="highlight-text">cat</span>
</div>
How I can have a event listener which enable I pass all selected text within this div? For example, if I select "a ca", the event listener can get e.target.value = "a ca".
It is possible the highlight part will be repeating within the full text, for example:
<div>
<span>It's a slim cat, not a fat </span>
<span className="highlight-text">cat</span>
</div>
In this case, the selection listener will get 2nd part string, start position of whole text.
I got one answer myself, in order to get the selected text, I can just use window.getSelection().
But not sure if there's a risk
This is the first thing that comes to mind.
// Example impl
<Component text="This is a cat" highlight="is a" />
// Component render
render() {
const {
text,
highlight,
} = this.props;
// Returns the start index for your high light string
// in our example 'is a' starts at index 5
const startHighlightTextIdx = text.search(highlight);
// Create a substring for the first part of the string
// ie: 'This is '
const startText = text.substring(0, startHighlightTextIdx);
// Create a substring for the last part of the string
// For this substr we want to start where our startHighlightTextIdx starts
// and we want to add the length of the highlighted string
// in order to ignore the highlighted string
// ie: start at index 5 + 4 (highlight text length) so substr(9) to end
const endText = text.substring(
startHighlightTextIdx + highlight.length,
);
<div>
<span>{startText}</span>
<span className="highlight-text">{highlight}</span>
<span>{endText}</span>
</div>
}
v2:
// This might be a cleaner solution
// explode by the hightlight string and grab index 0 and last
// this outputs ["This ", " cat"]
const textParts = text.split(highlight);
const startText = textParts[0];
const endText = textParts[1];
I want to get the innerText of an item in a rendered list, but accessing it using this.$refs doesn't seem to work. I've also tried to use v-modal and that doesn't seem to work either.
Here's my code:
<div id="simple" v-cloak>
<h1>Clicked word value!</h1>
<ul>
<li v-for="word in wordsList" #click="cw_value" ref="refWord">
{{ word }}
</li>
<h4> {{ clickedWord }} </h4>
</ul>
</div>
var app = new Vue({
el: '#simple',
data: {
clickedWord: '',
wordsList: ['word 1', 'word 2', 'word 3']
},
methods: {
cw_value: function() {
this.clickedWord = this.$refs.refWord.innerText
// "I don't know how to get inner text from a clicked value"
}
}
})
Since you've used ref="refWord" on the same element as a v-for, this.$refs.refWord is an array containing each DOM element rendered by v-for.
You should reference the index of each word, and then pass that to the click handler:
<li v-for="word, index in wordsList" #click="cw_value(index)" ref="refWord">
Then, in your cw_value method, use the index value to access the correct element in the array:
cw_value: function(index) {
this.clickedWord = this.$refs.refWord[index].innerText;
}
Here's a working fiddle.
Alternatively, it would be much simpler to just set the clicked word inline in the click handler:
<li v-for="word in wordsList" #click="clickedWord = word">
Here's a working fiddle for that too.
Since innerText takes CSS styles into account, reading the value of innerText triggers a reflow to ensure up-to-date computed styles. (Reflows can be computationally expensive, and thus should be avoided when possible.) Here is MDN document on that.
Now it is:
this.$refs.refWord[index].textContent
I'm using Vue.Js for a survey, which is basically the main part and the purpose of the app. I have problem with the navigation. My prev button doesn't work and next keeps going in circles instead of only going forward to the next question. What I'm trying to accomplish is just to have only one question visible at a time and navigate through them in correct order using next and prev buttons and store the values of each input which I'll later use to calculate the output that will be on the result page, after the survey has been concluded. I've uploaded on fiddle a short sample of my code with only two questions just to showcase the problem. https://jsfiddle.net/cgrwe0u8/
new Vue({
el: '#quizz',
data: {
question1: 'How old are you?',
question2: 'How many times do you workout per week?',
show: true,
answer13: null,
answer10: null
}
})
document.querySelector('#answer13').getAttribute('value');
document.querySelector('#answer10').getAttribute('value');
HTML
<script src="https://unpkg.com/vue"></script>
<div id="quizz" class="question">
<h2 v-if=show>{{ question1 }}</h2>
<input v-if=show type="number" v-model="answer13">
<h2 v-if="!show">{{ question2 }}</h2>
<input v-if="!show" type="number" v-model="answer10">
<br>
<div class='button' id='next'>Next</div>
<div class='button' id='prev'>Prev
</div>
</div>
Thanks in advance!
You should look at making a Vue component that is for a survey question that way you can easily create multiple different questions.
Vue.component('survey-question', {
template: `<div><h2>{{question.text}}</h2><input type="number" v-model="question.answer" /></div>`,
props: ['question']
});
I've updated your code and implemented the next functionality so that you can try and create the prev functionality. Of course you should clean this up a little more. Maybe add a property on the question object so it can set what type the input should be. Stuff like that to make it more re-useable.
https://jsfiddle.net/9rsuwxvL/2/
If you ever have more than 1 of something, try to use an array, and process it with a loop. In this case you don't need a loop, but it's something to remember.
Since you only need to render one question at a time, just use a computed property to find the current question, based on some index. This index will be increased/decreased by the next/previous buttons.
With the code in this format, if you need to add a question, all you have to do is add it to the array.
https://jsfiddle.net/cgrwe0u8/1/
new Vue({
el: '#quizz',
data: {
questions:[
{question:'How old are you?', answer: ''},
{question:'How many times do you workout per week?', answer: ''},
],
index:0
},
computed:{
currentQuestion(){
return this.questions[this.index]
}
},
methods:{
next(){
if(this.index + 1 == this.questions.length)
this.index = 0;
else
this.index++;
},
previous(){
if(this.index - 1 < 0)
this.index = this.questions.length - 1;
else
this.index--;
}
}
})