Rendering SVG with no container in Plastiq - javascript

I'm using Plastiq to create a simple icon component that renders SVG:
module.exports = function(icon) {
return h.rawHtml('.icon',
'<svg><use xlink:href="#i-' + icon + '"></use></svg>'
);
}
I'm using rawHtml since SVG isn't supported yet. This returns the HTML:
<div class="icon">
<svg>
<use xlink:href="#i-example"></use>
</svg>
</div>
I would prefer the HTML:
<svg class="icon">
<use xlink:href="#i-example"></use>
</svg>
But this doesn't work:
module.exports = function(icon) {
return h.rawHtml('<svg class="icon"><use xlink:href="#i-' + icon + '"></use></svg>');
}
Nor does this:
module.exports = function(icon) {
return h.rawHtml('svg.icon',
'<use xlink:href="#i-' + icon + '"></use>'
);
}
Or this:
module.exports = function(icon) {
return h('svg.icon',
h.rawHtml('<use xlink:href="#i-' + icon + '"></use>')
);
}
Looks like rawHtml requires an element selector Plastiq has support for? Is there a recommended approach for this?

Related

Angular + SVG: how to dynamically load template from string

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>

How to bind events on raw (imported) html via code?

I'm importing a html containing SVG code and render it via v-html directive, but I need to bind click events to the a-html tags from that file which aren't part of my template.
How do I get all the a-tag from that imported file?
And how do I bind events to these elements whitout using v-on directive?
The template
<v-flex mt-2 px-0 xs6
ref="map_svg"
v-html="mapHTML">
</v-flex>
Snippet of the imported file
<svg>
<g>
<g>
<a target="_blank" xlink:href="url to a subsite" >
<!-- content path which shows a circle -->
</a>
</g>
<g>
<a target="_blank" xlink:href="url to a differnt subsite" >
<!-- content path which shows a circle -->
</a>
</g>
<!-- many more elements like shown on the top -->
</g>
</svg>
I would need to bind an click event to these a-tags and remove or overwrite the xlink:href attribute so the click won't open a different tab in the browser.
Edit
I ended up doing it like this:
mounted() {
const that = this;
const links = this.$el.querySelectorAll('#map_svg svg > g > g > a');
links.forEach(el => {
const url = el.getAttribute('xlink:href');
if (url) {
// el.addEventListener('click', that.stationClick(url));
el.onclick = function (){
that.stationClick(url);
}
el.removeAttribute('xlink:href');
}
el.removeAttribute('target');
});
},
You can bind click events on those elements after you have loaded your svg:
export default {
name: "App",
data() {
return {
mapHTML: null
};
},
methods: {
async loadSvg() {
// Here, you load your svg
this.mapHTML = `
Google
`;
// You need to use $nextTick in order to wait for the DOM to be refreshed
this.$nextTick(() => {
this.bindClicks() // You bind click events here
})
},
bindClicks () {
// You search for all the a tags in your svg_container
this.$refs.map_svg.querySelectorAll('a').forEach(el => {
el.removeAttribute('_target') // You remove the _target attribute
el.addEventListener('click', this.handleClick) // You bind an event listener to the element
})
},
handleClick (e) {
// You handle the click like you would normally do
e.preventDefault()
}
}
};
You can use the ref you set and plain JS to bind to the tags.
this.$refs.map_svg.querySelectorAll('a').forEach(item => item.addEventListener('click', this.callback) )

React JSX Dynamic SVG Translate Error

