I'm trying to make a schema render component with angular, it depends on dynamic component, just like:
class DynamicComponent {
// ...
private loadComponent(host: ViewContainerRef, component: Type<any>) {
const type = this.componentFactoryResolver.resolveComponentFactory(component);
host.clear();
return host.createComponent(type);
}
}
in some case, a Component's Input can be TemplateRef element:
class SomeComponent {
#Input() public template: TemplateRef<void>;
}
so i want to create TemplateRef element for this case:
class DynamicComponent {
// ...
private loadComponent(host: ViewContainerRef, component: Type<any>) {
const type = this.componentFactoryResolver.resolveComponentFactory(component);
host.clear();
const componentRef = host.createComponent(type);
componentRef.instance.template = ?
}
}
is there any way can do that?
update
let me introduce the SchemaRenderComponent, it accept json schema and render the ui.
the json schema describe the structure of component:
{
type: SomeComponent,
style: {},
props: {
template: OtherComponent
},
events: {},
}
in my design, SomeComponent's Input template is a Component in the json schema, and i will dynamic create TemplateRef for it. In html template looks like:
<SomeComponent [template]="template"></SomeComponent>
<ng-template #template>
<OtherComponent></OtherComponent>
</ng-template>
so i have to find a way to create TemplateRef.
(The TemplateRef is rendered by ngTemplateOutlet directive)
Related
I have a modal component with ng-content. In this ng-content I can pass other components with injector. I need to pass anyway an object inside it for editing data. But I need some help. This is the modal component html:
<div class="modal">
<div class="modal-body">
this is my Object: {{model | json}}
<ng-content></ng-content>
</div>
<div class="modal-footer">
<button (click)="sendMessage()">success</button>
<button>abort</button>
</div>
</div>
The components inside ng-content may vary. In my example I have an input. I need to pass a model inside that components.
I use a service with injector to pass the content into my modalComponent, and in the function "open" I defined a parameter obj that's the object that I need to see into ng-content component. (The parameter is stored inside "model" input on my Modal component).
#Injectable()
export class ModalService {
dialogComponentRef: ComponentRef<ModalComponent>;
private subject = new Subject<any>();
open(content: any, obj: any) {
const contentComponentFactory = this.cfResolver.resolveComponentFactory(content);
const modalComponentFactory = this.cfResolver.resolveComponentFactory(ModalComponent);
const contentComponent = contentComponentFactory.create(this.injector);
const modalComponent = modalComponentFactory.create(this.injector, [[contentComponent.location.nativeElement]]);
modalComponent.instance.model = obj;
this.dialogComponentRef = modalComponent;
document.body.appendChild(modalComponent.location.nativeElement);
this.appRef.attachView(contentComponent.hostView);
this.appRef.attachView(modalComponent.hostView);
}
}
This is my modalComponent Class:
export class ModalComponent {
#Input() model: any;
sendMessage() {
this.modalService.sendMessage();
}
constructor(private modalService: ModalService) {}
}
Therefore I defined a component with this html:
<input type="text" value="name" placeholder="name">
Inside this input I desire to see the object for editing this value.
I can't find a solution for having the object inside the content-component....I need it because I would have a form where I must edit some values.
This is my stackblitz example: Example
What you can do is assign the formData to a property of a service and then read that property in the content component's ngOnInit(), I tried below code and its working for me.
#Injectable()
export class ModalService {
dialogComponentRef: ComponentRef<ModalComponent>;
formData:any;
private subject = new Subject<any>();
open(content: any, obj: any) {
this.formData = obj;
const contentComponentFactory = this.cfResolver.resolveComponentFactory(content);
const modalComponentFactory = this.cfResolver.resolveComponentFactory(ModalComponent);
const contentComponent = contentComponentFactory.create(this.injector);
const modalComponent = modalComponentFactory.create(this.injector, [[contentComponent.location.nativeElement]]);
modalComponent.instance.model = obj;
this.dialogComponentRef = modalComponent;
document.body.appendChild(modalComponent.location.nativeElement);
this.appRef.attachView(contentComponent.hostView);
this.appRef.attachView(modalComponent.hostView);
}
}
and in ngOnInit() of content-component,
ngOnInit() {
this.name = this.modalService.formData.name;
}
However you can try to wrap this property in some function inside service, rather than accessing the property directly(getFormData() or something)
can anyone help me with decorators #Model and #Emit?
I'm trying to change order on click in my component and used documentation from here: https://github.com/kaorun343/vue-property-decorator.
Here is my code:
<template>
<button #click="onSortClick">Sort</button>
</template>
<script lang="ts">
import Vue from "vue";
import { Emit, Componet, Model } from "vue-property-decorator";
export default class MyButton extends Vue {
#Model("sort", { type: String, default: "none" }) readonly order!: string;
#Emit("sort")
onSortClick() {
const nextSortOrder = {
ascending: "descending",
descending: "none",
none: "ascending"
};
return nextSortOrder[this.order];
}
}
</script>
But when I click the button, the value of variable "order" is not changing. Am I doing something wrong?
Yes, you are. There are a few things wrong here.
You need to import vue like this import { Vue, Component, Model, Emit } from 'vue-property-decorator;
The class needs to have an #Component decorator like this
#Component({/* Additional data can go in here */})
export default class MyButton extends Vue {}
This isn't how vue intended emitting of events to work. You cannot alter the value of order because it is a readonly property within the same file. If you place your button in another component like this
// ParentFile.vue
<template>
<my-button #sort="order = $event"></my-button>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import MyButton from '#/components/MyButton.vue';
#Component({
components: {
MyButton
}
})
export default class Home extends Vue {
order = 'Wow';
#Watch('order')
orderChanged(newVal: string) {
// eslint-disable-next-line no-console
console.log(newVal); // order will change here
}
}
</script>
and listen to the emitted event as above then the order variable in the parent component will change but not the child.
I've created a mixin to change the page titles, using document.title and global mixins.
My mixin file (title.ts):
import { Vue, Component } from 'vue-property-decorator'
function getTitle(vm: any): string {
const title: string = vm.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
#Component
export default class TitleMixin extends Vue {
public created(): void {
const title: string = getTitle(this)
if (title) {
document.title = title
}
}
}
Then i registered this mixin globally in main.ts:
import titleMixin from '#/mixins/title'
Vue.mixin(titleMixin)
Then setting up the title in a Vue component:
#Component
export default class Login extends Vue {
public title: string = 'New title'
}
I have like 5 components in my project, if i use console.log in a mixin, i can see that it fired in every component, step by step, thus document.title is set by a last component created() hook.
How to correctly set a title for a CURRENT page?
As you said, a global mixin will affect every component in your Vue app, which means that the logic to set the document.title will fire in the created hook of every component in your app.
I think what you're looking for is VueRouter's beforeRouteEnter hook, which is one of the navigation guards that the library makes available to any of your components. A component's beforeRouteEnter hook fires immediately before the route changes to whichever one it's associated with.
In your case it would look like this:
#Component
export default class TitleMixin extends Vue {
public beforeRouteEnter(to, from, next): void {
next(vm => {
const title: string = getTitle(vm)
if (title) {
document.title = title
}
})
}
}
You'll notice that the next function (which needs to be called for the route to resolve) is being passed a callback which has a reference to the component's instance (vm), which we're passing to getTitle instead of this. This is necessary because the beforeRouteEnter hook does not have a reference to this. You can read the docs I linked to for more info.
Instead of creating a global mixin, try using a route meta field along with a global resolve guard.
First, we'll start by adding a meta field to each RouteConfig in the /router/routes.ts file:
import { RouteConfig } from 'vue-router'
export default [
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: 'login-view' */ '#views/Login.vue'),
meta: {
title: 'Login', // Set the view title
},
},
// ... Add the title meta field to each `RouteConfig`
] as RouteConfig[]
Then, we'll create a global resolve guard, to set the title meta field as the document title, in the /router/index.ts file:
import Vue from 'vue'
import Router, { Route } from 'vue-router'
import routes from './routes'
Vue.use(Router)
const router = new Router({
// ... RouterOptions
})
// Before each route resolves...
// Resolve guards will be called right before the navigation is confirmed,
// after all in-component guards and async route components are resolved.
router.beforeResolve((routeTo, routeFrom, next) => {
const documentTitle = getRouteTitle(routeTo)
// If the `Route` being navigated to has a meta property and a title meta field,
// change the document title
if (documentTitle ) {
document.title = documentTitle
}
// Call `next` to continue...
next()
function getRouteTitle(route: Route): string {
const title: string = route.meta && route.meta.title
if (title) {
return `${title} | site.com`
}
return 'Admin panel | site.com'
}
})
export default router
You should use the mixin only in the parent component for your page (the one that holds all the page itself).
Using your vue-property-decorator should be in this way:
import { Vue, Component, Mixins } from 'vue-property-decorator';
#Component
export default class Login extends Mixins(titleMixin) {
public title: string = 'New title'
}
And do not import it globally with Vue.mixin(titleMixin). In this way it is imported for all the components.
I have a component that has a dynamic view, which I am implementing using an ng-template.
The following is that component, lets say parent.component.ts.
#Component({
...
template: '<div><ng-template child-area></ng-template></div>',
...
})
export class ParentComponent implements OnInit {
#ViewChild(ChildAreaDirective) childArea: ChildAreaDirective;
loadComponent (details) {
let _vcf = this.childArea.viewContainerRef;
let _cf= resolveComponentFactory(childrenList[details.name]);
// How do I pass the options object from here into the child component??
this.childOptions = details.options;
let componentRef = _vcf.createComponent(_cf);
}
}
The child.component.ts is written like this:
#Component ({
template: '<div>{{ title }}</div>'
})
I want to call the loadComponent method like this:
parent.loadComponent({ name: 'choose-doc', options: {title: "Main File"} });
How can I pass the options object into the child component, so that the title gets the value 'Main File'?
componentRef has the attribute instance which is the actual instance of the created component. From this instance, you can access to all public attributes and functions of the component.
You can map your options inside a function on the component or just set them from loadComponent.
Check the class doc: https://angular.io/api/core/ComponentRef#instance
I have a component MyComponent and it is declared like this:
export class MyComponent implements IComponent {
...
#Input() Departments: any;
#Input() DropDownOptions: any;
#Input() Data: any[];
...
}
However, there is no property Data, when I try to access from PersonComponent component.
HTML of PersonComponent component:
<fieldset>
<my-comp #myGrid [Options]="ps.Options['myGrid']"></my-comp>
</fieldset>
TypeScript of PersonComponent component:
export class PersonComponent implements OnInit {
#ViewChild('myGrid') myGridComponent: MyComponent;
ngAfterViewInit() {
debugger;
let localData2 = this.myGridComponent.Data; // NO DATA PROPERTY. Undefined
}
ngAfterContentInit() {
debugger;
let localData1 = this.myGridComponent.Data; // NO DATA PROPERTY. Undefined
}
}
Variables that can be seen at debugger of Chrome:
How can I read values of Data property of MyComponent? What am I doing wrong?
#Input Data ... decorator "receives" data from the parent component. You set it via the attribute [Data] inside the parent template. If you don't set it it will be indefined. On the other hand you have [Options] attribute that doesn't have the corresponding #Input in the child.
You can fix it like so:
<fieldset>
<my-comp #myGrid [Data]="person.data"></my-comp>
</fieldset>
where person is an array with data field in parent component.
Please read thoughtfully the documention https://angular.io/guide/template-syntax#inputs-outputs and https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
And it would be better to not use reserved/too generic name like Data, Options to avoid name collisions and also camel case them.