How to access element through shadow DOM - primary-key

I am trying to select an element that has by id through an imported web component.
Child Element
<template>
<paper-button id="button"></paper-button>
</template>
Parent Element
<template>
<custom-element id="el"></custom-element>
</template>
...
select() {
let address = this.$.el.button.innerHTML;
}
However, this returns undefined. Is there a way to sub-query or access the button element some other way from the parent?
Something like this.$.el(this.$.button.innerHTML);
or this.$.el.shadowRoot.button.innerHTML

Once the shadowroot is selected, you can chain selections together with getElementById.
this.$.shipMethod.shadowRoot.getElementById('address')

Related

How do I find out whether a Vue 3 component has any children?

Vue 3 removed the $children property and butchered $slots. However I can't find another solution for this scenario I'm having:
I have a component Checkbox. This component renders a checkbox. It also has a default slot. Depending on whether there was anything in that default slot I need to show/hide an element and apply some classes to the Checkbox' root element for styling and animation purposes of elements around it. I don't need access to the actual children, only the information whether there are any.
In Vue 2 I could write something like this:
if (this.$slots.default) {
// do something
}
The same thing in Vue 3 always is true, because it's a function now. However, calling that function returns nonsense:
console.log(this.$slots.default())
// Returns:
// [ Vnode ]
It doesn't matter whether I put something into the default slot or not, the end result is always the same, only with some elements buried in the Vnode tree having changed.
How am I supposed to find out whether there are any children in Vue 3?
For reference, the component template I used before:
<transition name="slide-down">
<div class="children" v-if="$slots.default">
<slot></slot>
</div>
</transition>
The above and below does not work in Vue 3 (the div always is being rendered):
<transition name="slide-down">
<div class="children" v-if="$slots.default()">
<slot></slot>
</div>
</transition>

VueJs - What's The Correct Way to Create a Child Component With Input Fields

