Using custom Angular component inside Contentful rich text renderer - javascript

I have a rich text element in Contentful that has an embedded entry within it. I am attempting to have that embedded entry render inside a custom Angular component. However, when I pass in options to the Contentful documentToHtmlString function, it displays the tag, but does not render anything or even trigger the console.log() function I have inside the custom component typescript file.
TYPESCRIPT for the rendering
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => `<embed-element [node]="${node}" [content]="${next(node.content)}"></embed-element>`
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
HTML for the rendering
<span [innerHTML]="convertToHTML(article.fields.content)"></span>
I have loaded the custom element <embed-element></embed-element> in the providers section of the app.module.ts file as well.
import { EmbedElementComponent } from './shared/components/embed-element/embed-element.component';
#NgModule({
declarations: [
...
EmbedElementComponent
],
providers: [
...
EmbedElementComponent
]
})
Then inside typescript file for the custom element, I just simply have a console.log in the onInit function for testing purposes. I am not seeing this occur in the console.
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'embed-element',
templateUrl: './embed-element.component.html',
styleUrls: ['./embed-element.component.css']
})
export class EmbedElementComponent implements OnInit {
#Input() node: any;
#Input() content: any;
constructor() { }
ngOnInit(): void {
console.log(this.node);
}
}
And in the HTML for the custom element, I removed the <p> tag just in case it was stripping it out as "unsafe" and replaced it with the following:
EMBEDDED ELEMENT WORKS!!!!!
Finally, I see nothing appear on screen for this custom element once everything is rendered. Inside the inspect element however, this is what I get.
<embed-element [node]="[object Object]" [content]=""></embed-element>
How do I manage to make the custom element actually get called in this aspect? And at least receive the console log message I am requesting inside the custom element?

Not sure if you still needed another solution, but I came across ngx-dynamic-hooks https://github.com/MTobisch/ngx-dynamic-hooks - and was able to reuse my custom components within the inner html.
const cmpt_data = {
title: 'This is a test'
};
const headerFooterRichTextOption: Partial<Options> = {
renderNode: {
["embedded-entry-inline"]: (node, next) => `${this.embeddedContentfulEntry(node.nodeType, node.data)}`,
["paragraph"]: (node, next) => `<span>${next(node.content)}</span>`,
}
};
const d = documentToHtmlString(tt.value, headerFooterRichTextOption);
const hSanit = this.sanitizer.bypassSecurityTrustHtml(d);
embeddedContentfulEntry(nodeType: any, data: any) {
return "<custom-component [title]='context.title'></custom-component>";
}
<ngx-dynamic-hooks [content]="hSanit" [context]="cpmt_data"></ngx-dynamic-hooks>

I believe that using a custom component for this type of situation was not working because it was rendering outside of Angulars scope or after initial components initiation.
I resolved this issue by essentially just removing the custom element all together and creating a function that renders the embedded element as I wanted.
// Notice the new function call, renderCustomElement()
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => this.renderCustomElement(node)
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
And finally that function that simply creates some basic html with Bootstrap styling and returns that back to be rendered.
renderCustomElement(node:any) {
let link = `/support-center/article/${node.data.target.fields.category[0].sys.id}/${node.data.target.sys.id}`;
return `<style>.card:hover{box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;}</style><a class="card clickable mb-2 text-decoration-none text-dark" href="${link}" target="_blank"><div class="card-header">Article</div><div class="card-body"><h4 class="fw-light">${node.data.target.fields.title}</h4><p class="fw-light pb-0 mb-0">${node.data.target.fields.preview}</p></div></a>`
}

Related

Data sharing between component with data servive not working in angular 5

