StencilJs: nested stencil component doesn't work with events, such as 'click' - stenciljs

I have a stenciljs component that has a nested stenciljs component:
<c-button-group>
<c-button></c-button>
</c-button-group>
In c-button-group template, I'm not using a slot and instead I'm using #Element() private element: HTMLElement to get all the nested elements after I render them in a loop.
#Component({
tag: 'c-button-group'
})
export class CButtonGroup {
#Element() private element: HTMLElement
render() {
return (
Array.from(this.element.children)
.map((child) => {
const el = child as ButtonInterface
return <c-button variant="grouped">{el.textContent}</c-button>
})
)
}
}
The reason why i use loop because i have to add attribute variant="grouped" for each nested element and i want to add it here, in the template. So this works but i noticed that if i assign a click event handler it doesn't work.
<body>
<c-button-group>
<c-button id="cBtn" #click="btnClickHandler">Years</c-button>
</c-button-group>
<script>
document.querySelector('#cBtn').addEventListener('click', () => {
console.log('Years')
})
</script>
</body>
Click event above doesn't work.
And seems this is obvious because in the render function i create a new 'c-button' based on a nested c-button.
My question is how do I pass all events that were assigned from the nested component to the new component that was created in the render function?
PS:
I noticed that if i use on-click attribute without a value to new c-button element, click event works:
Array.from(this.element.children)
.map((child) => {
const el = child as ButtonInterface
return <c-button
variant="grouped"
on-click
>{el.textContent}</c-button>
})
I just added on-click and it started working but in console i started getting errors:
TypeError: Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.
So at first this is not an option because i am getting that error and at second what if i need not only 'click' event, maybe there will be 5 or 10 events, so in that case i will have to add them all manually, not very comfortable to say the least.
Thank you so much in advance!

You can try using outerHTML and innerHTML
render() {
return (
Array.from(this.element.children)
.map((child) => {
child.setAttribute('variant', 'grouped')
return <div innerHTML={child.outerHTML}></div>
})
)
}
However this will only be able to pass events which are attached by attribute instead of addEventListener, as outerHTML is a string
<c-button-group>
<c-button id="cBtn" onclick="btnClickHandler()">Years</c-button>
</c-button-group>
Here btnClickHandler will work for rendered children elements

So the only way i found myself is to use <slot /> and #Element() private element: HTMLElement together.
#Element() private element: HTMLElement
componentWillLoad() {
Array.from(this.element.children).forEach(el => {
el.setAttribute('variant', 'grouped')
})
}
render() {
return <slot />
}
Using <slot /> allows as to use all events and using this.element.children gives the ability to edit DOM elements before their rendering, we can remove, add any attribute, class, and so on...

Related

Unable to add elements using the setAttribute

I am using the VUE JS code and trying to add the setAttribute to some of the tags.
Here is the code I am using :
changetab() {
const demoClasses = document.querySelectorAll(".delCon__select");
demoClasses.forEach(button => {
button.setAttribute("tabindex", "0");
});
return true;
},
but when I view in the code inspector, It does not show added to it, I have added the above function in computed.
template is like this :
<template>
<el-container class="orders"></el-download>
</template>
You need to make this type of request in Vue's Lifecycles, like: created or mounted.
Something like:
mounted() {
this.changetab()
}
Computed would not be the most appropriate place for this type of action.

How to get div in nested child component during mounted - Vue.js

I have problem whit scrolling to the element while opening the page. It's like scrolling to an anchor. I'm sending div id via props to the nested child component. On mounted I call a method scrollToSection where I have logic with scrolling.
props: {
section: String
},
mounted() {
this.scrollToSection();
},
methods: {
scrollToSection () {
if (this.section != null) {
const options = {
force: true,
easing: 'ease-in',
};
VueScrollTo.scrollTo(`#${this.section}`, 100, options);
}
}
}
In that point child component dosen't render the DOM and document.getElementById(this.section) return null. But in parent component I have an access to that div.
Do you have any idea how to resolve that issue? Method nextTick dosen't work.
Had the same problem, where I needed to access an image in a nested component, I had to wait till the image is loaded within the nested component, by adding a #load event listener that emit a loaded event to the parent, so I knew it was ready and perform my actions.
If nextTick isn't working, you can do the same, just emit a ready event on the mounted hook of the child component and when it's ready, perform the scroll.
//Child component
mounted(){
this.$emit('ready');
}
_
//Parent component
...
<child-component #ready="scrollToSection()" />
...
Or in alternative, if you have access to the div you want to scroll to, you can use pure javascript
MyDiv.scrollIntoView({ block: "start", behavior: 'smooth' });

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you

How can I get a child component's watcher's updated value from parent component in Vue 2?

I have two components that are in a parent-child relationship. In the child select component I've defined a watcher for the currently selected value and I want the parent to get the updated value whenever it changes. I tried to use events but the parent can only listen to its own vm.
In the child component
watch: {
selected (value) {
this.$emit('selectedYearChange', value)
}
}
In the parent
mounted () {
this.$on('selectedYearChange', function (value) {
this.selectedYear = value
})
}
This obviously doesn't work because of the reason I mentioned above.
I wasn't able to find relevant examples in the documentation.
You can setup an event listener on the child component element itself where its used in the parent and pass it a method to be executed
//parent component
<div id="app>
<child-comp #selected-year-change='changeYear'></child-comp>
</div>
In your parent component use the method changeYear to change the value of `selectedYear'
methods:{
//you get the event as the parameter
changeYear(value){
this.selectedYear = value
}
}
See the fiddle
And avoid using camelCase for event names use kebab-case instead as HTML attributes are case-insensitive and may cause problems
html:
<Parent>
<Child #selected-year-change=HandleChange(data)></Child>
</Parent>
Javascript/Child Component:
watch: {
selected (value) {
this.$emit('selectedYearChange', value)
}
}
Javascript/Parent Component:
HandleChange: function(data){}
good luck. maybe work!

Unable to append html element to the parent in Vue js directive

I have a global directive in vue js
Vue.directive('customDirective',{
bind(el,binding,vnode){
// need to get the parent
// append a div after the input element
}
})
I have the following html code .
<div class="parentDiv">
<input type="text" name="firstName" v-customDirective="Some text"/>
</div>
<div class="parentDiv"> is loading dynamically via jquery. I need to get the parent of input tag and need to append some html after the input tag. I have tried with parent(), but it is showing as parent is not a function. Any suggestions ?
You should probably use the inserted hook instead of bind if you want to access the parent element.
You can get the parent element via el.parentElement.
You probably should use vnode, and Vue nextTick.
import Vue from 'vue';
Vue.directive('customDirective',{
bind(el,binding,vnode){
Vue.bextTick(() => {
let parent = vnode.elm.parentElement;
});
}
})
If you want to use parent() which is the jQuery function, you must get the element by jQuery.
Vue.directive('customDirective',{
bind(el,binding,vnode){
Vue.bextTick(() => {
let id = vnode.elm.id;
let parent = jQuery('#'+id).parent();
});
}
})
I'm not sure, that second code works, but it will be something like that.