Add CSS rules from textarea using v-html - javascript

I am building a WYSIWYG type application where a user can write CSS in a textarea and that CSS rule will be applied to the HTML on page i tried something like this in template
<textarea v-bind="css"></textarea>
<style v-html="css"></style>
VueCompilerError: Tags with side effect ( and ) are ignored in client component templates.

Old answer, below is better one
Add textarea with v-model:
<textarea v-model="css" />
You can create style tag in onMounted hook:
onMounted(() => {
const style = document.createElement("style");
style.type = "text/css";
updateCss(style, css.value);
document.getElementsByTagName("head")[0].appendChild(style);
el.value = style;
});
You must be able to access this element later, so assign style to
el.value.
Then add watch on input value:
watch(css, () => {
updateCss(el.value, css.value);
});
Where updateCss is a function:
const updateCss = (el, css) => {
if (el.styleSheet) {
el.styleSheet.cssText = css.value;
} else {
el.innerHTML = "";
el.appendChild(document.createTextNode(css));
}
};
Demo:
https://codesandbox.io/s/cocky-mestorf-uqz6f?file=/src/App.vue:246-463
Edit
I found much better solution:
<template>
<textarea v-model="css" />
<component :is="'style'" type="text/css" v-text="css"></component>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const css = ref("body { background-color: blue; }");
return { css };
},
};
</script>
Component doesn't throw the error about style tag:
<component :is="'style'">
Note that there is v-text instead v-html. V-html could be unsafe.
Demo:
https://codesandbox.io/s/festive-germain-q9tg3?file=/src/App.vue:122-281

Related

Cannot appendChild to a custom web component

I have a web-component at root level. The simplified version of which is shown below:
class AppLayout {
constructor() {
super();
this.noShadow = true;
}
connectedCallback() {
super.connectedCallback();
this.render();
this.insertAdjacentHTML("afterbegin", this.navigation);
}
render() {
this.innerHTML = this.template;
}
get template() {
return `
<h1>Hello</h1>
`;
}
navigation = `
<script type="module">
import './components/nav-bar.js'
</script>
`;
}
customElements.define('app-layout', AppLayout);
I want to load a script after this component loads. The script creates html for navigation and tries to add it to the app-layout element shown above. However, even though, it does find the app-layout element, it is unable to append the navBar element. It is, however, able to append the navBar to the body of the html. Any ideas what I'm missing.
const navLinks =
`<ul>
<li>Some</li>
<li>Links</li>
</ul>
`;
const navBar = document.createElement('nav');
navBar.innerHTML = navLinks;
const appLayout = document.querySelector('app-layout'); // works with 'body' but not with 'appLayout'
console.log(appLayout); // Logs correct element
appLayout.appendChild(navBar);
I know that what I'm trying to do here (loading a script inside a web component) is not ideal, however, I would like to still understand why the above doesn't work.
using innerHTML or in your case insertAdjacentHTML to add <script> tags to the document doesn't work because browsers historically try to prevent potential cross site script attacks (https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0)
What you could do is something like:
const s = document.createElement("script");
s.type = "module";
s.innerText = `import './components/nav-bar.js'`;
this.append(s);
// or simply directly without the script: `import('./comp..')` if it is really your only js in the script tag.

Loading HTML file content in a Vue component