I, am using data service to share the data between the component. However, this seems not working for me.
Got the reference from here
Angular to update UI from the child component reflect the value to the parent component
https://angularfirebase.com/lessons/sharing-data-between-angular-components-four-methods/
I tried the same logic as above but seems to not work for me.
Here is the html binding for the angular material
<mat-progress-bar mode="indeterminate" *ngIf="commonViewModel.showProgressBar()"></mat-progress-bar>
Parent component
export class AppComponent {
constructor(public commonViewModel: CommonViewModel) { }
ngOnInit() {
this.isLoding();
}
isLoding() {
console.log("app=" + this.commonViewModel.showProgressBar());
return this.commonViewModel.showProgressBar();
}
}
Child Component
export class HomeComponent {
private GetHomeItemUrl: string = "Home/GetHomeItem";
private _homeItemService: GenericHttpClientService;
constructor(public commonViewModel: CommonViewModel) {
this.getHomeItemHttpCall();
}
private getHomeItemHttpCall(): void {
this.commonViewModel.setProgressBarShow = true;
this._homeItemService.GenericHttpGet<GenericResponseObject<HomeViewModel>>(this.GetHomeItemUrl).subscribe(data => {
if (data.isSuccess) {
this.commonViewModel.setProgressBarShow = false;
console.log("home=" +this.commonViewModel.showProgressBar());
}
}, error => {
console.log(error);
});
}
}
This is my service class which hold the value as true and false
#Injectable()
export class CommonViewModel {
progressBarShow: boolean = true;
public showProgressBar(): boolean {
return this.getProgressBarShow;
}
set setProgressBarShow(flag: boolean) {
this.progressBarShow = flag;
}
get getProgressBarShow(): boolean {
return this.progressBarShow;
}
}
The console output
In the console I,can see the output as True and False. But the app never hides as I can see the app component value is always true
Where I, am doing mistake. Can please someone let me know. I, dont want to use Input and Output to share the data.
Please let me know how can I resolve this issue.
it's possible that your parent component and your child component are being injected with two different instances of the service, depending on where you "provide" it. Try providing it from your app module.
Also, if the child is a direct child of the parent, you don't need the service, you can have an EventEmitter (an #Output) in child, and communicate through that.
See the documentation at https://angular.io/api/core/EventEmitter
I think, that GSSWain's answer must be work. If not, try use a getter
<mat-progress-bar *ngIf="isLoading"></mat-progress-bar>
get isLoading(){
return this.commonViewModel.showProgressBar();
}

Accessing Style of Ionic 2 DOM Elements

I'm trying to access the DOM elements of one of my pages with the following:
ionViewDidEnter() {
this.platform.ready().then(() => {
let elm = <HTMLElement>document.querySelector("ion-navbar.toolbar.toolbar-ios.statusbar-padding");
console.log(elm.style);
})
}
However, it appears this element has no style - I have tried various combinations to access it but no luck.
Specifically, I'm looking for the height of the ion-navbar. Is this possible?
You can get the actual height of an element with element.offsetHeight.
As for the style property, it will give you only the attributes defined in the element's inline style attribute (e.g. <div style="height: 20px;">...</div>), not the ones applied by the CSS using selector rules. See this MDN article for more details.
This is my workaround for that.
let tabs = document.querySelectorAll('.show-tabbar');
if (tabs !== null) {
Object.keys(tabs).map((key) => {
tabs[key].style.transform = 'translateY(56px)';
});
}
I always have found it terribly useful to simply use
ionic serve
inspect the element in chrome and easily see the style for the given device.
To access the DOM element in angular I have used the #id route. The following snippet was used to verify the appropriate class was applied by ionic.
html- home.html
<ion-spinner #testspinner name="crescent" paused="{{isPaused}}"></ion-spinner>
ts- home.ts
import {Component} from '#angular/core';
import {ViewChild} from '#angular/core';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
isPaused: boolean;
#ViewChild('testspinner') testspinner;
constructor() {
this.isPaused = false; // set to true will cause to never have animation state running.
}
containsPausedClass(list: string[]) {
return (list.indexOf('spinner-paused') != -1);
}
ngAfterViewInit() {
// if the spinner is allows to load without 'spinner-paused' then safari will work.
setTimeout(() => {
this.isPaused = true;
}, 0);
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}
togglePause() {
this.isPaused = !this.isPaused;
setTimeout(() => {
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}, 100);
}
}

View manipulation only works when one component is in view