I'm trying to use vuejs to display a list of instances of a child component.
The child component has input fields that a user will fill in.
The parent will retrieve the array of data to fill in the child components (If any exists), but since they're input fields the child component will be making changes to the value (Which generates errors in the console, as child components aren't supposed to change values passed from the parent).
I could just be lazy and just do everything at the parent level (i.e. use a v-for over the retrieved array and construct the list of elements and inputs directly in the parent and not use a child component at all), but I understand that it's not really the vuejs way.
I'm not very familiar with child components, but I think if it was just static data I could just declare props in the child component, and fill it from the parent.
However what I kind to need to do is fill the child component from the parent, but also allow changes from within the child component.
Could someone please describe the correct way to achieve this?
Thanks
You can use inputs on child components. The pattern is like this (edit it's the same pattern for an array of strings or an array of objects that each have a string property as shown here):
data: function() {
return {
objects: [ { someString: '' }, { someString: '' } ]
}
}
<the-child-component v-for="(object, i) in objects" :key="i"
v-model="object.someString"
></the-child-component>
Then, in the child component:
<template>
<div>
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</div>
</template>
export default {
name: 'the-child-component',
props: ['value'],
}
See it described here: https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Cannot get DOM events on component in host component

I have a Vue component that contains a list of objects named lines. I build a table from those lines using different components based on the line type. This works perfectly. Here's a stripped down version of the component:
<template>
<table>
<tr v-for="line in lines"
:key="line.key"
:is="componentForType[line.eventType] || 'LogLine'"
v-bind="line"
/>
</table>
</template>
<script>
export default {
name: 'DebugLog',
components: {
LogLine,
FormattedLogLine,
UserDebug,
Limits
},
data () {
return {
lines: [],
selectedKey: null,
componentForType: {
'USER_DEBUG' : 'UserDebug',
'LIMIT_USAGE_FOR_NS' : 'Limits',
'EXCEPTION_THROWN' : 'FormattedLogLine',
'FATAL_ERROR' : 'FormattedLogLine'
}
}
},
mounted() {
// code that loads this.lines
}
}
</script>
Now I want to be able to click any row of the table, and have the row become "selected", meaning that I want store line.key in this.selectedKey and use CSS to render that line differently. But I can't get the events working. Here's the updated <template>; nothing else is changed:
<template>
<table>
<tr v-for="line in lines"
:key="line.key"
:is="componentForType[line.eventType] || 'LogLine'"
v-bind="line"
:class="{selected: line.key == selectedKey}"
#click.capture="selectedKey = line.key"
/>
</table>
</template>
I've added the last 2 properties on the tr element - a dynamic class binding and a click event handler to set this.selectedKey to the active line's key. But it isn't working. I replaced the #click handler code with console.log(line.key) and nothing is logged, which tells me that my #click handler is never firing. I originally wrote it with out the .capture modifier, but tried adding the modifier when the original didn't work.
Is vue.js stopping propagation from the child component to the parent? Can I not bind the click event on the tr since it :is another vue component? Or is there something else going on? The examples I've found in the docs are much simpler and I'm not sure they correspond to my situation. The various child components are not binding any click events. I'd prefer to handle the event entirely in the parent as shown, since I will have a number of types of child component, and I don't want to have to implement click handlers in each.
Update: Looking at my child components, I note that each contains a tr tag that must effectively replace the tr in the parent template. For example, my most basic component is LogLine, shown here:
<template>
<tr>
<td>{{timeStamp}}</td>
<td>{{eventType}}</td>
<td>{{lineNumber}}</td>
<td>{{lineData}}</td>
</tr>
</template>
<script>
export default {
name: 'LogLine',
props: ['timeStamp', 'eventType', 'lineData', 'lineNumber'],
data: function () {
return {}
}
}
</script>
So I'm guessing that the binding in the parent isn't actually binding on the tr in the DOM; it's just binding on the Vue component, listening for a click event to be sent from the child with $emit; and that each child component will need to bind #click on its tr and emit it to the parent. Assuming I'm right, is there any shortcut I can use from the parent template to have vue forward the DOM events? Any other option I'm missing besides binding click in every child component?
Piggy-backing off of Jacob's answer here. Since you're essentially attaching an event listener to a dynamic component it expects a custom click event. So you have two options here:
Listen for the native DOM click event within that component (by attaching a click event listener to a normal DOM element within the component) and emit a custom click event to the parent.
Use the .native modifier to listen for the native DOM click event instead of a custom one directly in the parent.
Since you are using an :is prop, it's considered a dynamic Vue component, not a DOM element.
Events listener on a Vue component won't be passed down to its DOM element by default. You have to do it manually by going into the component template and add v-on="$listeners".
demo: https://jsfiddle.net/jacobgoh101/am59ojwx/7/
e.g. <div v-on="$listeners"> ... </div>
#Jacob Goh's use of v-on="$listeners" is simple and allows forwarding of all DOM events in one action, but I wanted to document an approach I tried on my own for completeness. I will be switching to Jacob's solution in my component. I am now using Husam's .native modifier in the parent as it is more suitable to my particular use case.
I was able to make my component work by editing each child component, capturing the click event and re-emitting it. For example:
<template>
<tr #click="$emit('click')">
<td>{{timeStamp}}</td>
<td>{{eventType}}</td>
<td>{{lineNumber}}</td>
<td>{{lineData}}</td>
</tr>
</template>
<script>
export default {
name: 'LogLine',
props: ['timeStamp', 'eventType', 'lineData', 'lineNumber'],
data: function () {
return {}
}
}
</script>

How to encapsulate / wrap a VueJS component?

Hi everybody, please pardon my english :-)
I have a Vue component that can take dynamic slots (the names of the slots will depend on a props).
I use it on several places and some of the slots are always present.
To avoid redundancy, I'm looking for a way to create a component that "wrap" the final component to allow me to define only the additionals slots.
If there is an "obvious" way to achieve it, I may have missed it :-)
Code example
Without a "wrap component"
<b-table
show-empty
small
hover
[...some others and always present props...]
:items="aDataVarThatWillChangeBasedOnTheContext"
[...some others and uniq props...]
>
<template slot="same-1">
A slot that will always be present with the same content (for example, a checkbox in the first column)
</template>
<template slot="same-2">
A slot that will always be present with the same content (for example, some action buttons in the last column)
</template>
[...some others and always present slots...]
<template slot="not-the-same">
A slot that is only used in this context (for example, a duration based on a row timestamp and a timestamp picked by the user)
</template>
[...some others and uniq slots...]
</b-table>
With a "wrap component"
<my-b-table
:items="aDataVarThatWillChangeBasedOnTheContext"
>
<template slot="not-the-same">
A slot that is only used in this context (for example, a duration based on a row timestamp and a timestamp picked by the user)
</template>
</my-b-table>
Note: The dynamic slot name is not predictible.
If I suddenly need a "foo" column, I should be able to pass a "foo" slot (and a "HEAD_foo" slot, in my case)
Some researches
I read here that:
They’re (the functionnal components) also very useful as wrapper components. For example, when you need to:
Programmatically choose one of several other components to delegate to
Manipulate children, props, or data before passing them on to a child component
And "Manipulate children, props, or data before passing them on to a child component" seems to be exactly what I need.
I looked on render function but a lot of things seems to be not implemented, like the v-model, and I have difficulties to figure out how to pass dynamic slots...
Thank you in advance for your(s) answer(s) !
up: At the 07.03.2018 I still dont have any idea about how to solve this case
Found the answer that was somehow unclear to me that month ago.
("Dynamic" means here "not explicitely declared by the component, but gived by the parent")
Wrapper component
Props and scoped slots can be gived dynamically by the options object of createElement function.
"Simple" Slots can be gived dynamically by the childs array of createElement function.
Wrapped component
Props can't be dynamic unless the component is functional.
Slots can always be retrieved dynamically.
Scoped slots can be retrieved only if the component isn't functional.
Conclusion
It's not possible to have dynamics props and scoped slots at the same time...
But it's possible to declare all the needed props and then to use a "non-functionnal" component as wrapper and as wrapped.
How to
Retrieve from non-functional component
var component = Vue.component('component-name', {
props: ['name', 'of', 'the', 'props'],
// [...]
aMethod: function () {
this._props // all the declared props
this.$slots // all the slots
this.$scopedSlots // all the scoped slots
}
});
Retrieve from functional component
var component = Vue.component('component-name', {
functional: true,
render: function (createElement, context) {
context.props // all the props
context.children // all the slots as an array
context.slots() // all the slots as an object
}
});
Give to child component
var component = Vue.component('component-name', {
render: function (createElement) {
return createElement(
childComponent,
{
props: propsToGive,
scopedSlots: scopedSlotsToGive
},
[
// non-scoped slots to give
createElement('slot-tag-name', {slot: 'slot-name'})
]
);
}
});
References
https://v2.vuejs.org/v2/guide/render-function.html
https://v2.vuejs.org/v2/guide/render-function.html#createElement-Arguments
https://v2.vuejs.org/v2/guide/render-function.html#Functional-Components
Sandbox
https://jsfiddle.net/5umk7p52/
Just make a regular component out of your customized <b-table>.
You'll need to define an items prop for your component to pass as the items for the <b-table> component.
And, to define a slot for your component, you'll need to use the <slot> tag, specifying the name using the name attribute.
If you'd like to make one of the slots from the <b-table> component accessible in the <my-b-table> component, simply pass a <slot> tag as the content of the slot in your custom component.
It would look something like this:
Vue.component('my-b-table', {
template: `
<b-table
show-empty
small
hover
:items="items"
>
<template slot="same-1">
Content to pass to the b-table's slot
</template>
<slot name="not-the-same">
A slot that is only used in this context
</slot>
<template slot="last_edit">
<slot name="last_edit">
A slot to pass content to the b-table component's slot
</slot>
</template>
</b-table>
`,
props: { items: Array },
});

