I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer
Related
This one has been puzzling me for a while, trying to create a type-safe email service.
I have an enum of possible template names:
enum TemplateName {
EXAMPLE_TEMPLATE = "EXAMPLE TEMPLATE"
...
}
I have an object of default settings per template:
type EmailConfig<X = React.ComponentType> = {
html: X
subject: string
...
}
type EmailMapping: EmailConfig = {
[key in TemplateName]: EmailConfig
}
const Emails = {
[TemplateName.EXAMPLE_TEMPLATE]: {
html: TestTemplate, // THIS IS A REACT FUNCTIONAL COMPONENT
subject: "This is a test",
...rest
}
...
}
My Templates look like so:
export interface TestTemplateProps {
title?: string
firstName?: string
preview?: string
headline?: string
site?: string
...
}
export const TestTemplate: React.FC<TestTemplateProps> = ({
title = 'Test Email',
site = 'My Website',
preview = 'Important Information from My Site',
firstName = 'there',
headline,
children,
}) => {
return (
...
)
}
I have a generic function I want to be able to pass in an enum value and all the props of the Component that relates to that enum value.
FOR EXAMPLE
sendEmail(TemplateName.EXAMPLE_TEMPLATE, { ... })
Where { ... } is typed to TestTemplateProps interface
My current attempt at sendEmail looks like this:
async sendEmail<X extends keyof EmailMapping>(
template: X,
opts: React.ComponentProps<typeof Emails[X]['html']>
) {
...
}
I've tried just playing around (honestly I am just guessing at what to change at this point) and this is the closest I have come so far.
When I call sendEmail with above code, it forced me to pass one of the Enums, but then in the opts the only "typing" that appears is "children?" and none of the other properties in the TestTemplateProps so I think I'm close!
TIA
Yes. but the first thing that I wanna know is ... are you making a class for data loading and passing data in your interfaces with static function or static constructor for async data loading... if so ... I want to know the
the exact problem you're facing...
I have my app.component with a list of objects
class Hero {
alias: string;
constructor(public firstName: string,
public lastName: string) {
}
}
class AppComponent {
...
heroes: Hero[] = [
new Hero("foo", "bar")
];
...
onHeroChange($event: Hero, index: number): void {
this.heroes[index] = $event;
}
<div *ngFor="let hero of heroes; let index=index">
<hero [hero]="hero" (heroChange)="onHeroChange($event, index)"></hero>
</div>
The HeroComponent is
export class HeroComponent {
#Input()
set hero(newValue: Hero) {
this._hero = newValue;
this.showAlias = !!newValue.alias;
}
get hero(): Hero {
return this._hero;
}
#Output() heroChange: EventEmitter<Hero> = new EventEmitter<Hero>();
showAlias: boolean = !1;
private _hero: Hero;
//
#HostListener('click')
onHeroClick(): void {
this.hero.alias = `alias_${+new Date()}`;
console.info('HERO TO EMIT', this.hero);
this.heroChange.emit(this.hero);
}
}
My problem is that even by assigning the changed hero in app.component, the set hero inside hero.component is not called, so showAlias in the example is not updated and I don't see the alias in the hero.component.
Do I need to force the ngFor by assigning the entire array?
Maybe a workaround could be removing the object from the array and then inserting again?
Sounds like useless computation though.
Note: this is just an example, it's not what I'm really working on, so something like
Update the showAlias prop in the onHeroClick method
or
Assign hero in the hero.component
unfortunately don't solve the issue. I need the changes to be on the outside because other stuff happens.
Could be another option changing the detection to onPush and marking for check manually?
Blitz ==> https://stackblitz.com/edit/angular-ivy-kpn3ds
You're not setting a new hero, you're just modifying a property on the existing one:
this.hero.alias = `alias_${+new Date()}`;
That doesn't fire the setter. Change the line like this:
this.hero = {...this.hero, alias: `alias_${+new Date()}`};
I have a simple app component with a search input and an observable resultItems: Observable<Array<string>>; powered by a search service that returns results to the UI via *ngFor. There is also a leaflet map that should render the locations of the results. The search service works well and I can render the location of one result in the map onclick. My question is: What is the recommended way to call the map service mapResults every time the search service returns new results or the observable changes? I can imagine how I could create a custom pipe that would iterate over the parks in the service results and call mapservice.mapResult but that seems odd since the pipe wouldn't return anything to the UI and I'm a little concerned with performance, understanding little about pure and impure pipes. I have also seen mention of a process by which you subscribe to changes of an observable, but I'm lost with the semantics and changes among API versions.
I apologize if this is a problem of poor design. I only have a few weeks of Angular learning and I admittedly haven't read the documentation thoroughly. Please point any and all issues you see.
simple Search Service
import { Injectable } from '#angular/core';
import { URLSearchParams, Jsonp } from '#angular/http';
#Injectable()
export default class ParkSearchService {
constructor(private jsonp: Jsonp) { }
search(parkSearchterm: string) {
var search = new URLSearchParams()
search.set('q', 'PARK_NAME:*' + parkSearchterm+'*');
search.set('wt', 'json');
search.set('json.wrf','JSONP_CALLBACK')
var test = this.jsonp
.get('http://parksearch/parks_full/select?', { search })
.map((response) => response.json()['response']['docs']);
return test
}
}
exert from app.component.html
<md-card *ngFor="let item of resultItems | async; let i = index"
class="search-result"
[ngClass]="{ 'selected-result': selectedIndex === i }">
<md-card-header class="clickable"
(click)="showBoundary(item)"
md-tooltip="Zoom to park">
<md-card-title>{{item.PARK_NAME}}</md-card-title>
</md-card-header>
<md-card-content style="height: 75px; overflow-y: auto">
<button md-button
color="primary"
md-tooltip="more info"
(click)="openDtl(item.PARK_HOME_PAGE_URL)">
<md-icon>info</md-icon>
<span>Details...</span>
</button>
<button md-button
color="primary"
md-tooltip="open park website"
(click)="openParkURL(item.PARK_HOME_PAGE_URL)">
<md-icon>web</md-icon>
<span>WebSite</span>
</button>
Amenties: {{ item.AMEN_LIST }}
</md-card-content>
</md-card>
app.component.ts (forgot to include)
export class AppComponent {
private selectedIndex: number;
public events: any[] = [];
//park search items
resultItems: Observable<Array<string>>;
parkSearchterm = new FormControl();
//setup resultitems
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm));
map service:
//Thanks for the help getting started https://github.com/haoliangyu
import { Injectable } from '#angular/core';
import { Map, GeoJSON } from 'leaflet';
#Injectable()
export default class MapService {
public map: Map;
private currentLayer: GeoJSON;
private resultsLayer: any;
private resultfeatureCollection: any;
constructor() {
}
initialize() {
if (this.map) {
return;
}
this.map = L.map('map', {
zoomControl: true,
zoom: 6,
minZoom: 3,
maxZoom: 19
});
L.tileLayer('http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap, Tiles courtesy of Humanitarian OpenStreetMap Team'
}).addTo(this.map);
L.control.scale().addTo(this.map);
//Add the results layer
this.resultsLayer = L.geoJSON(this.resultfeatureCollection, {
style: () => {
return {
color: '#ff00005',
fillColor: '#3F51B5'
};
}
}).addTo(this.map);
}
mapResults(park) {
//update the restults layer
let resultfeatureCollection: GeoJSON.FeatureCollection<any> = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: "Polygon",
coordinates: JSON.parse(park.BBOX[0])
},
properties: {
name: 'test'
}
}
]
};
this.resultsLayer.addData(resultfeatureCollection);
this.map.fitBounds(this.resultsLayer.getBounds());
}
}
You are very close to what you need: You already have an observable stream created for your results, called resultItems, that is right. Then on your template, when you use it through the async pipe what Angular does internally is subscribes to this stream to get its values.
So if you want to "also" map when resultItems yields is by subscribing to it by yourself as well. There's a catch though: by default every subscription duplicates the workload for the stream, meaning that every time the user makes a new search it would run the API call twice: 1 for the async subscription and another one for your .subscribe.
The way to resolve that is by using .publish(): What this allows is to share the result of an stream between many subscribers, so your code would look like this:
ngOnInit() {
this.mapService.initialize();
this.resultItems = this.parkSearchterm.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(parkSearchterm => this.parkSearchService.search(parkSearchterm))
.publish();
// With publish() we are sharing the items from this stream to all of their subscribers
// We just need to tell it to do the "first subscription"
this.resultConnection = this.resultItems.connect();
// This "resultConnection" is the base subscription... We will have to dispose it in our ngOnDestroy method or we might get memory leaks
// Now we can tell the map service to update when we get a value:
this.resultItems.subscribe((park) => this.mapService.mapResults(park));
// (Not sure if the data types are correct, I assume you can map them)
// When `async` calls .subscribe() to do his job, we won't get duplicate API calls thanks to .publish()
}
To clarify, what connect() does is subscribe to the original stream, and start forwarding the values recieved to the subscribers of the published stream (this.resultItems). Now you are the owner of that subscription, so you are responsible of disposing it whenever you don't need more searches.
Subscribe on observer and call then search is changed. https://angular.io/docs/ts/latest/guide/router.html#!#reuse
After get result from server, put it to array. Angular will update the data in the template itself, this is the fastest way.
Angular2 doesn't trigger the ChangeDetection after a click event. The code snippets below are to get the data from one component to another.
onClickEvent
(click)="$event.preventDefault(); setApartmentObject(FlatObject)";
ApartmentOverviewComponent
constructor(private _apart:ApartmentService) {}
setApartmentObject(flat:ApartmentObject) {
this._apart.setApartmentDetails(flat);
}
ApartmentService
Injectable()
export class ApartmentService {
apartmentDetails:ApartmentObject
getApartmentDetails():Observable<ApartmentObject> {
return Observable.create((observer) => {
observer.next(this.apartmentDetails);
observer.complete();
});
}
setApartmentDetails(value:ApartmentObject) {
this.apartmentDetails = value;
}
}
ApartmentDetailComponent
constructor(private _apart:ApartmentService)
get apartmentDetails() {
this._apart.getApartmentDetails().subscribe(data => {
this._apartmentDetails = data;
});
return this._apartmentDetails;
}
In the HTML file
<p><strong>{{apartmentDetails.name || 'Musterwohnung'}}</strong></p>
I also tried to fix this problem with an eventemitter, but without success. Only the following dirty fix works:
constructor(private _ref:ChangeDetectorRef) {
this._ref.detach();
setInterval(() => {
this._ref.detectChanges();
}, 300);
}
There are some issues with your code that actually prevent the value from being read.
First of all—in your service—when you set the value, you just do it on the service's instance, instead of feeding it to the observable object. The observable just can't know that value has changed, so it won't emit the change (next) event. This is why the ApartmentOverviewComponent. setApartmentObject() does nothing. To actually feed the observable with data, you need to use a Subject.
In the ApartmentDetailComponent, in this simple scenario (where data is always synchronously provided), you could get the value in the way you try it. But, as mentioned before, the data won't ever change. It's also needles to store the data on the component's instance's _apartmentDetails field. You could use the observable in your template.
The working implementation is like that:
#Injectable()
class ApartmentService {
// BehaviorSubject is a type of an Observable that can be manually fed with data
// and returns it's last value to any subscriber.
apartmentDetails = new BehaviorSubject<ApartmentObject>({name: 'Musterwohnung'});
// Instead of using a property of the service, just inform the
// subject about knew data and let it spread the change for you.
setApartmentDetails(value: ApartmentObject) {
this.apartmentDetails.next(value);
}
}
#Component({
selector: 'overview-cmp',
// Side note: you don't need to .preventDefault() here.
template: `<a (click)="setApartmentObject({name: 'Shiny Aparament'})">click</a>`
})
class ApartmentOverviewComponent {
constructor(private apartService: ApartmentService) {}
// Works same as before.
setApartmentObject(flat: ApartmentObject) {
this.apartService.setApartmentDetails(flat);
}
}
#Component({
selector: 'details-cmp',
// Use the 'async' pipe to access the data stored in an Observable
// object. Also, to secure the code, use '?' to safely access the property.
template: `<p><strong>{{(details | async)?.name}}</strong></p>`
})
class Apartament {
// This is the observable with data.
details: Observable<ApartmentObject>;
constructor(private apartService: ApartmentService) {}
// When component initialises, assign the observable data from the service
ngOnInit() {
this.details = this.apartService.apartmentDetails;
}
}
I have an Angular 2 app where I need to create child components dynamically.
Is it mandatory to call 'detectChanges()' and 'detach()' method on the component reference variable 'componentRef.changeDetectorRef' ?
I see things work properly even if I dont use them.
Are these methods are actually meant component injection performance improvement ?
#Component({
selector: 'container',
template: '<template #content></template>'
})
export class ContainerComponet implements AfterViewInit {
contentComponentRef:any;
#ViewChild('content', {read: ViewContainerRef}) contentHandle;
constructor(private componentResolver:ComponentResolver) {
super();
}
ngAfterViewInit() {
if (this.contentComponentRef)
this.contentComponentRef.destroy();
this.componentResolver.resolveComponent(ChildComponent)
.then((factory:ComponentFactory<any>) => {
let componentRef = this.contentHandle.createComponent(factory);
componentRef.instance['child_component_property'] = 'dummy value for child';
componentRef.changeDetectorRef.detectChanges();
componentRef.onDestroy(() => {
componentRef.changeDetectorRef.detach();
});
this.contentComponentRef = componentRef;
return componentRef;
});
}
}