In Angular, it's possible to use SVG as component template. I wonder if there is a way to convert a SVG content (string loaded from server) into a component template, including the possibility to bind some properties to it (i.e, <tspan [attr.color]="textColor" ...>{{myText}}</tspan>).
I've made some tests and all that I got was to load and render the SVG content, but the binds are not working. Instead, they are rendered as strings.
I know that if put the SVG content in a file and reference it in component's templateUrl setting, it works. For example:
...
#Component({
templateUrl: './content.svg'
})
export class MyComponent {
...
}
...
I've already tested it. However, I need to load the SVG from a server (it cannot be a static file or hard coded in source-code).
Can anyone help me?
Update
Just to clarify a little bit...
Consider the following component:
#Component({
selector: 'my-component',
template: `<div [innerHtml]="mySvg"></div>`
})
export class MyComponent {
textColor: string;
myText: string;
mySvg: string;
constructor() {
this.textColor = '#ff0000';
this.myText = 'Just testing';
}
loadSvg() {
// here goes some logic to request the SVG string from server
// ...
this.mySvg = response;
}
}
Now, consider that the server returned the following string:
<svg>
<tspan [attr.color]="textColor">{{myText}}</tspan>
</svg>
I would like the SVG to be rendered and the interpolations to work as well (making it possible to change the text and color).
Is it possible?
If you stick to native standard Javascript template literal notation: ${x}
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
`string ${property} string string ${property} string`
You can create a tagged template function that does all the replacing of property values for you
Here the template string is the innerHTML, properties as attributes
When you make up your own template syntax (or use 3rd party) You will have to add a lot more boilerplate code or libraries to parse that String
<svg-from-template cx="30" cy="30">
<rect x='0' y='0' width='100%' height='100%' fill='${bgcolor}'></rect>
<circle cx='${cx}' cy='${cy}' r='10%' fill='${fill}'></circle>
</svg-from-template>
<svg-from-template bgcolor="lightgreen" cx="70" cy="70" fill="green">
<rect x='0' y='0' width='100%' height='100%' fill='${bgcolor}'></rect>
<circle cx='${cx}' cy='${cy}' r='20%' fill='${fill}'></circle>
</svg-from-template>
<script>
customElements.define(
"svg-from-template",
class extends HTMLElement {
connectedCallback() {
function parseTemplate(templateString, templateData = {}) {
return new Function( // create Function AND return the executed function result
"templateProps", [
"let f = ( " + Object.keys(templateData).join(",") + " ) =>",
"`" + templateString + "`",
"return f(...Object.values(templateProps))",
].join("\n")
)(templateData); // execute tagged template function
}
setTimeout(() => { // wait till all innerHTML is parsed by the Browser
let templateData = [...this.attributes].reduce((acc, attr) => {
acc[attr.name] = attr.value; // copy element attributes to {} object
return acc;
}, { /*default object*/ bgcolor: 'pink', fill: 'red' });
let templateString = this.innerHTML; // OR read from file/server/universe
this.innerHTML = "<svg mlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>" +
parseTemplate(templateString, templateData) + "</svg>";
});
}
}
);
</script>
<style>
svg {
width: 180px;
/* SO snippet window height */
}
</style>
Related
Here is a stackBlitz demo.
I have a d3.js service file that builds my svg layout. Its a force directed d3 graph that has nodes. Each node carries its own data.
I have extracted that data into an array, capturing the ids of the nodes when selected. In my example, to select a node and capture the ID a user needs to hold/press Ctrl then click on a node.
This is done in a d3 .on click function within my angular service file.
Service.ts
export class DirectedGraphExperimentService {
public idArray = []
_update(_d3, svg, data): any {
...
svg.selectAll('.node-wrapper').on('click', function () {
if (_d3.event.ctrlKey) {
d3.select(this).classed(
'selected',
!d3.select(this).classed('selected')
);
const selectedSize = svg.selectAll('.selected').size();
if (selectedSize <= 2) {
svg
.selectAll('.selected')
.selectAll('.nodeText')
.style('fill', 'blue');
this.idArray = _d3.selectAll('.selected').data();
return this.idArray.filter((x) => x).map((d) => d.id);
}
}
});
...
}
}
My global variable this.idArray = [] does not update with the id strings therefore cant pass the array to the component like this this.directedGraphExperimentService.idArray its always [] empty.
component.ts
import { Component, OnInit, ViewChild, ElementRef, Input } from '#angular/core';
import { DirectedGraphExperimentService } from './directed-graph-experiment.service';
#Component({
selector: 'dge-directed-graph-experiment',
template: `
<style>
.selected .nodeText{
fill:red;
}
</style>
<body>
<button (click)="passValue()">Pass Value</button>
<svg #svgId width="500" height="700"><g [zoomableOf]="svgId"></g></svg>
</body>
`,
})
export class DirectedGraphExperimentComponent implements OnInit {
#ViewChild('svgId') graphElement: ElementRef;
constructor(
private directedGraphExperimentService: DirectedGraphExperimentService
) {}
ngOnInit() {}
#Input()
set data(data: any) {
this.directedGraphExperimentService.update(
data,
this.graphElement.nativeElement
);
}
passValue() {
console.log(this.directedGraphExperimentService.idArray); // returns []
}
}
I've also tried emitting an event with the value via the <svg></svg> container in the component template.
Is there another way I can get the updated this.array values into the parent component? A way to subscribe to the value in the function? Perhaps with a behaviourSubject from rxjs?
Here is a stackBlitz demo. In this demo you will see I have added a button that I press to trigger the update to my component file, obviously its only passing the empty global variable on my service. To add to the array you will need to press Ctrl and click on a node, you will see in console the array filling up(max of 2 string).
Here's a transition loader running while downloading an external mp3, waiting to be ready to stream with <audio></audio> tags inside a <div class="player"></div>.
How can I apply this script to the specific <div class="player"></div> only and not to the whole page? Thanks.
let e= {
backgroundColor: "#fff", filterBrightness:2, strokeWidth:10, timeOnScreen:100
},
t=document.querySelector("*"),
r=document.createElement("style"),
i=document.createElement("<div>"),
s="http://www.w3.org/2000/svg",
n=document.createElementNS(s, "svg"),
o=document.createElementNS(s, "circle");
document.head.appendChild(r),
r.innerHTML="#keyframes swell{to{transform:rotate(360deg)}}",
i.setAttribute("style", "background-color:"+e.backgroundColor+";color:"+e.backgroundColor+";display:flex;align-items:center;justify-content:center;position:fixed;top:0;height:100vh;width:100vw;z-index:2147483647"),
document.body.setAttribute("style", "overflow:hidden!important;"),
document.body.prepend(i),
n.setAttribute("style", "height:50px;filter:brightness("+e.filterBrightness+");animation:.3s swell infinite linear"),
n.setAttribute("viewBox", "0 0 100 100"),
i.prepend(n),
o.setAttribute("cx", "50"),
o.setAttribute("cy", "50"),
o.setAttribute("r", "35"),
o.setAttribute("fill", "none"),
o.setAttribute("stroke", "currentColor"),
o.setAttribute("stroke-dasharray", "165 57"),
o.setAttribute("stroke-width", e.strokeWidth),
n.prepend(o),
t.style.pointerEvents="none",
t.style.userSelect="none",
t.style.cursor="wait",
window.onload=()=> {
setTimeout(()=> {
t.style.pointerEvents="", t.style.userSelect="", t.style.cursor="", i.remove(), document.body.setAttribute("style", "")
}
, e.timeOnScreen)
}
Convert "myplayer" into a web component.
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
class MyPlayer extends HTMLParagraphElement {
constructor() {
// Always call super first in constructor
super();
// Element functionality written in here
// ADD YOUR CODE HERE
}
}
customElements.define("myplayer", MyPlayer);
This creates an HTML element that has code specific to its self.
1 ISSUE
I am trying to implement the following:
I have a container component ContainerComponent and child components ChildComponent. I want to modify the rendering and overall behaviour of the child components via the controlling ContainerComponent.
2 TECHNOLOGIES USED
Angular2, HTML, CSS, Javascript, Typescript, ES6
3 CODE
ContainerComponent.ts
export class ContainerComponent {
children: Array<Child>;
constructor(
private _el: ElementRef,
private _dcl: DynamicComponentLoader,
private _childService: ChildService) {
}
ngOnInit() {
let index = 0; // index of child component in container
this._childService.getChildren().then( // get the children models
(children) => {
this.children = children;
this.children.forEach((child, index) => {
this._dcl.loadIntoLocation(ChildComponent, this._el, 'dynamicChild')
.then(function(el){
el.instance.child = child; // assign child model to child component
el.instance.index = index;
});
});
}
);
}
}
ChildComponent.ts
export class ChildComponent {
child: Child;
index: number;
constructor(private _renderer: Renderer, private _el: ElementRef) {
}
ngOnInit() {
let delay = (this.index + 1) * 0.5; // calculate animation delay
this._renderer.setElementStyle(this._el, '-webkit-animation-delay', delay + 's !important');
this._renderer.setElementStyle(this._el, 'animation-delay', delay + 's !important');
}
}
4 CODE EXPLANATION
In the above code, the ContainerComponent dynamically inserts ChildComponents (granted, this could be done without the DynamicContentLoader).
The ChildComponents should dynamically add css properties, in this case, the animation delay once it is displayed. So based on the index of the child, the animation delay increases.
However the modifications from the renderer do not take effect, the css properties are not there at runtime.
I tried to reproduce your problem. In fact, I have problem to add styles like -webkit-animation-delay and animation-delay.
If I try with another style like color, it works fine and the style is taken into account at runtime.
ngOnInit() {
this._renderer.setElementStyle(this._el, 'color', 'yellow');
}
So it seems to be linked to animation styles... I see these links that could interest you:
How to set CSS3 transition using javascript?
http://www.javascriptkit.com/javatutors/setcss3properties.shtml
Otherwise it seems that there is some support for animation in Angular2 but it's not really documented... See this file: https://github.com/angular/angular/blob/master/modules/angular2/src/animate/animation.ts.
Hope it helps you,
Thierry
This seems to be a bug in angular2 itself. Adding !important to a style will result in an illegal value to the style and it is not applied to the element. The correct way in plain js is to use another parameter which implies if the style is important.
So the correct answer is to use:
this._renderer.setElementStyle(this._el, 'animation-delay', delay + 's'); //sans !important
and if you want to add !important you have to use:
this._el.nativeElement.style.setProperty('animation-delay', delay + 's', 'important');
The -webkit- prefix gets added (or removed) if necessary, so there is no need to add that as well
From here:
https://angular.io/docs/ts/latest/api/core/ElementRef-class.html
You should only use ElementRef as an absolute last resource. The whole idea of Angular 2 is that you don't have to mess with the dom at all. What you are trying to do can be acomplished very easy using a template:
import {NgStyle} from 'angular2/common';
import {Component} from "angular2/core";
#Component({
selector: 'app',
template: `
<div *ngFor="#child of children; #i = index">
<div [ngStyle]="{ 'z-index': i * multiplier,
'-webkit-animation-delay': i * multiplier + 's',
'animation-delay': i * multiplier + 's' }"> {{i}} - {{child}} </div>
</div>
`,
directives: [NgStyle]
})
export class AppComponent{
public children:string[] = [ "Larry", "Moe", "Curly" ];
public multiplier:number = 2;
}
Depending on the browser you might see those css properties or not, that's why I added the z-index which is more common and old so you can see you can render the css value dynamically using the index variable from ngFor inside a template.
I hope this helps !
I'm displaying text that was stored in the database. The data is coming from firebase as a string (with newline breaks included). To make it display as HTML, I originally did the following:
<p className="term-definition"
dangerouslySetInnerHTML={{__html: (definition.definition) ? definition.definition.replace(/(?:\r\n|\r|\n)/g, '<br />') : ''}}></p>
This worked great. However there's one additional feature. Users can type [word] and that word will become linked. In order to accomplish this, I created the following function:
parseDefinitionText(text){
text = text.replace(/(?:\r\n|\r|\n)/g, '<br />');
text = text.replace(/\[([A-Za-z0-9'\-\s]+)\]/, function(match, word){
// Convert it to a permalink
return (<Link to={'/terms/' + this.permalink(word) + '/1'}>{word}</Link>);
}.bind(this));
return text;
},
I left out the this.permalink method as it's not relevant. As you can see, I'm attempting to return a <Link> component that was imported from react-router.However since it's raw HTML, dangerouslySetInnerHTML no longer works properly.
So I'm kind of stuck at this point. What can I do to both format the inner text and also create a link?
You could split the text into an array of Links + strings like so:
import {Link} from 'react-router';
const paragraphWithLinks = ({markdown}) => {
const linkRegex = /\[([\w\s-']+)\]/g;
const children = _.chain(
markdown.split(linkRegex) // get the text between links
).zip(
markdown.match(linkRegex).map( // get the links
word => <Link to={`/terms/${permalink(word)}/1`}>{word}</Link> // and convert them
)
).flatten().thru( // merge them
v => v.slice(0, -1) // remove the last element (undefined b/c arrays are different sizes)
).value();
return <p className='term-definition'>{children}</p>;
};
The best thing about this approach is removing the need to use dangerouslySetInnerHTML. Using it is generally an extremely bad idea as you're potentially creating an XSS vulnerability. That may enable hackers to, for example, steal login credentials from your users.
In most cases you do not need to use dangerouslySetHTML. The obvious exception is for integration w/ a 3rd party library, which should still be considered carefully.
I ran into a similar situation, however the accepted solution wasn't a viable option for me.
I got this working with react-dom in a fairly crude way. I set the component up to listen for click events and if the click had the class of react-router-link. When this happened, if the item has a data-url property set it uses browserHistory.push. I'm currently using an isomorphic app, and these click events don't make sense for the server generation, so I only set these events conditionally.
Here's the code I used:
import React from 'react';
import _ from 'lodash';
import { browserHistory } from 'react-router'
export default class PostBody extends React.Component {
componentDidMount() {
if(! global.__SERVER__) {
this.listener = this.handleClick.bind(this);
window.addEventListener('click', this.listener);
}
}
componentDidUnmount() {
if(! global.__SERVER__) {
window.removeEventListener("scroll", this.listener);
}
}
handleClick(e) {
if(_.includes(e.target.classList, "react-router-link")) {
window.removeEventListener("click", this.listener);
browserHistory.push(e.target.getAttribute("data-url"));
}
}
render() {
function createMarkup(html) { return {__html: html}; };
return (
<div className="col-xs-10 col-xs-offset-1 col-md-6 col-md-offset-3 col-lg-8 col-lg-offset-2 post-body">
<div dangerouslySetInnerHTML={createMarkup(this.props.postBody)} />
</div>
);
}
}
Hope this helps out!
Hiello!
I'm wondering whats wrong in the React example bellow or if React works differently than I thought?
I'm looking for a way to reuse the underlying html element for a child react component, when the parents are two different components.
In the example bellow, I would like the inside the Circle component to have the same element after renderC1 and renderC2 is called. For instance so that I could apply a transition css property to animate the color switch, like they would if I e.g. just changed the style directly on the element.
When I render the bellow, React always seems to generate different HTML elements, ref, key or id on the DIV (in the render function of Circle) doesn't help much.
So my questions: is it possible to get React to just reuse the DIV that gets rendered via C1 when C2 is rendered? I thought this was how React should work, optimizing the underlying HTML elements?
Something like:
var C1 = React.createClass({
render: function () {
return (
<Circle background="deeppink" onClick={renderC2}/>
);
}
});
function renderC1 () {
React.render(
<C1 />,
document.getElementById('mount-point'));
}
var C2 = React.createClass({
render: function () {
return (
<Circle background="salmon" onClick={renderC1}/>
);
}
});
function renderC2 () {
React.render(
<C2 />,
document.getElementById('mount-point'));
}
var Circle = React.createClass({
styler: {
width: "100px",
height: "100px",
mozBorderRadius: "50%",
webkitBorderRadius: "50%",
borderRadius: "50%",
background: 'hotpink'
},
componentWillMount: function() {
if (this.props && this.props.background &&
this.props.background !== this.styler.background) {
this.styler.background = this.props.background;
}
},
render: function() {
return (
{/* tried adding key, ref and id, but does not reuse element */}
<div onClick={this.props.onClick} style={this.styler}></div>
);
}
});
renderC1();
This is impossible. The DOM does not allow one element to be in two places at once. Attempting to put a DOM element in a new location will automatically remove it from the old location.
You can see that here. (or more visually, here)
var parent1 = document.createElement('div'),
parent2 = document.createElement('div'),
child = document.createElement('div'),
results = document.createElement('span');
document.body.appendChild(results);
parent1.appendChild(child);
results.textContent += child.parentNode === parent1; //true
parent2.appendChild(child);
results.textContent += ', ' + (child.parentNode === parent1); //false