I am writing an app for angular 2 with ES5.
I want to have a component with dynamically loaded view child, that will load existing components.
I saw examples in TypeScript but I am failing do that in ES5, and to inject ng.core.ViewChild in component constructor based on a ng.core.Directive and update the contents of the DOM element ( marked with that directive) with a dynamically loaded, existent, component.
I've tried with
queries:[
formBody: ng.core.ViewChild('formBody')
]
...and I get a ElementRef, but would need a ViewContainerRef to update DOM contents with a dynamically loaded component.
I've tried :
queries:[
formBody: ng.core.ViewChild(app.FormBodyDirective)
]
... but I get a "empty" object. __proto__ object
Component is loaded like this:
ngAfterViewInit: function() {
var dialogComponentFactory = this.componentResolver.resolveComponentFactory(app.FormBody1_Component);
this.formBody = this.formBody.createComponent(dialogComponentFactory);
},
I have tried to inject ng.core.ViewContainerRef into component constructor:
.Class({
constructor: [
ng.core.ViewContainerRef,
function(viewContainer){
this.formBody = viewContainer
}],
but this of course injects a instance of ng.core.ViewContainerRef for my 'qform' element, and I get the dynamically loaded component at the end of the 'qform' element
Link to plunker with my code (not working) http://plnkr.co/edit/mRxGKYvKy8tHRjupNzju?p=preview
I would be very grateful if someone would help me sort this out, or throw a hint..
Thanks !
I finally find a solution. I am not sure that this is the best and most elegant way, but it works.
I injected ng.core.ViewContainerRef, in directive constructor and saved it's instance into class member.
The directive class member was accessible from within component. It was made available through component's queries option.
The directive:
app.FormBodyDirective =
ng.core.Directive({
selector: '[formBody]'
}).Class({
constructor: [
ng.core.ViewContainerRef,
function(viewContainer){
this._viewContainer = viewContainer;
}]
});
The component:
//...
queries : {
formBody: new ng.core.ViewChild(app.FormBodyDirective)
},
directives:[ app.FormBodyDirective ]
//...
Working plunker: http://plnkr.co/edit/ooWhiMqSFDtGNGdW3LMK?p=preview
Related
This question is somewhat similar to the one that reads the same but it is for Angular 2, How can I use/create dynamic template to compile dynamic Component with Angular 2 the solution presented there is archaic at this point and the methods there don't even exist now. So basically my questions is this.
I am loading HTML from a service, the HTML comes with links like this:
<a routerLink='/content' (click)="loadHTMLData(pageId)">
so to load this properly into a container in my view I need to create a component on the fly and set its templateHTML attribute also dynamically to the received HTML, so that the (click) events work.
My idea was to create this dynamic component so they have an event handler that would trigger an event (#Output) with a notification to be then captured by the host component which in turn would perform another call to the service that returns the HTML and would replace this component for a new one with the new loaded HTML and it would repeat the process for each time a link is pressed in the content.
I have setup the dynamic creation of the component according with the instructions in https://angular.io/guide/dynamic-component-loader, so now I have a dynamic component but I don't know how to load the HTML as its template and have it rendering correctly. (right now the template renders as a string literal)
I tried passing the HTML to a static template using [innerHTML] by first sanitizing it with settings to trust the content, but that didn't work either, the html and styles were interpreted but the links weren't working.
The most relevant code looks like this:
#Component({
selector: 'app-content-info',
templateUrl: './content-info.component.html',
styleUrls: ['./content-info.component.css']
})
export class ContentInfoComponent implements OnInit {
#Input() info: ContenInfo;
#ViewChild(ContentInfoDirective,{static:true}) cInfoD: ContentInfoDirective;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit() {
this.loadComponent();
}
loadComponent() {
const Item = this.info;
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(Item.component);
const viewContainerRef = this.cInfoD.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
(<AdComponent>componentRef.instance).data = Item.data;
}
}
I adapted this from the Angular documentation site (ad-banner.component.ts), but the standard documentation says nothing on how to add a template dynamically.
So basically what I need to know is how can I inject/insert the HTML template with its (click) events in this dynamic component in Angular 8 to make the events work?
The answer that works will be immediately marked as correct.
I have the following html structure where foo and bar are two directives and baz is a custom component.
<div foo>
<div bar></div>
<baz><baz>
<baz><baz>
</div>
The html of the baz-component looks something like ...
<div bar>Yada Yada Yada</div>
... and the foo-directive looks something like this:
// ...
#Directive({
selector: '[foo]'
})
export class FooDirective implements AfterContentInit {
#ContentChildren(BarDirective)
m_bars: QueryList<BarDirective>;
public ngAfterContentInit(): void {
console.log('barCount:', this.m_bars.length);
}
}
The problem I experience is that the length of m_bars inside the FooDirective is 1. It only contains the div that is a direct child of <div foo>. I however expected this number to be 3 (the one direct child of <div foo> and the other div's inside the two baz-components).
Why is that so and how can this problem be solved - if it can be solved at all?
Edit 1:
Changing the ContentChildren decorator to
#ContentChildren(BarDirective, { descendants: true })
m_bars: QueryList<BarDirective>;
doesn't have any effect at all.
It's just not possible, which sucks. See https://github.com/angular/angular/issues/20810#issuecomment-401341413 for further information. I'll quote the tl;dr version for reference:
In short: we only query component's own content, not content coming from other templates. This makes sense since a template forms a namespace #foo in one template might mean one thing and completely different thing in another template. Names might be the same but meaning quite different.
Check this StackBlitz out (watch for console output):
https://stackblitz.com/edit/angular-h7ybb3
You would have to chain that through outputs. QueryList has an Observable for changes, you could try using it as the output. Something along those lines:
#ContentChildren() something: QueryList<Type>;
#Output() queryListChange = new EventEmitter();
ngOnInit() {
this.something.changes.subscribe(v => {
this.queryListChange.emit(v);
}
}
And in the upper component you would need to subscribe to this output:
(queryListChange)="onQueryListChange($event)"
And in onQueryListChange you would need to merge those items you received with component's own ViewChildren.
And pass it along the same way if more levels of nesting is required.
I don't know a way to merge QueryLists so I just got original array from them and merged those in my stackblitz.
I've built a ractive.js app using partials. These partials are loaded via fetch/ajax - and all works nicely.
I then decided I wanted to encapsulate data along with the partial so looked at components - as I understood a component to do just that: Isolate a template/partial with its data.
I then looked to load the components in: http://ractivejs.github.io/ractive-load/
However, I don't really see the advantage of this approach - as it appears with the loader you can only load in the components template, not the entire encapsulated component (data, templates etc). You still have to put the data onto the main ractive instance (as you would with a partial).
I'm trying to dyanamically update the component. I'm also using page.js for routing. I'm trying to separate out all the concerns.
I'm probably not explaining myself very well - here is my code... most of it was taken from martydpx's answer here How to create Ractive's subcomponents dynamically and change them programmatically )
....
<dynamic name='{{name}}'/>
</script>
<script>
// Component loader
Ractive.load({
home: '/components/home.html', // seems this can only contain a template. Is it possible for it to contain everything - data and all?
packs: '/components/packs.html',
....
addplayer: '/components/addplayer.html',
notfound: '/components/notfound.html',
}).then( function ( components ) {
Ractive.components[ 'home' ] = components.home;
Ractive.components[ 'packs' ] = components.packs;
....
Ractive.components[ 'addplayer' ] = components.addplayer;
Ractive.components[ 'notfound' ] = components.notfound;
// dynamically load component based on route
Ractive.components.dynamic = Ractive.extend({
template: '<component/>',
components: {
component: function() {
this.set('foo','bar'); // I can dynamically set the data here.. but how would I add defaults for each component, within the component?
return this.get('route');
}
},
oninit: function(){
this.observe('route', function(){
this.reset();
},
{ init: false}
);
}
});
var r = new Ractive({
el: document.body,
template: '#template',
data: {
route: 'home'
}
});
// Routing. Sets the route... which triggers the component
page('/', index);
...
page();
function index() {
console.log('index');
r.set('route','home')
}
EDIT
I've read this - which has been a great help :)
https://github.com/ractivejs/component-spec/blob/master/authors.md
In the dynamic component scenario - how would I dynamically update component specific data. I seem to be able to do it when the component tag is hardwired into the page... but not when the component tag is dynamically created. After much playing about in the console - its as if it doesn't see the dynamic component. So things like r.findComponent('home').get() don't work.
Yet, if I put a <home/> tag in the template - it does work.
Also, do components automatically 'tear down' when they're un-rendered?
I'm not 100% sure what you are looking for.
First you create a child component -
var MyWidget = Ractive.extend({
template: '<div>{{message}}</div>',
data: {
message: 'No message specified, using the default'
}
});
You register this with Ractive runtime
Ractive.components.widget = MyWidget;
Then you create a parent component
var Parent = Ractive.extend({
template: '<div>
<MyWidget message={{widget}} />
</div>'
});
You use the parent instance to pass the data to child
// Live instance of parent
new Parent({
el: 'id',
data : {
widget: {
message : 'Waddup kiddo'
}
}
});
data.widget gets mapped to MyWidget's data, in-turn gets the message data.
For more info refer this
Generally there are 3 types of components you will be creating & using -
Self-sufficient Components - It knows everything it needs to know by itself. You don't pass anything to it. It creates it's own data or knows where to get it from. Ex: A logo component which knows by itself where to get the image from.
Dumb Components - They have no intelligence and all the data that it needs should be passed from parent. Like in our example - MyWidget has no idea where and what message stands for. Just renders it. No questions asked. Parent will fetch message and just pass it on.
Smart Components - Components which do some heavy lifting. An example would be Profile component. Parent will pass just a profileID to this, and it knows where to get profile data from, does some ajax calls, knows how to parse and interpret the data, may be even starts a socket and listens to changes etc.
So you decide how you want to make your components, who takes responsibility and think about data-encapsulation then.
I've made a component that copies some code from a code box. The component javascript looks like:
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'code',
classNames: ['lm-code-box'],
dataTarget: null,
dataTrigger: Ember.computed('dataTarget',
function() {
return `.${this.get('dataTarget')}`;
}
),
copyAction: null,
icon: 'ion-code',
copyStatus: null,
buttonText: 'Copy',
didInsertElement() {
this.clipboard = new Clipboard('.lm-button--copy');
this.clipboard.on('success',(e) => {
this.set('icon','ion-checkmark');
this.set('copyStatus','success');
this.set('buttonText','Copied');
e.clearSelection();
});
this.clipboard.on('error',(e) => {
this.set('icon','ion-android-warning');
this.set('copyStatus','error');
this.set('buttonText','Error');
});
},
willDestroyElement() {
this.clipboard.destroy();
}
});
Component code looks like:
<a class="lm-button--copy {{buttonClass}}" data-clipboard-target={{dataTrigger}} data-clipboard-action={{copyAction}}>
{{buttonText}}
<i class="icon {{icon}}"></i>
</a>
<pre class="{{dataTarget}}">
{{yield}}
</pre>
Then in my template the code looks like:
{{#lm-code-copy dataTarget="testOne"
copyAction="copy"}}
test one
{{/lm-code-copy}}
{{#lm-code-copy dataTarget="testTwo"
copyAction="copy"}}
test two
{{/lm-code-copy}}
Everything copies fine, but in the block:
this.set('icon','ion-checkmark');
this.set('copyStatus','success');
this.set('buttonText','Copied');
changes those key values on both components that are rendered. How can I tell ember to only change the value for the current component? I assumed this would set that context but it doesn't seem to do the trick.
I'll take a chance here, since you didn't supply your component template. I think you're problem might be with your CSS selector
this.clipboard = new Clipboard('.lm-button--copy');
You're always targeting all .lm-button--copy elements in the page with that selector. Meaning that each component instance will have a separate this.clipboard reference but all pointing to the same dom element.
Also the this you refer to is not the component:
this.clipboard.on('success',(e) => { <--- This `this` is your component
this.set('icon','ion-checkmark');
this.set('copyStatus','success'); <---- These `this` are the context of the invoking success handler (you can set a break point here to see its not the ember component)
this.set('buttonText','Copied');
e.clearSelection();
});
You might want something like this (assuming this Clipboard thing can also receive a dom element):
this.clipboard = new Clipboard(this.$('.lm-button--copy'));
In an Ember component this.$ refers to the outer div that wraps the component. As such you will only select elements that are within the component. Which I think what you might need.
#Pedro Rio was close. Using clipboard.js you have to pass in a DOM element similar to jquery using syntanx like clipboard = new Clipboard('.class-name') or clipboard = new Clipboard('#id-name'). Somehow, in Ember world the scope of this was bound to the scope of Clipboard.js's query. So the fix was to use Ember's jQuery syntax to scope clipboard to each button item.
this.clipboard = new Clipboard(this.$(`.lm-button--copy`).get(0));
Please note that there is no . after this.$ as there is in the other answer.
Knockout gives you two ways of instantiating a component, either with a custom html element or with the component binding.
However I have discovered a slight issue when trying to style the root component element. It's fine if you just use the custom element syntax as you can just assign css styles to that - however, if you then use the component binding, the css rules don't match and so they fail.
Ideally I want to support both scenarios as they both have their uses. If I could get knockout to add a class to the root component element which is just the component name it would solve the issue but reading the documentation it isn't clear where it would be best to do this.
I've already got a custom template loader which retrieves the template from an ajax call, but this template is just the inner html of the root node.
Basically I want this:
<my-custom-element>
...
...
<my-custom-element>
To become this:
<my-custom-element class="my-custom-element">
...
...
<my-custom-element>
Anyone got any ideas?
You can use "createViewModel" method and access element in the component (e.g. to add some class):
ko.components.register('some-component', {
viewModel: {
createViewModel: function(params, componentInfo) {
var $element = $(componentInfo.element.children[0]);
// some other code ...
}
},
template: "<div></div>"
});