I'm having troubles loading the content of an HTML file in a Vue component. Basically i have a Django backend that generates an HTML file using Bokeh and a library called backtesting.py. My frontend is using Nuxt/Vue, so i can't just load the HTML on the page dynamically.
Here is what the HTML file looks like (it was too long to post here): https://controlc.com/aad9cb7f
The content of that file should be loaded in a basic component:
<template>
<div>
<h1>Some content here</h1>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
The problem is that i really don't know how to do that. If i just copy and paste the content in the vue component, i'll get a lot of error due to the fact that i'm using a <script> tag in a component. The only thing i managed to do was to load the BokehJS CDN in my index.html file, but even after that i'll get a Bokeh is undefined error in the component.
What can i do to accomplish this? Any kind of advice is appreciated
Tao's answer is spot on and is very similar to how I've solved this issue for myself in the past.
However, I'd like to throw in an alternative iframe approach that could work in case reactivity is important. Here's a codesandbox link
The only difference is that this approach loads the code/HTML via XHR and writes it manually into the iframe. Using this approach, you should be able to add some reactivity if necessary.
<script>
export default {
components: {},
data() {
return {};
},
async mounted() {
this.initialize();
},
methods: {
async initialize() {
const html = await this.loadHTML();
const doc = this.htmlToDocument(html);
this.updateIframe(doc);
},
async loadHTML() {
const response = await fetch("/plot");
const text = await response.text();
return text;
},
htmlToDocument(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
},
updateIframe(doc) {
const iframe = this.$refs.frame;
const iframeDocument = iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(doc.documentElement.innerHTML);
iframeDocument.close();
}
},
};
</script>
In the codesandbox, I've thrown in two additional methods to give you an example of how reactivity can work with this approach:
modify() {
if (this.orig) {
// Only for the purpose of this example.
// It's already been modified. Just short-circuit so we don't overwrite it
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
// Store old data so we can reset it
this.orig = data.Close[idx];
data.Close[Math.floor(data.Close.length / 2)] = 0;
modelWithData.change.emit();
},
reset() {
if (!this.orig) {
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
data.Close[idx] = this.orig;
modelWithData.change.emit();
delete this.orig;
}
Probably the simplest way is to make your HTML available at the URL of your choice, on your server (regardless of Vue).
Then, in your app, use an <iframe> and point its src to that html. Here's an example, using codesandbox.io, where I placed what you posted into the index.html. Below you can see it working with both <iframe> and <object> tags:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
'el': '#app'
})
body {
margin: 0;
}
h1, h3 {padding-left: 1rem;}
object, iframe {
border: none;
height: 800px;
width: 100%;
min-height: calc(100vh - 125px);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>This content is placed in Vue</h1>
<h3>Vue doesn't really care.</h3>
<iframe src="https://1gk6z.csb.app/"></iframe>
<h1><code><object></code> works, too:</h1>
<object type="text/html" data="https://1gk6z.csb.app/"></object>
</div>
Note: if the domain serving the graph and the one displaying it differ, you'll need server-side configuration to allow the embed (most domains have it turned off by default).
Strategy:
insert and init bokeh in head tag of public/index.html
read file in a string via ajax/xhr and parse as dom tree
extract each needed dom element from the parsed tree
recreate and append each element
No iframe needed. window.Bokeh is directly accessible.
A skeletal example of reactivity is suggested through the method logBkh that logs the global Bokeh object when clicking on the graph
<template>
<div id="app">
<div id="page-container" #click="logBkh"></div>
</div>
</template>
<script>
// loaded from filesystem for test purposes
import page from 'raw-loader!./assets/page.txt'
// parse as dom tree
const extDoc = new DOMParser().parseFromString(page, 'text/html');
export default {
methods: {
logBkh(){
console.log(window.Bokeh)
}
},
mounted() {
const pageContainer = document.querySelector('#page-container')
// generate and append root div
const dv = document.createElement('div')
const { attributes } = extDoc.querySelector('.bk-root')
for(const attr in attributes) {
dv.setAttribute(attributes[attr].name, attributes[attr].value)
}
pageContainer.append(dv)
for(const _scrpt of extDoc.body.querySelectorAll('script')) {
// generate and append each script
const scrpt = document.createElement('script')
for(const attr in _scrpt.attributes) {
scrpt.setAttribute(
_scrpt.attributes[attr].name,
_scrpt.attributes[attr].value
)
}
scrpt.innerHTML = _scrpt.innerHTML
pageContainer.append(scrpt)
}
}
}
</script>
result:

Add class after return (or map) Javascript + React

i'm trying to add a class after returning a map, basically im trying to highlight the text after translating each word im doing it by adding a CSS class to it. I'm able to add the class but just to the whole box instead of the words i changed, i've tried many methods but i guess im not understanding well the functions, i've tried matchAll, also tried replacing them as the previous method, and tried styling them directly but im still unable to achieve it, i've also tried adding the function right after the "return" but javascript ends the execution of the function when return is called.... :/
Thanks for your time!
export default function Text() {
var inputText;
var mapObj = { blue: "azul", green: "verde", yellow: "amarillo" };
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
outputF.classList.add("mark");
}
return (
<div>
<textarea id="inputF"></textarea>
<button onClick={changeText}>Change</button>
<textarea id="outputF"></textarea>
</div>
);
};
///CSS
.mark{
background-color:blue;}
I would recommend using more React here instead of native JS especially for the state management of this component I may have gone a bit over board, but I updated your component to the following:
const Text = () => {
const [input, setInput] = React.useState('');
const [output, setOutput] = React.useState('');
const mapObj = {
blue: "azul",
green: "verde",
yellow: "amarillo"
};
const changeText = (e) => {
e.preventDefault();
const changedText = input.replaceAll(
/\b(blue|green|yellow)\b(|,.$)/gi,
(matched) => `<span class="mark">${mapObj[matched]}</span>`
)
setOutput(changedText);
}
return (
<form>
<textarea
id="inputF"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit" onClick={changeText}>Change</button>
<div
id="outputF"
dangerouslySetInnerHTML={{__html: output}}
/>
</form>
);
};
Fiddle: https://jsfiddle.net/3tc2xzms/
For starters, I added 2 useState hooks to manage the input and output text. And then for the highlight portion, each matched word that was changed gets wrapped in a span.mark. Also the output textarea was changed to a div.
Related Docs:
Forms: https://reactjs.org/docs/forms.html
State Hook: https://reactjs.org/docs/hooks-state.html
Setting string as HTML: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
The style you applied will change the background color of whole textarea, what you can do instead is use setSelectionRange() method of textarea, that will give you the result near to what you are expecting.
function changeText() {
inputText = document.querySelector("#inputF");
document.getElementById("outputF").innerHTML = inputText.value.replaceAll(/\b(blue|green|yellow)\b(|,.$)/gi,
function(matched){
return mapObj[matched];
})
// something like this
// here you need to set start and end index for the highlights part, you can use the logic as per need
outputF.setSelectionRange(0, inputText.value.length )
}
and to change the highlight color you can use the following css:
::selection {
color: red;
background: yellow;
}
::-moz-selection { /* Code for Firefox */
color: red;
background: yellow;
}

