Angular 2. How dynamically created component could call by itself the destroy method? - angular2-components

I got stuck with the destroying of dynamic components. I would be really appreciate for some hints.
Here my root component, it adds perfectly on a page some components from a service:
/*Root*/
#Component({
selector: 'convertors',
template: "<div #target></div>"})
export class AppComponent {
#ViewChild('target', {read: ViewContainerRef}) target: ViewContainerRef;
private componentRef: ComponentRef<any>;
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef){}
addComponent(){
let someComponent = this.service.getService("someComponent");
const factory = this.componentFactoryResolver.resolveComponentFactory(someComponent);
this.componentRef = this.target.createComponent(factory);
}}
Here my child component which is added by root component. It has to be self destroyed:
#Component({
selector: 'convertors',
template: "<button (click)="deleteComponent()" >Delete</button>"})
export class someComponent{
deleteComponent(){
/*How could I implement this function?*/
}
}
How could I implement the method deleteComponent() ?
Thank you!

Ok, my solution uses message service. Look here if you don't know what it does. it's very simple.
Firstly, I assign unique id to any dynamic component. Next I keep reference on the component and his id in a map.
private componentRef: ComponentRef<Component>;
onComponent(event: string){
let component = "ref to any component"
const factory = this.componentFactoryResolver.resolveComponentFactory(component);
let componentRef = this.target.createComponent(factory);
let id = "id" + this.index;
(<any>componentRef.instance).componentId = id;
this.idMap.set(id, componentRef);
this.componentRef = componentRef;
this.index += 1;
}
Secondly, event click on child's view sends his id to parent through message service. Take in account that "componentId: string" declared in a child. But it is assigned in the parent!
<a (click)="deleteComponent()"></a>
private componentId: string;
deleteComponent() : void {
this.messageService.sendMessage(this.componentId);
}
Lastly, our parent receives from message service the id. Next, it looks for child's reference in the map. Finally, we call native method this.target.remove("child index in componentRef") to remove component. Take in account, that componentRef has his own indexes of his children.
this.subscription = this.messageService.getMessage()
.subscribe(message => this.removeChild(message))
removeChild(message){
let component = this.idMap.get(message.id);
let indice = this.target.indexOf(component);
this.target.remove(indice);
this.idMap.delete(message.id);
}
If know how improve this solution write in commments. I have the feeling that it could be better. Thanks.

Related

Passing data to an ancestor component vue3

I have a multi-step form in Vue3 that has the following structure:
<Formroot />
...
<formPage2 />
...
<postcodeWidget />
I have a data structure in my formRoot that holds all the field values from the child components and then uses them to make an external API call and present a result.
I use Props to pass the data down to the child components and then emit from the children to the parent.
The issue is, my autocomplete widget - which pulls from an external api - does all the autocomplete in the setup() function. I cannot figure out the best way to communicate input from that widget back up to the formRoot component.
I tried emitting from the widget but I can't access the instance from within setup, and I can't seem to access the data from setup variables within an instance method.
For example, I have a function called changePostcode that fires on input to the field:
methods: {
changePostcode(e){
//I have tried calling the input event:
this.$emit('update:postcode', e.target.value)
//I have tried accessing my setup variable:
this.$emit('update:postcode', this.selectedPostcode) //or postcode.value this is the actull value I want to emit.
//these dont work.They return nothing.
},
}
my selectedPostcode variable is set in the setup() function as follows:
setup() {
...
let selectedPostcode = ref('')
let searchTerm = ref('')
...
// searchTerm is used in a filter with data from an external API to offer suggestions. This is the ultimate source of the "location" object
const selectPostcode = (location) => {
selectedPostcode.value = location.postcode
searchTerm.value = location.locality
}
return {
searchTerm,
...
selectPostcode,
selectedPostcode,
...
}
}
I have a locality and a postcode variable because I want to populate the input with a "locality" that includes the full name of the suburb while I want to emit only the post/zip code.
My setup does a bunch of other work including calling and api for a list of suburb and filtering on user input to make suggestions. That all works fine.
In summary,
A multi step form
One step includes a nested component that needs to pass data up to the root ancestor
I cannot seem to access/emit data from setup() back up to the ancestor element
What is the right way to do this? It seems like it should be a pretty common use case.
I looked into provide/inject as well but I also couldn't understand how to send data back up to the ancestor only down to the child.
The ancestor could provide a function (e.g., a setter) that the nested component could inject to communicate a value back to the ancestor:
// RootAncestor.vue
<script setup>
import { ref, provide } from 'vue'
const postCode = ref('initial value')
const setPostCode = value => {
postCode.value = value
}
provide('setPostCode', setPostCode)
</script>
// NestedComponent.vue
<script setup>
import { inject } from 'vue'
const setPostCode = inject('setPostCode')
const save = () => {
setPostCode('12345')
}
</script>
demo

How to avoid rerendering all child components which are created by v-for directive

