I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.
Related
this is written in JS
i cant seem to make the MovieDetails button work at all.
function searchMovie(query) {
const url = `https://imdb8.p.rapidapi.com/auto-complete?q=${query}`;
fetch(url, options)
.then(response => response.json())
.then(data => {
const list = data.d;
list.map((item) => { //makes a list of each individual movie from the data
const name = item.l; // holds the name of movie
const poster = item.i.imageUrl; // holds the poster, given by the data
const detail = item.id // holds the ttid of the movie
// below is what shows the poster, movie name, etc
const movie =
`
<body>
<div class="colmd3">
<div class = "well text-center">
<li><img src="${poster}">
<h2>${name}</h2>
</li>
<button type = "button" id = "MovieDetails" class="btn btn-primary" href="#">Movie Details</button>
<script>
document.getElementById('MovieDetails').addEventListener("click",myFunction);
function myFunction(){
console.log(detail)
}
</script>
</div>
</div>
</body>
`;
document.querySelector('.movies').innerHTML += movie; // returns the first element movies and poster to movie div
//console.log()
});
document.getElementById("errorMessage").innerHTML = "";
})
.catch((error) => {
document.getElementById("errorMessage").innerHTML = error;
});
// we should make a condition here for when a new item is placed here there will be a page refresh
// setTimeout(() => {
// location.reload(); }, 2000);
}
the function above will make an api call and then save the results into list, i can hold specific elements of the list in the three const's and const movie will output the movie poster, name and display it.
I want to make a button for each movie that when clicked will output the id of the movie which is held in const details.
But i cant figure out how to make it work, i have tried (button onclick = function ..) and (document.getElementById...) but it says that getElementById cant be null.
i know that this seems like a silly problem but i cant seem to figure how to make the button actually output something useful or any other way to make a button be mapped out to each api call.
You're heading in the right direction but there are a couple of pain-points with your code as the other commenters have indicated.
Your template string is adding a brand new body element to the page for each movie where there should just be one for the whole document. Nice idea to use a template string though - by far the simplest method to get new HTML on to the page.
Adding JS to the page dynamically like that is going to end up causing you all kinds of problems - probably too many to mention here, so I'll just skip to the good part.
First remove the body element from the template string, and perhaps tidy up the remaining HTML to be a little more semantic. I've used section here but, really, anything other than having lots of divs is a step in the right direction.
Second: event delegation. Element events "bubble up" the DOM. Instead of attaching a listener to every button we can add one listener to the movie list containing element, and have that catch and process events from its children.
(Note: in this example, instead of logging the details to the console, I'm adding the details to the HTML, and then allowing the button to toggle the element on/off.)
// Cache the movie list element, and attach a listener to it
const movieList = document.querySelector('.movielist');
movieList.addEventListener('click', handleClick);
// Demo data
const data=[{name:"movie1",poster:"img1",details:"Details for movie1."},{name:"movie2",poster:"img2",details:"Details for movie2."},{name:"movie3",poster:"img3",details:"Details for movie3."}];
// `map` over the data to produce your HTML using a
// template string as you've done in your code (no body element)
// Make sure you `join` up the array that `map` returns into a
// whole string once the iteration is complete.
const html = data.map(obj => {
return `
<section class="movie">
<header>${obj.name}</header>
<section class="details">${obj.details}</section>
<button type="button">Movie Details</button>
</section>
`;
}).join('');
// Insert that HTML on to the movie list element
movieList.insertAdjacentHTML('beforeend', html);
// This is the handler for the listener attached to the
// movie list. When that element detects an event from a button
// it finds button's previous element sibling (the section
// with the `.details` class), and, in this case, toggles a show
// class on/off
function handleClick(e) {
if (e.target.matches('button')) {
const details = e.target.previousElementSibling;
details.classList.toggle('show');
}
}
.movie { border: 1px solid #555; padding: 0.5em;}
.movie header { text-transform: uppercase; background-color: #efefef; }
.details { display: none; }
.show { display: block; }
button { margin-top: 0.5em; }
<section class="movielist"></section>
Additional documentation
insertAdjacentHTML
matches
classList
toggle
I only want a particular div to display if showHideClassName is set to the value of true. I have this code in my React application so that a div will either display or not depending on the status of showHideClassName:
render() {
...
const showHideClassName = showPrompt ? 'show-div' : 'display-none';
return (
<div className={showHideClassName}>
...
</div>
);
}
The div is always visible though. Is there any way I can get this to work as I desire?
I don't have display-none in my css. Now that I've added the below, it works ad desired.
.display-none { display: none; }
I'm relatively new to Vue so I may not be doing this in the most performant way. I have a table of rows created with a v-for loop. I also have a corresponding list of row titles also created with a v-for loop. They need to be separate DOM elements to enable fixed header scrolling. When I hover over the row I want to change its color AND the color of the corresponding row title.
So I thought you would be able to do the following with Vue:
On mouseover set a data property to the index of the element being hovered over.
Bind a class to the elements of interest to change their styles when the index is equal to their index from the v-for loop.
This does work but it results in very slow / laggy code. I have quite a few rows (40) and columns (60) but it isn't an enormous amount.
An abridged version of the code is below:
Template html
<div class="rowTitle"
v-for="(rowName, i) in yAxis.values" :key="i"
#mouseover="hover(i)"
:class="hoverActive(i)" >{{rowName}}</div>
<div class="row"
v-for="(row, i) in rows" :key="i"
#mouseover="hover(i)"
:class="hoverActive(i)" >{{row}}</div>
Vue object
export default {
data:()=>(
{
rows: rows,
yAxis: yAxis,
rowHover: -1,
}
),
methods:{
hover(i) {
this.rowHover = i
},
hoverActive(i) {
return this.rowHover == i ? 'hover' : ''
}
},
}
The likely answer is that the hover event is firing very frequently, and each time it fires you are forcing vue to re-render. If you could add a debounce the event handler, or even better mark the event handler as passive, it would make a big difference in performance. In this runnable snippet, I'm telling vue to use a passive event modifier (see https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers) to tell the browser not to prioritize that event handler over critical processes like rendering.
To be clear, if you can use CSS to solve the problem like a commenter mentioned (see https://developer.mozilla.org/en-US/docs/Web/CSS/:hover), I strongly advise that as the browser's performance should be excellent with that approach. But sometimes css does not solve everything, so a debounce or a passive event listener might be your next best option. You could also experiment with the "stop" (stop propagation) modifier if additional tuning is required.
const grid = [];
for (var i = 0; i < 40; i++) {
grid.push([]);
for (var j = 0; j < 60; j++) {
grid[i].push({
i,
j
});
}
}
Vue.component('aTable', {
props: ['grid'],
data() {
return {
rowHover: -1
}
},
template: `
<div class="table">
<div class="row" v-for="(row, x) in grid">
<span v-for="cell in row"
#mouseover.passive="hover(x)"
:class="hoverActive(x)"
class="row-cell"
>i:{{cell.i}},j:{{cell.j}}</span>
</div>
</div>
`,
methods: {
hover(i) {
this.rowHover = i
},
hoverActive(i) {
return this.rowHover == i ? 'hover' : ''
}
}
})
var app = new Vue({
el: '#app',
data() {
return {
grid
}
}
})
body {
font-size: 11px;
}
.row-cell {
min-width: 40px;
display: inline-block;
border: 1px solid gray;
}
.row-cell.hover {
color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
My table
<a-table :grid="grid" />
</div>
I am trying to get a vue component to announce information dynamically to a screen reader when different events occur on my site.
I have it working to where clicking a button will populate a span that is aria-live="assertive" and role="alert" with text. This works decently the first time, however, clicking other buttons with similar behavior causes NVDA to read the previous text twice before reading the new text. This seems to be happening in vue, but not with a similar setup using jquery, so I'm guessing it has something to do with the way vue renders to the DOM.
I'm hoping there is some way to workaround this problem or perhaps a better way to read the text to the user that would not have this issue. Any help is greatly appreciated.
Here is a simple component I set up in a working code sandbox to show the problem I am having (navigate to components/HelloWorld.vue for the code) -- Note: This sandbox has changed per the answer below. Full code for the component is below:
export default {
name: "HelloWorld",
data() {
return {
ariaText: ""
};
},
methods: {
button1() {
this.ariaText = "This is a bunch of cool text to read to screen readers.";
},
button2() {
this.ariaText = "This is more cool text to read to screen readers.";
},
button3() {
this.ariaText = "This text is not cool.";
}
}
};
<template>
<div>
<button #click="button1">1</button>
<button #click="button2">2</button>
<button #click="button3">3</button><br/>
<span role="alert" aria-live="assertive">{{ariaText}}</span>
</div>
</template>
Ok so what I've found works way more consistently is instead of replacing the text in the element with new text, to add a new element to a parent container with the new text to be read. Instead of storing the text as a single string, I am storing it in an array of strings which will v-for onto the page within an aria-live container.
I have built a full component that will do this in various ways as an example for anyone looking to do the same:
export default {
props: {
value: String,
ariaLive: {
type: String,
default: "assertive",
validator: value => {
return ['assertive', 'polite', 'off'].indexOf(value) !== -1;
}
}
},
data() {
return {
textToRead: []
}
},
methods: {
say(text) {
if(text) {
this.textToRead.push(text);
}
}
},
mounted() {
this.say(this.value);
},
watch: {
value(val) {
this.say(val);
}
}
}
.assistive-text {
position: absolute;
margin: -1px;
border: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
<template>
<div class="assistive-text" :aria-live="ariaLive" aria-relevant="additions">
<slot></slot>
<div v-for="(text, index) in textToRead" :key="index">{{text}}</div>
</div>
</template>
This can be used by setting a variable on the parent to the v-model of the component, and any changes to that variable will be read to a screen reader once (as well as any time the parent container becomes tab-focused).
It can also be triggered by this.$refs.component.say(textToSay); -- note this will also be triggered again if the parent container becomes tab-focused. This behavior can be avoided by putting the element within a container that will not receive focus.
It also includes a slot so text can be added like this: <assistive-text>Text to speak</assistive-text> however, that should not be a dynamic/mustache variable or you will encounter the problem in the original question when the text changes.
I've also updated the sandbox posted in the question with a working example of this component.
I've been messing around with aurelia-dialog trying to get a modal dynamically populated with some information. I have some stuff working but the modal is the incorrect size for the data its displaying.
welcome.js
import {DialogService} from 'aurelia-dialog';
import {CmdModal} from './cmd-modal';
export class Welcome {
static inject = [DialogService];
constructor(dialogService) {
this.dialogService = dialogService;
}
OpenCmd(intName, opName, opDescription, parameters){
var cmd = { "CmdName" : opName, "Description" : opDescription, "Params" : parameters};
this.dialogService.open({ viewModel: CmdModal, model: cmd}).then(response => {
if (!response.wasCancelled) {
console.log('good - ', response.output);
} else {
console.log('bad');
}
console.log(response.output);
});
}
cmd-modal.html
<template>
<ai-dialog>
<ai-dialog-header>
<h2>${cmd.CmdName}</h2>
</ai-dialog-header>
<ai-dialog-body>
<p>${cmd.Description}</p>
<b>Parameters</b>
<div repeat.for="param of cmd.Params">
<p class="col-md-6">${param.Key}</p>
<p class="col-md-6">${param.Value}</p>
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="controller.ok(person)">Ok</button>
</ai-dialog-footer>
</ai-dialog>
</template>
cmd-modal.js
import {DialogController} from 'aurelia-dialog';
export class CmdModal {
static inject = [DialogController];
constructor(controller){
this.controller = controller;
}
activate(cmd){
this.cmd = cmd;
}
}
When a link is clicked, a modal like the following is displayed:
As the image shows, the modal is the wrong size for the body and some of the text spills over the side. I think this is because cmd-modal.html is being rendered before the data for the repeater has been inserted.
Does anybody know how I could resize the modal to be the correct size for the body or delay the modal display until cmd-modal.htmlhas been correctly evaluated?
You can add style for width and height to the ai-dialog tag like this:
<ai-dialog style="width:600px; height: 350px;">
I think I found something similar to this when trying to add items of varying width to the dialog. The widths weren't know until after the dialog had been rendered. Well I think that is why!
In the end I added a CSS class on the ai-dialog element which included a general width setting and a media query.
...
width: 90vw;
#media (min-width: 46em) {
width: 44em;
}
....
I know I mixed vw and em measurements and there's probably better ways - but it works well in this app. I'm sure there's probably a "correct" Aurelia way to get the dialog to re-render but this is ample for our situation.
FWIW I also added a "margin-top: 4em !important" so that the dialog would appear just below the fixed header bar that Bootstrap was providing us.