Change direction with selecting Language in Vuejs

I have a Language select-option. If I choose Arabic then it will change the direction.
* {
direction: rtl!important;
}
While I am using this, then the whole direction changed to right to left. But how can I do that with Methods?
methods: {
languageSelection() {
if(this.lang == 'arb') {
document.getElementsByTagName("*")[0].style.direction = "rtl!important";
document.getElementsByTagName("html")[0].style.direction = "rtl!important";
document.getElementsByTagName("body")[0].style.direction = "rtl!important";
}
}
}
The above code is not working!
If I can add a CSS file then it will be better for me. For example:
languageSelection() {
if(this.lang == 'arb') {
// active a css file like: style-rtl.css
}
}
But how is this possible?
Ok. So when you use getElementsByTagName("*")[0] you will probably get a handle to a <html> element. So the same element you're accessing in the next line.
To change all elements direction you would have to iterate over the collection:
const elems = document.getElementsByTagName("*")
for (let elem of elems) {
elem.style.direction = 'rtl'
}
But this will still include <script>, <style>, <meta> tags, which is not the best solution.
My solution
I would create class in the css
html.is-rtl * {
direction: rtl;
}
then just toggle the class when you select the language which is read from right to left.
languageSelection() {
if (this.lang == 'arb') {
document.querySelector('html').classList.add('is-rtl')
}
}
Could create a stylesheet and append it to the head:
languageSelection() {
if(this.lang == 'arb') {
const style = document.createElement('style');
style.innerHTML = `*{direction:rtl!important}`;
document.head.appendChild(style);
}
}
You should manage it inside App.vue. add custom css class based on chosen language to #App element.
<template>
<div id="app" :class="{'dir-rtl': isRtl}">
...
</div>
</template>
<script>
export default {
computed: {
isRtl () {
return this.lang === 'arb'
}
}
}
</script>
<style lang="scss">
.dir-rtl {
direction: rtl !important;
}
</style>
best place to save and change lang is vuex store.

Trying to use React.DOM to set body styles

How can I use React.DOM to change styles on HTML body?
I tried this code and it's not working:
var MyView = React.createClass({
render: function() {
return (
<div>
React.DOM.body.style.backgroundColor = "green";
Stuff goes here.
</div>
);
}
});
If you execute this from the browsers console it works (but I need it working in ReactJS code):
document.body.style.backgroundColor = "green";
Also see this question for similar but different solution:
Change page background color with each route using ReactJS and React Router?
Assuming your body tag isn't part of another React component, just change it as usual:
document.body.style.backgroundColor = "green";
//elsewhere..
return (
<div>
Stuff goes here.
</div>
);
It's recommended to put it at componentWillMount method, and cancel it at componentWillUnmount:
componentWillMount: function(){
document.body.style.backgroundColor = "green";
}
componentWillUnmount: function(){
document.body.style.backgroundColor = null;
}
With functional components and useEffect hook :
useEffect(() => {
document.body.classList.add('bg-black');
return () => {
document.body.classList.remove('bg-black');
};
});
A good solution to load multiple atributes from a js class to the document body can be:
componentWillMount: function(){
for(i in styles.body){
document.body.style[i] = styles.body[i];
}
},
componentWillUnmount: function(){
for(i in styles.body){
document.body.style[i] = null;
}
},
And after you write your body style as long as you want:
var styles = {
body: {
fontFamily: 'roboto',
fontSize: 13,
lineHeight: 1.4,
color: '#5e5e5e',
backgroundColor: '#edecec',
overflow: 'auto'
}
}
The best way to load or append extra classes is by adding the code in componentDidMount().
Tested with react and meteor :
componentDidMount() {
var orig = document.body.className;
console.log(orig); //Just in-case to check if your code is working or not
document.body.className = orig + (orig ? ' ' : '') + 'gray-bg'; //Here gray-by is the bg color which I have set
}
componentWillUnmount() {
document.body.className = orig ;
}
This is what I ended up using.
import { useEffect } from "react";
export function useBodyStyle(style: any){
useEffect(()=>{
for(var key in style){
window.document.body.style[key as any] = style[key];
}
return () => {
window.document.body.style[key as any] = '';
}
}, [style])
}
Even if you can set body styles from react with the provided answer, I prefer if a component is only responsible for setting its own style.
In my case there was an alternative solution. I needed to change the body backgroundColor. This could easily be achieved without changing the body style in a react component.
First I added this style to the index.html header.
<style>
html, body, #react-app {
margin: 0;
height: 100%;
}
</style>
Then, in my outermost component, I set the backgroundColor to the needed value and the height to 100%.

Categories

Resources