Render string input of ng-content directly as HTML - html

Is it possible for a component to receive content passed to it between opening and closing tags and assign the input directly to innerHTML inside the receiving template?
I have a string input like this:
public content: string =
'<div class="row">' +
' <div class="col">' +
' <h1>Friendly greetings!</h1>' +
' <p>' +
' This is my homepage..' +
' </p>' +
' </div>' +
'</div>';
And put it in my component like this:
<app-content>{{ content }}</app-content>
Inside app-content I can use ng-content in the template to project the input content. Since the input is a string I only get to see a string, the HTML elements it describes are not rendered.
I tried to inject the ElementRef and access the text node via this.elementRef.nativeElement.textContent, but the content must have been rendered first to access it that way.
Of course, I could hide the content ng-content produces like this:
<div style="display: none">
<ng-content></ng-content>
</div>
And then get the textContent in a variable (e.g. content) and set it like this:
<div [outerHTML]="content"></div>
But that seems hacky to me.
StackBlitz project: get-input-of-ng-content-directly
Am I missing something?
I want to do something like this in the template: <div [outerHTML]="incoming ng-content stuff"></div>
Or at least inject the content in the components constructor and set it to a variable, but without rendering the content first just to hide it.

I'd say you have two options:
1.) #Input() field that receives your content above and then renders the content as innerHTML like you mentioned. So something like
<app-content [content]="content"></app-content>
And then you can render you content Input like you have above with the <div [innerHTML]=...
2.) Or you can use <ng-content> inside of your <app-content> HTML file and then just do something like this:
<app-content>
<div [innerHTML]="content"></div>
</app-content>
But this second option probably assumes <app-content> is not the root element from which your Angular app is boostrapped.

Related

Angular innerHTML contents are jumbled. Only the last innerHTML is displayed correctly

In my Angular project (version 8) I am creating a list of static HTML from database and rendering it in parent HTML. Only the last div having innerHTML is rendered correctly, all the preceding divs having child html is not rendered correctly. The contents are jumbled. Basically the child html's style is not honored except for the last child html.
I am using sanitize html pipe for the div.
The angular component onInit queries DB in a loop. Each get call returns HTML text which is appended to an array of strings. The HTML text is basically PDF to HTML converted file. Each of the HTML file has its own style tag.
My guess is that only the last innerHTML's style is applied to all the preceding child innerHTML hence the jumbled contents (unless my guess is incorrect)
Any suggestion to solve the issue ?
HTML
<div *ngFor="let qBank of tsqm.selectedQuestions; let i = index">
<div class="page">
<div [innerHTML]="questionDataFromHtml[i] |
sanitizeHtml"></div>
</div>
</div>
Sanitize HTML:
#Pipe({ name: 'sanitizeHtml'})
export class SanitizeHtmlPipe implements PipeTransform {
constructor(private _sanitizer: DomSanitizer) { }
transform(value: string): SafeHtml {
return this._sanitizer.bypassSecurityTrustHtml(value);
}
}
Component:
ngOnInit(){
this.questionset = this.storage.get(quesId);
//pseudo code
forEach(item in this.questionset){
this.getHTMLfromDB(item)
}
}
getHTMLfromDB(question: QuestionBank) {
this.Service.getQuestionHtmlFile(question.questionFilePath).subscribe(res =>
{
this.questionDataFromHtml.push(res.text());
question.questionData.questionDataFromHtml = res.text();
});
Correct display. Question1 and Question2 are same
Correct display
Incorrect display. Question1 and Question2 are different
Incorrect display
Stackblitz:
stackblitz
The issue is all the css styling is overridden and the final values are applied.
Use id/class to apply the style to specific component.
I've made changes to your stackblitz example. Check here
In hello.component.ts
Applied red color to the text using text-red id.
export class HelloComponent {
#Input() name: string;
html1 =
"<html><head><style> #text-blue {color:blue;}</style></head><body><h2 id='text-blue'>Inner HTML1 in red</h2></body></html>";
html2 =
"<html><head><style> #text-red {color:red;}</style></head><body><h2 id='text-red'>Inner HTML2 in blue</h2></body></html>";
}
I solved this issue by using iFrame tag and srcdoc attribute. The backend service will return html text to angular. After DOM sanitizing the html documents are displayed in the iFrames.

How to do an image as a component template, with src as a property in Vue

I am trying to make a component for images in Vue. However, in my latest attempts, the only result is that an empty element is put in it's place.
In previous attempts, by putting a second element inside of the outer element, I was able to get it so that there was no image tag, and all of my props were displayed as a string:
<div> src=`${source}` alt=`${alternate}}` title=`${tit}}` width=`${w}}` height=`${h}}` /> </div>
My current code is:
<body>
<template id="comp-img-template">
<asimage source="uclx3S.jpg" alternate="red" tit="red" w="100" h="100"></asimage>
</template>
<script>.
Vue.component('asimage', {
props: ['source', 'alternate', 'tit', 'w', 'h'],
template: '<img src=`${source}` alt=`${alternate}}` title=`${tit}}` width=`${w}}` height=`${h}}` />'
})
new Vue({ el: '#comp-img-template' })
</script>
</body>
I am expecting there to be an image with the modifications present in the top portion, however as mentioned before I only get the empty tags
Your template string isn't valid HTML.
Something like this may work:
template: '< img :src="source" :alt="alternate" :title="tit" :width="w" :height="h" />'
Check out Vue v-bind syntax here: https://v2.vuejs.org/v2/guide/syntax.html#v-bind-Shorthand

Why doesn't src work for templates?

The HTML script tag can be used to define named templates:
<script type="text/html" id="Lookup">
<label class="field-title" data-bind="text: Metadata.FieldTitle, css: { hasError: !IsValid() }"></label>
<div data-bind="html: Metadata.FieldIntro"></div>
<select class="form-control" data-bind="attr: { id: 'ff' + Metadata.FormFieldID }, options: Lookup, optionsText: 'Caption', optionsValue: 'Id', optionsCaption: Metadata.FieldIntro || 'Select an item...', value: Value, css: { hasError: !IsValid() }"></select>
</script>
According to MDN and msdn, I should be able to move the content of a template into a file and reference it like this:
<script type="text/html" id="Lookup" src="Lookup.html"></script>
According to Fiddler template files are being loaded (I have several). What stops knockout from using the templates when they aren't inline, and is there anything that can be done about it?
From the comments, script tags don't parse what they load into the DOM. I could use something like jQuery ajax to load, parse and inject but knockout uses the script node's id to reference the template, so the next question is how to make visible the fragment so loaded.
At a stab I surmise that I need to do something like this:
$(document.body)
.append("<div id='Lookup' style='display:none'></div>")
.load("Templates/Lookup.html" );
Close, but no cigar. Here it is corrected and wrapped up as a function:
function loadTemplate(name) {
if (name)
switch (name.constructor) {
case String:
$(document.body).append("<div id='" + name + "' style='display:none'></div>");
$("#" + name).load("Templates/" + name + ".html");
break;
case Array:
name.map(loadTemplate);
break;
default:
throw "Must be a string or an array of strings";
}
}
You can pass a single template name or an array of names.
Knockout happily uses the template.
Browsers don't know how to process programs written in text/html (yes, that sentence doesn't make a whole lot of sense, this is a hack you are using).
Whatever JavaScript you are using to process the templates is reading the childnodes of the script element in the DOM. It hasn't been written to check for a src attribute and load external content.