There is a list of child component
<question-list-item
v-for="(item, index) in questionListParsed"
:key="item.id"
:both-question="item"
:class-id="classId"
:subject-id="subjectId"
:index="index+1"
/>
and the questionListParsed is a getter in vuex.
/**************************************************************************
* getters
**************************************************************************/
get questionListParsed(): QuestionListItemRes[] {
const { questionList, showingOriginalQuestion } = this
const questionListParsed = questionList.map((e) => {
const recommendQuestion = e.recommendedQuestions[0]
const recommendQuestionIds = showingOriginalQuestion[e.questionNumber]
let arr = []
if (recommendQuestionIds) {
arr = recommendQuestionIds.filter((item) => {
return !this.removedRecommendQuestionIds.includes(item)
})
}
return {
recommendQuestion: {
...recommendQuestion,
stem: recommendQuestion.question,
knowledges: splitMultiKnowledge(recommendQuestion.knowledge),
questionSourceList: recommendQuestion.sources,
categoryId: recommendQuestion.categoryId,
},
originalQuestion: {
...e,
id: e.questionNumber,
stem: e.question,
difficulty: e.complexity,
knowledges: splitMultiKnowledge(e.knowledge),
},
id: recommendQuestion.id,
questionSimilarId: e.questionNumber,
mistakeAnswerId: e.id,
targetExerciseId: e.targetExerciseId,
status: recommendQuestion.status,
}
})
return questionListParsed
}
and the questionListParsed is mainly depends on the state questionList whitch is the originnal data from server side. Now i change questionList by the following way
#Mutation
updateQuestionListByIndex(data: UpdateParams): void {
if (data.value) {
const temp = [...this.questionList]
temp[data.index] = data.value
this.questionList = temp
}
}
and commit the mutation inside an Action like these
this.context.commit('updateQuestionListByIndex', {
index: targetIndex,
value: originQuestion[0],
})
I just want to change one item in the array questionList and then questionListParsed changed.
The expectation is that only one component updated but all of the child component updated(use console.log('updated') in its updated hocks).
How to do that?
The reason why all components are updated is because you use computed property (Vuex getters are Vue computed properties).
Whenever anything in questionList is changed, questionListParsed is recomputed and because you are using map and generating new objects, the result is a new array with completely new objects --> every child in list is updated
I would not consider it a problem because in reality only the DOM elements of the changed item are updated (that is the beauty of virtual DOM). If you do see some performance problem, the way around it is to stop using computed/getters and instead do the transformation only once when data is loaded and continue to work only with questionListParsed
You don't need to prevent the child components from rerendering, Vue does that for you. By providing a unique key to each list element :key="item.id" you give Vue a hint about the item, so Vue can identify and reuse the already rendered parts.
See https://v2.vuejs.org/v2/api/#key for more information.

angular (version 8) pass async #input value to fetch another call

I have a parent component that has a button when the user clicks it will make a request and passed the data into a child component (a ngx-bootstrap modal). The child component will use the async data to make another request to render its UI.
My parent component is like this:
<Parent>
<child-modal [subSectionInfo]="subSectionInfo"></child-modal>
</Parent>
the subSectionInfo is async data retrieved from an ajax in Parent component.
My child component will make call like this:
ngOnInit() {
const id = this.subSectionInfo.id;
this._fetchData(id)
}
private _fetchData(id: number) {
this.service
.getData(id).pipe(
.subscribe((data: any) => {
this.list = data;
});
}
However, this always gives me undefined which causes the child component ajax call failed. Is any way to solve this issue? Thank you so much in advance!
Currently, your code does not wait on the first request to complete. While the request for subSectionInfo is being made in the parent component, it's value is undefined since the value hasn't returned from the server yet. This is why the child component get's undefined. There are 2 ways to approach this.
In your parent component, you can wait until the data is loaded and then pass in subSectionInfo to your child component.
In your child component, you can make use of the ngOnChanges life-cycle hook. This is where your child component implements OnChanges. You can read more about it here.
ngOnChanges(changes: SimpleChanges) {
if (changes['subSectionInfo'].currentValue !== undefined) {
const id = this.subSectionInfo.id;
this._fetchData(id)
}
}
private _fetchData(id: number) {
this.service
.getData(id).pipe(
.subscribe((data: any) => {
this.list = data;
});
}
Hope this helps

watch props update in a child created programmatically

I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".

i am trying to bind the details of an object in a component but i am unable to capture the object?

in the component if i try to do console.log(patient) is see a xml represented object is coming into my console but if i try to bind the data in my component from the returned object i am unable to for suppose in my component in the template section i am doing {{patient.Patient_Name}} but i am unable to see the data in the component?
#Component({
selector: 'dashboard-thumbnail',
template: <h2>Name:{{patients.Patient_Name}}</h2>
})
export class DashboardThumbnailComponent {
patients : Patient
constructor(private dashboardService : DashboardService, private activatedRoute: ActivatedRoute){}
ngOnInit(){
this.dashboardService.getPatient(+this.activatedRoute.snapshot.params['id']).subscribe((patient) => {
this.patients.Patient_Name = patient.Patient_Name;enter code here
console.log(patient);
})
}
}
I understand after lot of research that the return type of my getPatient method is an array so to bind the data i had to loop through the patients object instead of directly binding it. hope this helps!!
enter code here<div *ngFor = "let patient of patients" class = "well hoverwell thumbnail" >
<h2>Name : {{patient.Patient_Name}}</h2>