I currently have this div:
<div class="center-align" :class="setDisplay()">
<div class="lds-hourglass"></div>
</div>
I need to check if "this" div contains "center-aligh" to execute code in setDisplay:
In my Vue 3 setup I have:
setup() {
const setDisplay = () => {
console.log(this.$el);
}
}
return {
setDisplay
}
this.$el returns as undefined. From what I've read I would return it like so:
this.$el.className based on this answer
But it's not working. Any idea what I'm doing wrong?
You can use ref in setup function and with nextTick get class in mounted hook with value.classList :
const { ref, onMounted, nextTick } = Vue
const app = Vue.createApp({
setup() {
const el = ref(null)
const myClass = ref(null)
const setDisplay = () => {
myClass.value = 'myclass'
}
onMounted(async () => {
await nextTick()
console.log(Array.from(el.value.classList))
Array.from(el.value.classList).includes('center-align') && setDisplay()
})
return { setDisplay, el, myClass }
}
})
app.mount('#demo')
.myclass {
color: red;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div class="center-align xxx" ref="el" :class="myClass">
<div class="lds-hourglass"></div>
class applied
</div>
</div>
Related
I'm using composition in React and would like to call a parent method. All of the examples I've found use inheritance.
Container component - Inserts a child component
interface ContainerProps {
children: ReactNode;
}
function Container(props: ContainerProps) {
const [showApply, setShowApply] = useState<boolean>(false);
return (
<>
<div>Children</div>
{props.children}
</>
);
// I want to call this method from the `children`
function calledByChild(){}
}
Composition - Needs to call Container method when button is clicked
function CombinedComponent() {
return <Container handleApplyClicked={handleApplyClicked}>
<Button type="primary" shape="round" onClick={tellContainerThatButtonWasClicked}>
</Container >
}
When the button is clicked in the CombinedComponent I would like it to inform the Container. The examples I've seen use inheritance and pass the parents method to the child but in this case the child is defining the parent within it.
How can this be achieved?
Update
I've tried adding this to the parent but the child components don't seem to have the extra property added.
{React.cloneElement(props.children as React.ReactElement<any>, { onClick: myFunc })}
Child interface/props
interface CombinedComponentProps{
// This value is always undefined
onClick?: () => void;
}
function CombinedComponent(props: CombinedComponentProps) {
...
// Undefined
console.log(props.onClick)
}
I recently had to do something similar and, inspired by this post, I ended up with:
const WrapperComponent = ({ children }) => {
const myFunc = React.useCallback(() => {
// ...
}, []);
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { onClick: myFunc });
}
});
}
[edit] a working demo:
The following snippet demonstrate how the above approach could be used to read a child prop from the wrapper/parent component.
Please be aware that it might take a few seconds for the snippet to load and run; I did not investigate the reason for this, as it is out of the scope for the question.
const App = () => {
return (
<div>
<h1>MyApp</h1>
<WrapperComponent>
<button id='btn1'>btn1</button>
<button id='btn2'>btn2</button>
<button id='btn3'>btn3</button>
<div className='fakeBtn' id='div1'>div1</div>
</WrapperComponent>
</div>
);
}
const WrapperComponent = ({ children }) => {
const [clickedChildId, setClickedChildId] = React.useState();
const myFunc = React.useCallback((id) => {
setClickedChildId(id)
}, [setClickedChildId]);
React.useEffect(() => {
clickedChildId && console.log(`the clicked child ID is ${clickedChildId}`);
}, [clickedChildId]);
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { onClick: () => myFunc(child.props.id) });
}
});
}
ReactDOM.render(<App />, document.querySelector('#mountNode'))
div.fakeBtn {
background-color: #cdf;
padding: 5px;
margin: 3px 0;
border: 1px solid #ccc;
border-radius: 2px;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id='mountNode'></div>
You can do it by cloning the children, and giving it the props that you want:
React.cloneElement(children, {calledByChild})
This way, you add the function calledByChild to the children, so you can call it from the children component.
It could look like this:
const Parent = ({ children }) => {
const func = () => console.log("click in Parent");
return (
<>
<div>children</div>
{cloneElement(children, {func})}
</>
);
};
const Children = ({func}) => {
return <button onClick={func}>Click</button>;
};
Take a look at this article
I have this code here,
it basically fetch from firestore in the setup(), then display the information with the Categoria component. It also should update the Categoria components when the <span> is pressed. However, something don't work. My snippet successfully update the database but does not reload the component... any ideas?
<template>
<div class="header">
<span class="mdi mdi-home icona" />
<h1>Modifica menĂ¹</h1>
</div>
<Categoria
v-model="categorie"
v-for="categoria in categorie"
v-bind:key="categoria"
:Nome="categoria.nome"
/>
<div class="contenitore_aggiungi">
<span #click="crea()" class="mdi mdi-plus aggiungi" />
</div>
</template>
<script>
import Categoria from "#/components/edit/Categoria-edit.vue";
import { useRoute } from "vue-router";
import { creaCategoria, ottieniCategorie } from "#/firebase";
export default {
name: "Modifica",
components: { Categoria },
async setup() {
const route = useRoute();
let idRistorante = route.params.id;
let categorie = await ottieniCategorie(idRistorante);
console.log(categorie);
return { idRistorante, categorie };
},
methods: {
crea() {
let nuovaCategoria = "Nuova categoria";
creaCategoria(this.idRistorante, nuovaCategoria);
this.categorie.push({ nome: nuovaCategoria });
console.log(this.categorie);
},
},
};
</script>
Thanks for your answers!
You need to declare categorie as a reactive property. Also you can write methods in setup() itself instead of methods:
import { ref } from 'vue'
export default {
setup() {
const route = useRoute();
let idRistorante = route.params.id;
const categorie = ref({}) // <-- add default value of properties
const getData = async () => {
const data = await ottieniCategorie(idRistorante);
categorie.value = data
}
getData() // or void getData()
const crea = () => {
let nuovaCategoria = "Nuova categoria";
categorie.value.push({ nome: nuovaCategoria });
console.log(categorie.value);
},
return { idRistorante, categorie, crea };
}
}
Make sure the default value of categorie is set in ref(). If it's an array set it to ref([]).
I want to be able to create reusable custom elements. With my current implementation, each of the custom elements renders only once. All the elements (all 4) are injected into the DOM, but only the first instance of each is rendered.
I have tried with both using ShadowDOM and not using it. Any ideas?
Screenshot from dev tools:
index.html (extract from <body>):
<body>
<funky-header></funky-header>
<funky-content></funky-content>
<funky-header></funky-header>
<funky-content></funky-content>
<script src="index.js" defer></script>
</body>
I have created a generic script to create custom elements from a .html file:
index.js:
const elements = [
{ name: 'funky-header', shadowDom: false },
{ name: 'funky-content', shadowDom: false }
]
async function registerCustomElement(elementName, shadowDom) {
console.log(`Registering ${elementName}`)
await fetch(`./${elementName}.html`)
.then(stream => stream.text())
.then(async markup => {
const doc = new DOMParser().parseFromString(markup, 'text/html');
const template = doc.querySelector('template[alpine]')
const templateContent = template.content
const styles = doc.querySelector('style[scoped]')
const styleContent = styles.textContent.toString()
const elements = templateContent.querySelectorAll('[class]')
class CustomElement extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
console.log(`inserting ${elementName}`)
if (shadowDom) {
const shadowRoot = this.attachShadow({ mode: 'closed' })
shadowRoot.appendChild(template.content)
} else {
this.setAttribute(`data-x-${elementName}`, '')
elements.forEach(element => {
element.setAttribute(`data-x-${elementName}`, '')
})
const scopedStyles = styleContent.replaceAll(' {', '{').replaceAll('{', `[data-x-${elementName}] {`)
const styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.append(document.createTextNode(scopedStyles))
this.append(styleTag)
this.append(templateContent)
}
}
}
customElements.define(elementName, CustomElement)
})
.catch(err => {
console.log('ERROR:', err)
})
}
elements.forEach(element => registerCustomElement(element.name, element.shadowDom))
An example template file:
funky-header.html:
<template alpine>
<h1 class="font-black text-blue-800">This is my header</h1>
<p class="font-thin text-xs text-blue-600 my-text">This is a paragraph with a longer text, to simualte a descritpion.</p>
</template>
<style scoped>
.my-text {
color: purple;
}
</style>
The solution was as simple as replacing the this.append(templateContent) with this.innerHTML = template.innerHTML.
How to trigger the clicked method on app2 from ComponentA in this example.
const app = Vue.createApp({});
app.component('ComponentA', {
template: `<button #click="clicked" class='btn'>Click</button>`
});
const app2 = Vue.createApp({
methods: {
clicked() {
this.clickCount += 1;
}
},
render() {
return Vue.h(app.component('ComponentA'), options);
}
}).mount("#App");
From the button click event handler try to emit an event called clicked, in render function define it by prefixing it by on and upper casing the first letter like onClicked: (e) => {...} inside the body of this function run this.clicked
let options = {}
options.baseUrl = "someurl.com";
const app = Vue.createApp({})
app.component('ComponentA', {
template: `
<button #click="$emit('clicked')" class='btn'>Click</button>
`
});
const app2 = Vue.createApp({
methods: {
clicked() {
console.log("clicked !!!")
this.clickCount += 1;
}
},
render() {
return Vue.h(app.component('ComponentA'), {
onClicked: (e) => {
this.clicked()
}
}, options)
}
}).mount("#app");
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" someVariable='some value'>
</div>
I have few DIV elements which I make selectable using DRAGSELECT library.
In the componentDidMount,
componentDidMount() {
var ds = new DragSelect({
selectables: document.getElementsByClassName('ele-select'),
area: document.getElementById('canvas'),
onElementSelect: (element) => {
var idArray = [];
idArray.push(ds.getSelection())
this.check(idArray)
}
})
$('.text').draggable({
cursor: 'move'
})
$('#canvas').on('mousedown', '.main-container', (e) => {
if ($(e.target).attr('class').indexOf('main-container') > -1) {
this.unwrapAll()
ds.start()
} else {
//idArray = []
ds.stop();
ds.break();
}
})
$('#canvas').on('mouseup', function (e) {
//idArray = []
ds.stop();
ds.start()
})
}
This results in error that: 'ds' is not defined no-undef
However I fixed the error using - /* eslint no-undef: 0 */ // --> OFF
at the top of component.
Now when I try add a new element and make it selectable it throws:
ReferenceError: ds is not defined.
I'm preety sure this is due to variable scope as var have functional scope.
But where should I declare it then? even tried inside constructor but doesn't seems to work.
import React, { Component } from 'react';
import './App.css';
import $ from 'jquery'
import 'jquery-ui/ui/widgets/draggable';
import DragSelect from 'dragselect';
import Button from './components/Button/Button'
/* eslint no-undef: 0 */ // --> OFF
class App extends Component {
state = {
btn: ['Add Element', 'Wrap', 'Unwrap']
}
componentDidMount() {
var ds = new DragSelect({
selectables: document.getElementsByClassName('ele-select'),
area: document.getElementById('canvas'),
onElementSelect: (element) => {
var idArray = [];
idArray.push(ds.getSelection())
this.check(idArray)
}
})
$('.text').draggable({
cursor: 'move'
})
$('#canvas').on('mousedown', '.main-container', (e) => {
if ($(e.target).attr('class').indexOf('main-container') > -1) {
this.unwrapAll()
ds.start()
} else {
//idArray = []
ds.stop();
ds.break();
}
})
$('#canvas').on('mouseup', function (e) {
//idArray = []
ds.stop();
ds.start()
})
}
clickHandler = (val) => {
if(val === 'Add Element'){
this.addElement();
}
}
addElement = () => {
var x = $('.text:last')
var top = parseFloat(x.css('top').slice(0, -2))
x = x.attr('id').slice(-1)
$('.main-container').append("<div class='text ele-select'>" +Math.random()+ "</div>")
$('.text:last').attr('id', 'text' + (++x))
$('.text:last').css({
'top': top + 27
})
$('.text:last').draggable({
cursor: 'move'
})
ds.addSelectables(document.getElementsByClassName('ele-select'));
}
render() {
var button = this.state.btn.map((element,i) => {
return <Button value={element} clicked = {() => this.clickHandler(element)} key={i + element}/>
})
return (
<div>
{button}
<div id='canvas'>
<div className="main-container">
<div className="text ele-select" id='text1' draggable="true">Some text</div>
<div className="text ele-select" id='text2' draggable="true">Another text</div>
</div>
</div>
</div>
);
}
}
export default App;