Delete a Vue child component

I'm really stuck on this one.I have created a Vue (2.0) component that is made up of child components, it's all being Webpacked etc. For example, this is the parent:
<div>
<h1>This is just a title for lulz</h1>
<rowcomponent v-for="row in rows" :row-data="row"></rowcomponent>
</div>
which has a prop of rows passed to it which looks something like this:
rows: [{ sometext: "Foo"} , { sometext: "Bar" }]
So my row component looks like this:
<div>{{ this.sometext }} <button #click="deleteRow">Delete Row</button></div>
And I feel like it should be really easy to set a method on the rowcomponent that is something like this:
deleteRow() {
this.delete();
}
Do I need to $emit something to the parent with the index of the row in it to remove it? It's driving me crazy. All the examples seem to suggest it would be easy to do, but not in the case where you have child components that want to delete themselves.
Yes, the child component cannot delete itself. The reason is because the original data for rows is held in the parent component.
If the child component is allowed to delete itself, then there will be a mismatch between rows data on parent and the DOM view that got rendered.
Here is a simple jsFiddle for reference: https://jsfiddle.net/mani04/4kyzkgLu/
As you can see, there is a parent component that holds the data array, and child component that sends events via $emit to delete itself. The parent listens for delete event using v-on as follows:
<row-component
v-for="(row, index) in rows"
:row-data="row"
v-on:delete-row="deleteThisRow(index)">
</row-component>
The index property provided by v-for can be used to call the deleteThisRow method, when the delete-row event is received from the child component.