I have a parent component which has an accordion panel (with multiple/dynamic number of rows)
{{#my-accordion accordionPanels=accordionPanels as |accordion| }}
{{my-details-row section=accordion.props.section removeRow='removeRow'}}
{{/my-accordion}}
The corresponding JS is as below;
accordionPanels: function() {
var accordionPanels = [];
var self = this;
var myRows = this.get('section').myRows;
myRows.forEach(function(myRow) {
accordionPanels.pushObject({
panel: {
name: myRow.rowId,
props: {
section: myRow
}
}
});
});
return accordionPanels;
}.property('section.myRows'),
actions: {
removeRow: function(row){
var numberContainers = this.get('section').myRows.length;
for (var i = 0; i < numberContainers; i++){
if(this.get('section').myRows.contains(row.name)){
console.log("row found!");
this.get('section').myRows.removeObject(row.name);
}
}
},
}
The child component (my-details-row) code is as below
actions: {
removeRow: function(row){
this.sendAction('removeRow', row);
}
}
The child hbs is as below;
<div class="dataBlockItem">
{{my-field field=(field-from-section section "SECTION_NAME" "FIELD_NAME") }}
</div>
{{my-button label="Remove" action="removeRow"}}
Now when the Remove button is clicked, I want the corresponding row to be removed. While I do get the action in the parent (passed from child),
even after executing the line
this.get('section').myRows.removeObject(row.name);
The UI does not get updated (i.e. the data changes in the parent do not reflect in the child component)
Do I need to write additional code/logic to be able to reflect the changes on the UI ?
You are on the right track. You should be able to use closure actions to help simplify connecting the parent and child component actions. Please see the below code and a very basic example Ember Twiddle at the link below. Also, you may have seen this, but just in case here is a link to the Ember.js guides that provides an explanation of component actions. Ember Component Actions -version 2.15
Parent component.hbs
{{#my-accordion accordionPanels=accordionPanels as |accordion| }}
{{my-details-row section=accordion.props.section removeRow=(action 'removeRow' accordion)}}
{{/my-accordion}}
Parent component.js
--here the row can be removed by simply passing the row object itself .removedObject(row)
actions: {
removeRow: function(row){
var numberContainers = this.get('section.myRows').length;
for (var i = 0; i < numberContainers; i++){
if(this.get('section.myRows').includes(row.name)){
console.log("row found!");
this.get('section.myRows').removeObject(row);
}
}
},
}
Child component.hbs
--here tie the removeRow action to the button component's click event
<div class="dataBlockItem">
{{my-field field=(field-from-section section "SECTION_NAME" "FIELD_NAME") }}
</div>
{{my-button label="Remove" click=removeRow}}
Child component.js
--here the removeRow function does not have to be defined.
actions: {
// No need to define the removeRow function
}
Example Ember Twiddle --using ember.js#2.2.2 to show the rough compatibility of the above approach
Related
How can i pass a Javascript Variable to a Vue Component?
I have this jQuery function which generates the menu and pushes all the Values inside the array menu:
var menu = [];
$(document).ready(function(){
$('.service-desc-wrap h2,.cta-button').each(function(){
if($(this).hasClass('cta-button')) {
if($(this).attr('title') && $(this).attr('href')) {
var linkTitle = $(this).attr('title');
var href = $(this).attr('href');
data = [
title = linkTitle,
href = href,
]
menu.push(data);
$('#menu, #menuMobile').append('<a class="menuText" href="'+href+'">'+linkTitle+'</a>')
};
} else {
var tag = $.slugify($(this).text());
$(this).attr('id',tag);
var linkTitle = $(this).text();
if($(this).attr('title')) {
var linkTitle = $(this).attr('title');
};
data = [
title = linkTitle,
href = tag,
]
menu.push(data);
$('#menu, #menuMobile').append('<a class="menuText" href="#'+tag+'">'+linkTitle+'</a>')
}
});
});
I want to pass the array to a Vue Component called
<service-menu-component></service-menu-component>
The jQuery Function and the Component are inside a blade.php file, i'm using Laravel as a backend.
Any Vue component has access to the global scope (a.k.a window object), in which $ performs. You don't have to do anything special about it. In simpler words, if a variable has been declared in global scope at the time your Vue component is created - Vue can access it. But Vue won't react to later mutations performed on the contents of that variable. Not out of the box, anyway.
In Vue, that behavior is called reactivity. If that's what you want, you could use Vue.observable():
declare a const, holding a reactive reference (store.menu in this example - name it to whatever makes sense to you)
use a computed in your Vue component, returning the reactive reference
at any point, (before or after Vue instance's creation) modify the reference from anywhere (including outside Vue component/instance) and the Vue instance will get the change
Proof of concept:
Vue.config.productionTip = false;
Vue.config.devtools = false;
// you don't need the above config, it just suppresses some warnings on SO
// declare store, with whatever contents you want:
const store = Vue.observable({menu: []});
// optionally push something to menu:
// works before Vue instance was created
store.menu.push({foo: 'bar'});
$(function() {
// optionally push something to menu
// also works after Vue instance was created - i.e: 3s after $(document).ready()
setTimeout(function() {
store.menu.push({boo: 'far'});
}, 3000)
})
new Vue({
el: '#app',
computed: {
menu() {
// this injects the store's menu (the reactive property) in your component
return store.menu
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.js"></script>
<div id="app">
<pre v-text="menu"></pre>
</div>
The computed doesn't have to be on the root Vue element (it can be inside your <service-menu-component> component). The above is just a basic implementation, to demo the principle.
Use props:
<service-menu-component :data=YOUR_ARRAY></service-menu-component>
In the component:
props: ['data'] // the data being passed.
Yes, It's possible you need to import this jQuery snippet file to your parent component, and then pass it down to your service-menu-component.
Here's how the code should look like:
Your Parent.vue
<template>
<service-menu-component :data=menu></service-menu-component>
</template>
<script src="<pathToYourJqueryFile>"></script>
And then in your service-menu-component:
<template>
<div>
{{ menu }}
</div>
</template>
<script>
export default {
name: 'service-menu-component',
props: {
menu: {
type: Array
}
}
}
</script>
What made it work and seemed simple after trying different things is moving the jQuery function inside the component mounted hook like this:
mounted() {
var menu = [];
$(document).ready(function(){
$('.service-desc-wrap h2,.cta-button').each(function(){
if($(this).hasClass('cta-button')) {
if($(this).attr('title') && $(this).attr('href')) {
var linkTitle = $(this).attr('title');
var href = $(this).attr('href');
data = {
'title': linkTitle,
'href': href,
}
menu.push(data);
$('#menu, #menuMobile').append('<a class="menuText ml-2" href="'+href+'">'+linkTitle+'</a>')
};
} else {
var tag = $.slugify($(this).text());
$(this).attr('id',tag);
var linkTitle = $(this).text();
if($(this).attr('title')) {
var linkTitle = $(this).attr('title');
};
var data = {
'title': linkTitle,
'href': tag,
}
menu.push(data);
$('#menu, #menuMobile').append('<a class="menuText ml-2" href="#'+tag+'">'+linkTitle+'</a>')
}
});
console.log(menu);
});
this.menu = menu;
},
It worked like a charm.
#Şivam Kuvar verdiği örnekteki gibi :data='menu' yazan yeri :data="json_encode($controllerdata)" ile değiştir ve verilerini kullanmaya hazırsın. veri yapına göre #{{ data.data[0].title }} olabilir örneğin veya #{{ data.title }} bu gelen veriye bağlı dostum.
Just like #Şivam Kuvar's example, replace :data='menu' with :data="json_encode($controllerdata)" and you're ready to use your data. According to the data structure, it can be #{{ data.data[0].title }} for example or #{{ data.title }} it depends on the incoming data, my friend.
I am using ng2-dragula for drag and drop feature. I am seeing issue when I drag and drop first element(or any element) at the end and then try to add new item to the array using addNewItem button, new item is not getting added to the end. If i don't drop element to the end, new item is getting added at the end in UI.
I want new items to be displayed at the bottom in any scenario. Any help is appreciated.
This issue is not reproducible with Angular 7. I see this happening with Angular 9
JS
export class SampleComponent {
items = ['Candlestick','Dagger','Revolver','Rope','Pipe','Wrench'];
constructor(private dragulaService: DragulaService) {
dragulaService.createGroup("bag-items", {
removeOnSpill: false
});
}
public addNewItem() {
this.items.push('New Item');
}
}
HTML
<div class="container" [dragula]='"bag-items"' [(dragulaModel)]='items'>
<div *ngFor="let item of items">{{ item }}</div>
</div>
<button id="addNewItem" (click)="addNewItem()">Add New Item
I edited the stackblitz from the comment to help visualize the issue. This seems to be triggered when a unit is dragged to the bottom of the list. Updated stackblitz : https://stackblitz.com/edit/ng2-dragula-base-ykm8fz?file=src/app/app.component.html
ItemsAddedOutOfOrder
You can try to restore old item position on drop.
constructor(private dragulaService: DragulaService) {
this.subscription = this.dragulaService.drop().subscribe(({ name }) => {
this.dragulaService.find(name).drake.cancel(true);
});
}
Forked Stackblitz
Explanation
There is some difference between how Ivy and ViewEngine insert ViewRef at specific index. They relay on different beforeNode
Ivy always returns ViewContainer host(Comment node)ref if we add item to the end:
export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
null {
const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
if (nextViewIndex < lContainer.length) {
const lView = lContainer[nextViewIndex] as LView;
const firstTNodeOfView = lView[TVIEW].firstChild;
if (firstTNodeOfView !== null) {
return getFirstNativeNode(lView, firstTNodeOfView);
}
}
return lContainer[NATIVE]; <============================= this one
}
ViewEngine returns last rendered node(last <li/> element)ref
function renderAttachEmbeddedView(
elementData: ElementData, prevView: ViewData|null, view: ViewData) {
const prevRenderNode =
prevView ? renderNode(prevView, prevView.def.lastRenderRootNode!) : elementData.renderElement;
...
}
The solution might be reverting the dragged element back to original container so that we can let built-in ngForOf Angular directive to do its smart diffing.
Btw, the same technique is used in Angular material DragDropModule. It remembers position of dragging element and after we drop item it inserts it at its old position in the DOM which is IMPORTANT.
I have written a code in which I am creating a treeview structure in angular 2.
Parent Component HTML:
<child-component [data]="tree"></child-component>
Parent Component.ts file has the following:
tree: TreeData[] = [];
treeData: TreeData;
ngOnInit() {
this.tree.push({answer:'first',child:[]});
this.treeData = this.tree[0];
}
I have created a class in the common.ts file which has answer(input in the textbox) and child(it will get populated recursively when add node is clicked) :
export class TreeData {
answer: string;
child: TreeData[];
constructor(answer: string, child: TreeData[]) {
this.answer = answer;
this.child = child;
}
}
Child component's html:
<li *ngFor="#eachChild of data ; let i = index">
<input type="text" [(ngModel)]="inputData" required/>
<child-component [data]="eachChild.child"></child-component>
</li>
Child component's ts file:
#Input() data : TreeData[];
inputData: any = null;
eachChild: TreeData;
add(inputData: any, index: number) {
this.data[index].child.push({answer: inputData, child: []});
}
Current output treeview
:The output looks like this when clicked on add Node, a new text box gets added below
The actual output should be like a tree structure (when add node is clicked, new textbox gets added as its child)
The issue is that the object gets refreshed on every click and is not able to retain the previous state. The contents should get appended but this seems like a new instance is created every time. Any insight on how I can proceed with this to create a treeView in angular 2?
I am trying to write a custom directive that if the user selects the "All" option on a drop down it automatically selects all the options I have been able to get my custom directive selecting all the options but this does not update the model on the consuming component.
Custom Directive
#Directive({ selector: '[selectAll]' })
export class SelectAllDirective {
private selectElement: any;
private selectedItemsd:number[];
constructor(private el: ElementRef) {
this.selectElement = el.nativeElement;
}
#HostListener('change')
onChange() {
if (this.selectElement.options[0].selected) {
for (let i = 0; i < this.selectElement.options.length; i++) {
this.selectElement.options[i].selected = true;
}
}
}
}
And the template
<select selectAll multiple
ngControl="shoreLocation"
#shoreLocation="ngModel"
id="shoreLocation"
[(ngModel)]="customTask.shoreLocations"
name="shoreLocation"
class="form-control multi-select"
required
style="height:150px">
I have tried creating an #Input and using the banana in a box syntax to try and update the model, I wasn't succesful with that.
I was able to use #Output and emit an event that the consuming component could handle similar to https://toddmotto.com/component-events-event-emitter-output-angular-2 but I would prefer if I could simply update the model automatically.
I am wondering if its possible? Or if creating a custom component similar to http://plnkr.co/edit/ORBXA59mNeaidQCLa5x2?p=preview is the better option.
Thanks in advance
Setting the selected state in Javascript doesn't send the "changed" event on the select component (probably because it is direct access to the child option, but not sure). Try triggering the changed event yourself by doing this:
if (this.selectElement.options[0].selected) {
for (let i = 0; i < this.selectElement.options.length; i++) {
this.selectElement.options[i].selected = true;
}
//These are the two new lines
let changeEvent = new Event("change", {"bubbles":true, "cancelable": false});
this.selectElement.dispatchEvent(changeEvent);
}
And see if that triggers the ngModel to get updated.
How can I find the vue.js component corresponding to a DOM element?
If I have
element = document.getElementById(id);
Is there a vue method equivalent to the jQuery
$(element)
Just by this (in your method in "methods"):
element = this.$el;
:)
The proper way to do with would be to use the v-el directive to give it a reference. Then you can do this.$$[reference].
Update for vue 2
In Vue 2 refs are used for both elements and components: http://vuejs.org/guide/migration.html#v-el-and-v-ref-replaced
In Vue.js 2 Inside a Vue Instance or Component:
Use this.$el to get the HTMLElement the instance/component was mounted to
From an HTMLElement:
Use .__vue__ from the HTMLElement
E.g. var vueInstance = document.getElementById('app').__vue__;
Having a VNode in a variable called vnode you can:
use vnode.elm to get the element that VNode was rendered to
use vnode.context to get the VueComponent instance that VNode's component was declared (this usually returns the parent component, but may surprise you when using slots.
use vnode.componentInstance to get the Actual VueComponent instance that VNode is about
Source, literally: vue/flow/vnode.js.
Runnable Demo:
Vue.config.productionTip = false; // disable developer version warning
console.log('-------------------')
Vue.component('my-component', {
template: `<input>`,
mounted: function() {
console.log('[my-component] is mounted at element:', this.$el);
}
});
Vue.directive('customdirective', {
bind: function (el, binding, vnode) {
console.log('[DIRECTIVE] My Element is:', vnode.elm);
console.log('[DIRECTIVE] My componentInstance is:', vnode.componentInstance);
console.log('[DIRECTIVE] My context is:', vnode.context);
// some properties, such as $el, may take an extra tick to be set, thus you need to...
Vue.nextTick(() => console.log('[DIRECTIVE][AFTER TICK] My context is:', vnode.context.$el))
}
})
new Vue({
el: '#app',
mounted: function() {
console.log('[ROOT] This Vue instance is mounted at element:', this.$el);
console.log('[ROOT] From the element to the Vue instance:', document.getElementById('app').__vue__);
console.log('[ROOT] Vue component instance of my-component:', document.querySelector('input').__vue__);
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<h1>Open the browser's console</h1>
<div id="app">
<my-component v-customdirective=""></my-component>
</div>
If you're starting with a DOM element, check for a __vue__ property on that element. Any Vue View Models (components, VMs created by v-repeat usage) will have this property.
You can use the "Inspect Element" feature in your browsers developer console (at least in Firefox and Chrome) to view the DOM properties.
Hope that helps!
this.$el - points to the root element of the component
this.$refs.<ref name> + <div ref="<ref name>" ... - points to nested element
💡 use $el/$refs only after mounted() step of vue lifecycle
<template>
<div>
root element
<div ref="childElement">child element</div>
</div>
</template>
<script>
export default {
mounted() {
let rootElement = this.$el;
let childElement = this.$refs.childElement;
console.log(rootElement);
console.log(childElement);
}
}
</script>
<style scoped>
</style>
So I figured $0.__vue__ doesn't work very well with HOCs (high order components).
// ListItem.vue
<template>
<vm-product-item/>
<template>
From the template above, if you have ListItem component, that has ProductItem as it's root, and you try $0.__vue__ in console the result unexpectedly would be the ListItem instance.
Here I got a solution to select the lowest level component (ProductItem in this case).
Plugin
// DomNodeToComponent.js
export default {
install: (Vue, options) => {
Vue.mixin({
mounted () {
this.$el.__vueComponent__ = this
},
})
},
}
Install
import DomNodeToComponent from'./plugins/DomNodeToComponent/DomNodeToComponent'
Vue.use(DomNodeToComponent)
Use
In browser console click on dom element.
Type $0.__vueComponent__.
Do whatever you want with component. Access data. Do changes. Run exposed methods from e2e.
Bonus feature
If you want more, you can just use $0.__vue__.$parent. Meaning if 3 components share the same dom node, you'll have to write $0.__vue__.$parent.$parent to get the main component. This approach is less laconic, but gives better control.
Since v-ref is no longer a directive, but a special attribute, it can also be dynamically defined. This is especially useful in combination with v-for.
For example:
<ul>
<li v-for="(item, key) in items" v-on:click="play(item,$event)">
<a v-bind:ref="'key' + item.id" v-bind:href="item.url">
<!-- content -->
</a>
</li>
</ul>
and in Vue component you can use
var recordingModel = new Vue({
el:'#rec-container',
data:{
items:[]
},
methods:{
play:function(item,e){
// it contains the bound reference
console.log(this.$refs['key'+item.id]);
}
}
});
I found this snippet here. The idea is to go up the DOM node hierarchy until a __vue__ property is found.
function getVueFromElement(el) {
while (el) {
if (el.__vue__) {
return el.__vue__
} else {
el = el.parentNode
}
}
}
In Chrome:
Solution for Vue 3
I needed to create a navbar and collapse the menu item when clicked outside. I created a click listener on windows in mounted life cycle hook as follows
mounted() {
window.addEventListener('click', (e)=>{
if(e.target !== this.$el)
this.showChild = false;
})
}
You can also check if the element is child of this.$el. However, in my case the children were all links and this didn't matter much.
If you want listen an event (i.e OnClick) on an input with "demo" id, you can use:
new Vue({
el: '#demo',
data: {
n: 0
},
methods: {
onClick: function (e) {
console.log(e.target.tagName) // "A"
console.log(e.targetVM === this) // true
}
}
})
Exactly what Kamil said,
element = this.$el
But make sure you don't have fragment instances.
Since in Vue 2.0, no solution seems available, a clean solution that I found is to create a vue-id attribute, and also set it on the template. Then on created and beforeDestroy lifecycle these instances are updated on the global object.
Basically:
created: function() {
this._id = generateUid();
globalRepo[this._id] = this;
},
beforeDestroy: function() {
delete globalRepo[this._id]
},
data: function() {
return {
vueId: this._id
}
}