I have an angular2 component which manipulates an SVG image based on data it receives from my real time API.
The exact same component is used in two views, the first is a list view, where there are lots of instances of the component.
The second is the detail component where there is only one instance of the component.
However, the component only works in the second view. The component look like:
#Component({
selector: 'app-line-overview',
template: `<object id="img" data="assets/img/big_layout.svg" width="292" height="164" #svg></object>`
})
export class LineOverviewComponent {
stations: string;
#ViewChild('svg') svg: ElementRef;
constructor(public socketService: SocketService) { }
ngOnInit() {
this.socketService.getStations('api/getStations/' + this.line.name).subscribe(stns => {
this.stations = stns.stations;
this.load();
})
}
load() {
let svgDoc = this.svg.nativeElement.contentDocument;
if (this.stations) {
JSON.parse(this.stations).forEach(station => {
station.element = svgDoc.getElementById(station.name);
console.log(station)
station.element.removeAttribute('class');
if (station.status === 'running') {
station.element.setAttribute('fill', 'lime');
}
})
}
}
}
In the list view, station.element is null throwing the error:
inline template:19:24 caused by: Cannot read property 'removeAttribute' of null
Update:
see this [plunk](http://plnkr.co/edit/BNK6rbfBUAleXF6X9ckg?p=preview.

Aurelia - how to register view resources for ViewCompiler?

I have a row-based (table-like) custom element, which renders its template dynamically based on a bindable columns attribute. It composes the row template in string and uses ViewCompiler, ViewFactory and View to render.
import {inject} from 'aurelia-dependency-injection';
import {bindable, ViewCompiler, ViewResources} from 'aurelia-templating';
#inject(ViewCompiler, ViewResources)
export class MyDynamicGrid {
#bindable columns: any[];
#bindable rows: any[];
constructor(viewCompiler: ViewCompiler, viewResources: ViewResources) {
const template = '<template><custom-element></custom-element></template>'; //this is rather complex in practice
viewResources.registerElement('custom-element', /* HtmlBehaviorResource? */);
this._viewFactory = viewCompiler.compile(template, viewResources);
}
_render() : void {
const view = this._viewFactory.create(/* some container */);
view.bind(someContext, someOverrideContext);
//attach view to the DOM
}
}
This works fine, until the custom template contains standard HTML elements. Once I start putting custom elements in the template, it stops working. It still renders the HTML, but the custom element's behaviour is not being attached by Aurelia.
I'm aware, that all custom elements should be "registered" in order to be used. "Normal registration" happens either in the view via <require>, or in the view-model #viewResources, or registering it globally.
In this particular case, however, the injected ViewCompiler only inherits the view resources of the view-model's parents. My question is: how can be any additional view-resources registered? I'm aware of the second parameter in ViewCompiler's compile method, but couldn't make it work. The only way I was able to make it work, if I register it globally.
Note: this question is focusing on registering view resources. The dynamic rendering works just fine
I've found a solution by diggin into the docs+github. I've created two samples for two different approaches:
Create HtmlBehaviorResource instance manually, this is to register one specific element.
Sample (based on: https://github.com/aurelia/templating-resources/blob/master/test/repeat-integration.spec.js)
import {CustomElement} from 'my-components/my-custom-element';
import {inject, Container} from 'aurelia-dependency-injection';
import {ViewCompiler, ViewResources} from 'aurelia-templating';
import {metadata} from 'aurelia-metadata';
#inject(ViewCompiler, ViewResources, Container)
export class MyDynamicGrid {
//bindables and constructor is ommitted
init(): void {
const resource: HtmlBehaviorResource = metadata.get(metadata.resource, CustomElement);
resource.initialize(this._container, CustomElement);
resource.load(this._container, CustomElement)
.then(() => {
resource.register(this._viewResources);
this._viewFactory = viewCompiler.compile(template, this._viewResources);
});
}
}
Remark: the line resource.register(this._viewResources); is equivalent to this._viewResources.registerElement('custom-element', resource);. The only difference is the first reads the name from convention or decorator.
Use ViewEngine and import (a) whole module(s). This is more appropriate if importing multiple resources, even different types (attribute, element, converter, etc.) and from different files.
Sample:
import {CustomElement} from 'my-components/my-custom-element';
import {inject} from 'aurelia-dependency-injection';
import {ViewCompiler, ViewResources, ViewEngine} from 'aurelia-templating';
#inject(ViewCompiler, ViewResources, ViewEngine)
export class MyDynamicGrid {
//bindables and constructor is ommitted
init(): void {
this._viewEngine
.importViewResources(['my-components/my-custom-element'], [undefined], this._viewResources)
.then(() => {
this._viewFactory = viewCompiler.compile(template, this._viewResources);
});
}
}
I've found your question while struggling with the same problem, but I think I managed to make it work with resources parameter of compile. Here is my setup:
I wrapped the compilation into a helper class:
#autoinject
export class ViewFactoryHelper {
constructor(resources, viewCompiler, container, loader) {
}
compileTemplate(html, bindingContext) {
let viewFactory = this.viewCompiler.compile(html, this.resources);
let view = viewFactory.create(this.container);
view.bind(bindingContext);
return view;
}
}
And then the client looks like this:
#autoinject
export class SomethingToCreateDynamicControls {
constructor(viewFactoryHelper, myCustomConverter, anotherCustomConverter) {
viewFactoryHelper.resources.registerValueConverter('myCustomConverter', myCustomConverter);
viewFactoryHelper.resources.registerValueConverter('anotherCustomConverter', anotherCustomConverter);
}
makeControl(model) {
let html = '...'; // HTML that uses those converters in normal way
let compiledTemplate = this.viewFactoryHelper.compileTemplate(html, model);
// ...
}
}
UPDATE: So far I'm not able to call registerElement instead of registerValueConverter with desired effect, so my answer is probably not a good one yet. I'll keep trying...

how can I listen to changes in code in angular 2?

I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.

Categories

Resources