I'm new to React and trying to do a dynamic Svg Transition where I translate the Position of a Group according to the current Component State. For some reason I get the Error Message:
DOMPropertyOperations.js:139 Error: <g> attribute transform: Expected
'(', "translate:(100 100)".
Here is the render function:
render() {
let turns = Route.Leg.map((turn) => <circle cx={turn.to.location.latitude} cy={turn.to.location.longitude} r="5"></circle>)
let legs = Route.Leg.map((leg) => <line x1={leg.from.location.latitude} y1={leg.from.location.longitude} x2={leg.to.location.latitude} y2={leg.to.location.longitude} stroke="black"></line>)
let move = "translate:(" + this.state.pos.location.latitude + " " + this.state.pos.location.longitude + ")";
return (
<div>
<svg width="800px" height="800px">
<circle cx="400" cy="400" r="10"></circle>
<g transform={move}>
{turns}
{legs}
</g>
</svg>
</div>
);
}
The lines and circles are drawn correctly, and when I log the "move" variable and looks correct every time the dom updates. When I hardcode the translate it also works. Does someone have an idea what is wrong here, or am I just missing something? Cheers in advance
As says in the error Expected (',...
// Look down, there is no ":" character in css syntax. And check for ","
let move = "translate(" + this.state.pos.location.latitude + "," + this.state.pos.location.longitude + ")";
That's because of you are using wrong syntax. You should use translate(... not translate:(....
Also you should comma seperate values inside translate

React SVG rendering issue

I'm trying to import and SVG logo as a component and I get the error as seen in the screenshot below.
Opening the SVG into a web browser, this renders fine as expected but when dropping into my app it doesn't like it for some reason.
The only thing I note is the name of the SVG - the actual name of the svg is "test.svg" however, the error recognises it as "test.5d5d9eef.svg". I'm not sure where or how this number is being generated.
Please also note the "SVG" code within my component is a test to see if this will render and it does render just fine.
Here is my code:
import React from 'react';
import MainLogo from '../../../assets/svg/logos/test.svg';
export default class Logo extends React.Component {
render() {
return (
<div className="logo-wrap col-xs-2">
<h1>Heading</h1>
<MainLogo />
<svg>
<circle cx={50} cy={50} r={10} fill="red" />
</svg>
</div>
)
}
}
You can create a generic component which accepts some props and creates any SVG for you, like this:
import React from 'react';
export class BuildNewSVG extends React.Component {
constructor(props) {
super(props);
}
render() {
let bg = props.color || '#000';
let height = props.height || props.size || '24';
let width = props.width || props.size || '24';
let path1 = props.path1 || 'M7 10l5 5 5-5z';
let path2 = props.path2;
let fill1 = props.fill1 || '';
let fill2 = props.fill2 || 'none';
let viewBox = props.viewBox || '0 0 24 24';
return (
<svg fill={bg} className={props.class} height={height} viewBox={viewBox} width={width}>
<path d={path1} fill={fill1} />
<path d={path2} fill={fill2} />
</svg>
);
}
}
You can call the component like this:
<BuildNewSVG color={'red'} height={'100'} viewBox={'0 0 24 24'} width={'100'} size={'50'}
path1="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" path2="M0 0h24v24H0z" fill1={'#ffffff'} fill2={'red'} />
Note: This will be a generic component and will only accept SVG paths (path1 and path2). Path2 is the outer container. Any simple or complex shape (for example: circle) can be defined as svg path(s).

How can I get Knockout.js to set the namespaceURI for attributes?

In an svg, if I use knockout to set the xlink:href attribute for an a node, the attribute's namespace isn't set correctly, so the a doesn't work as a link when clicked.
For example, consider the following svg that contains two linked ellipses. One has its xlink:href attribute hardcoded, the other is set by knockout via the data-bind attribute:
<svg width="5cm" height="6cm" viewBox="0 0 5 6" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x=".01" y=".01" width="4.98" height="5.98"
fill="none" stroke="blue" stroke-width=".03"/>
<a xlink:href="#hardcoded">
<ellipse data-bind="attr: blue" />
</a>
<a data-bind="attr: { 'xlink:href': href }">
<ellipse data-bind="attr: red" />
</a>
</svg>
Getting knockout to run is pretty easy:
ko.applyBindings( {
blue: { cx:2.5, cy:1.5, rx:2, ry:1, fill:"blue" },
href: '#foo',
red: { cx:2.5, cy:4.5, rx:2, ry:1, fill:"red" },
});
But only the link for the hardcoded one works. If I add some code to view the namespaceURI value for the attribute node, I can see that
the xlink:href attribute set by knockout has a null namespaceURI, as opposed to the hardcoded one, which is set to http://www.w3.org/1999/xlink.
Array.forEach( document.getElementsByTagName('a'), function(a){
a.setAttribute('title', 'xlink:href namespaceURI = ' + a.getAttributeNode('xlink:href').namespaceURI);
});
You can view all this in a fiddle.
Is there an easy way to tell knockout what the correct namespace should be for an attribute, or do I need to write a custom binding?
My fallback solution is to add this custom binding:
ko.bindingHandlers['attr-ns'] = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.utils.objectForEach( ko.unwrap( valueAccessor() ), function(name, value){
var prefixLen = name.indexOf(':');
var prefix = name.substr( 0, prefixLen );
var namespace = prefixLen < 0 ? null : element.lookupNamespaceURI( prefix );
element.setAttributeNS( namespace, name, ko.unwrap( value ) );
});
}
};
and change the template to use the attr-ns binding I defined:
<a data-bind="attr-ns: { 'xlink:href': href }">
<ellipse data-bind="attr: red" />
</a>
This seems to work ok, but I'd rather not do this if I don't have to. More custom code = more that could go wrong.
You can view this solution in a fiddle.
You'd have to override the default attr binding handler with a namespace-aware version.
FWIW, here is my take on it, use it as a transparent drop-in, it even supports namespace prefixes:
var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' },
namespaceDecl = {
svg: "http://www.w3.org/2000/svg",
xlink: "http://www.w3.org/1999/xlink"
};
ko.bindingHandlers['attr'] = {
'update': function(element, valueAccessor, allBindings) {
var value = ko.utils.unwrapObservable(valueAccessor()) || {};
ko.utils.objectForEach(value, function(attrName, attrValue) {
var attrRawValue = ko.utils.unwrapObservable(attrValue),
toRemove = (attrRawValue === false) || (attrRawValue === null) || (attrRawValue === undefined),
attrStrValue = attrRawValue.toString(),
attrNameParts = attrName.split(":"),
attrNsUri = (attrNameParts.length === 2) ? namespaceDecl[attrNameParts[0]] : null,
attrName = (attrNsUri) ? attrNameParts[1] : attrNameParts[0];
if (toRemove) {
if (attrNsUri) {
element.removeAttributeNS(attrNsUri, attrName);
} else {
element.removeAttribute(attrName);
}
}
if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap && !attrNsUri) {
attrName = attrHtmlToJavascriptMap[attrName];
if (toRemove)
element.removeAttribute(attrName);
else
element[attrName] = attrValue;
} else if (!toRemove) {
if (attrNsUri) {
element.setAttributeNS(attrNsUri, attrName, attrStrValue);
} else {
element.setAttribute(attrName, attrStrValue);
}
}
if (attrName === "name") {
ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
}
});
}
};
http://jsfiddle.net/ZghP7/1/

Categories