Reusing pages in WinJS's Pivot

I'm developing app for Windows Phone 8.1 using WinJS and I used Visual Studio's template for pivot application. My Applications queries external API and displays results in PivotItem. Since there are three very similar queries that reurn same type of data, I'd like to reuse one code for all the sections in Pivot. The PivotItem page consist basically only of ListView with items received from API. My section page javascript looks like this:
var ControlConstructor = WinJS.UI.Pages.define("/pages/bookmarks/sectionPage.html", {
ready: function(element, options) {
//Here I call API based on received option and render the page
}
}
WinJS.Namespace.define("bookmarksApps_SectionControls", {
SectionControl: ControlConstructor
});
My page declaring the Pivot looks like this:
<div class="bookmarks" data-win-control="WinJS.UI.Pivot" data-win-res="{ winControl: {'title': 'BookmarksTitle'} }">
<div class="section1 section" data-win-control="WinJS.UI.PivotItem" data-win-options="{ isHeaderStatic: true }" data-win-res="{ winControl: {'header': 'BookmarksNew'} }">
<div class="sectioncontrol" id="section1contenthost" data-win-control="bookmarksApps_SectionControls.SectionControl" data-win-options="{'section': 'new'}"></div>
</div>
<div class="section2 section" data-win-control="WinJS.UI.PivotItem" data-win-options="{ isHeaderStatic: true }" data-win-res="{ winControl: {'header': 'BookmarksAll'} }">
<div class="sectioncontrol" id="section2contenthost" data-win-control="bookmarksApps_SectionControls.SectionControl" data-win-options="{'section': 'all'}"></div>
</div>
<div class="section3 section" data-win-control="WinJS.UI.PivotItem" data-win-options="{ isHeaderStatic: true }" data-win-res="{ winControl: {'header': 'BookmarksHistory'} }">
<div class="sectioncontrol" id="section3contenthost" data-win-control="bookmarksApps_SectionControls.SectionControl" data-win-options="{'section': 'history'}"></div>
</div>
</div>
Now, when I open the app,pivot page correctly loads and displays first section with data. But when I swipe the different section, new data is loaded (so the ready function is called, but nothing is displayed (page is blank, only PivotItems' headers are visible). But if I swipe back to section1, it contains data, that I want to display in section2.
Is it possible to reuse my SectionPage.html and SectionPage.js in different PivotItems, preferably without too much of boilerplate code?
You need to create custom HTML control which will host these pages, custom control can accept uri as data-win-options, then inside your control you can have updateLayout() which will render the page and append to parentElement.
Sample code in update layout method:
var options = {} //Page options
if (!this._isLoaded) {
this._isLoaded = true;
WinJS.UI.Pages.render(this.uri, this._pageElement, options);
}
I found source of my problem. In page /pages/bookmarks/sectionPage.html I had <div> with an id meant for holding my ListVIew. And I was getting win control for listview using document.getElementById("listViewId").winControl. This is wrong, because then I had three divs with same id (each for every section), so getElementById was always returning same list (the one on the first section).
So I changed getting of the wincontrol to
var discussionList = document.querySelector("#" + contentHost + " .disucssionsListView").winControl;
where contentHost depends on data-win-options received from main page and everything works as expected.

Polymer 1.0 - Binding css classes

I'm trying to include classes based on parameters of a json, so if I have the property color, the $= makes the trick to pass it as a class attribute (based on the polymer documentation)
<div class$="{{color}}"></div>
The problem is when I'm trying to add that class along an existing set of classes, for instance:
<div class$="avatar {{color}}"></div>
In that case $= doesn't do the trick. Is any way to accomplish this or each time that I add a class conditionally I have to include the rest of the styles through css selectors instead classes? I know in this example maybe the color could just simple go in the style attribute, it is purely an example to illustrate the problem.
Please, note that this is an issue only in Polymer 1.0.
As of Polymer 1.0, string interpolation is not yet supported (it will be soon as mentioned in the roadmap). However, you can also do this with computed bindings. Example
<dom-module>
<template>
<div class$="{{classColor(color)}}"></div>
</template>
</dom-module>
<script>
Polymer({
...
classColor: function(color) {
return 'avatar '+color;
}
});
<script>
Edit:
As of Polymer 1.2, you can use compound binding. So
<div class$="avatar {{color}}"></div>
now works.
Update
As of Polymer 1.2.0, you can now use Compound Bindings to
combine string literals and bindings in a single property binding or text content binding
like so:
<img src$="https://www.example.com/profiles/{{userId}}.jpg">
<span>Name: {{lastname}}, {{firstname}}</span>
and your example
<div class$="avatar {{color}}"></div>
so this is no longer an issue.
The below answer is now only relevant to versions of polymer prior to 1.2
If you are doing this a lot, until this feature becomes available which is hopefully soon you could just define the function in one place as a property of Polymer.Base which has all of it's properties inherited by all polymer elements
//TODO remove this later then polymer has better template and binding features.
// make sure to find all instances of {{join( in polymer templates to fix them
Polymer.Base.join = function() { return [].join.call(arguments, '');}
and then call it like so:
<div class$="{{join('avatar', ' ', color)}}"></div>
then when it is introduced by polymer properly, just remove that one line, and replace
{{join('avatar', color)}}
with
avatar {{color}}
I use this a lot at the moment, not just for combining classes into one, but also things like path names, joining with a '/', and just general text content, so instead I use the first argument as the glue.
Polymer.Base.join = function() {
var glue = arguments[0];
var strings = [].slice.call(arguments, 1);
return [].join.call(strings, glue);
}
or if you can use es6 features like rest arguments
Polymer.base.join = (glue, ...strings) => strings.join(glue);
for doing stuff like
<div class$="{{join(' ', 'avatar', color)}}"></div>
<img src="{{join('/', path, to, image.jpg)}}">
<span>{{join(' ', 'hi', name)}}</span>
of just the basic
Polymer.Base.join = (...args) => args.join('');
<div class$="{{join('avatar', ' ', color)}}"></div>
<template if="[[icon_img_src]]" is="dom-if">
<img class$="{{echo_class(icon_class)}}" src="[[icon_img_src]]">
</template>
<span class$="{{echo_class(icon_class, 'center-center horizontal layout letter')}}" hidden="[[icon_img_src]]">[[icon_text]]</span>
<iron-icon icon="check"></iron-icon>
</div>
</template>
<script>
Polymer({
echo_class: function(class_A, class_Z) {
return class_A + (class_Z ? " " + class_Z : "");
},