Vue.js detach styles from template - javascript

I use a template with a <style> block which must be near its div for CMS reasons.
When I run Vue.js, it seems to remove the style block, saying...
- Templates should only be responsible for mapping the state to the UI.
Avoid placing tags with side-effects in your templates, such as <style>,
as they will not be parsed.
What can I do?
var app = new Vue({
el: '#app'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script>
<div id="app">
<style>
#div_123 {
background: http://placehold.it/850x150;
}
#media screen and (max-width: 640px) {
#div_123 {
background: http://placehold.it/350x150;
}
}
</style>
<div id="div_123">
Test
</div>
</div>

The Issue
In Vue 2 the root instance is treated more like a component than it was in Vue 1.
This means when you bind the Vue instance to #app it digests everything in #app as a vue template. This means tags are invalid and they'll be removed from the template. This is just the way things work in Vue 2.
Recreation
I recreated the issue in a codepen here
https://codepen.io/Fusty/pen/gqXavm?editors=1010
The <style> tag nested within the tag Vue is bound to. It should style the background red and the text color green. However, we see only a flash of this (depending on how fast your browser fires up Vue) and eventually vue removes these style tags as it digest #app as a template and then updates the DOM with what it thinks should be there (without <style> tags).
Better Recreation
Thanks to user #joestrouth1#6053 on the Vue-Land discord, we also have this fork of my recreation of the issue.
https://codepen.io/joestrouth1/pen/WPXrbg?editors=1011
Check out the console. It reads . . .
"[Vue warn]: Error compiling template:
Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as <style>, as they will not be parsed.
1 | <div>
2 | <style>
| ^^^^^^^
... etc ...
Complaining about the style tags in a template.
This zeroes in on the actual issue. It is good to note this doesn't occur in Vue 1. Probably because it treats the root instance more uniquely than components, but I am not 100% sure on this topic.
Solution (Hack, not best practice or especially recommended)
The <style> tags are still in the DOM during the created lifecycle hook for the Vue instance and they are removed by the time the mountedlifecycle hook fires. Let's just query for all of the style tags within the #app element, save them, and then append them back to the #app element after Vue has digested the template.
Adding the following to your root Vue instance will take any <style> tags within whatever element your Vue instance is bound to (via el: 'someSelector') and append them (possibly relocating them) to the element your Vue instance is bound to.
created: function() {
this.styleTagNodeList = document.querySelector(this.$options.el).querySelectorAll('style');
},
mounted: function() {
for(var i = 0; i < this.styleTagNodeList.length; ++i)
this.$el.appendChild(this.styleTagNodeList[i]);
}
NOTE: This is definitely a hack which likely has unintended consequences I have not run into yet and cannot specifically disclaim. USE AT YOUR OWN RISK.

This works for my specific situation where I allow the users to store a string of CSS and then I need to render it on specific pages - ei: preview page.
The context here is css is saved as string in database, fetched and rendered within an Vue component.
# html
<html>
<head>
<style id="app_style"></style>
</head>
<body>
<div id="app"></div>
</body>
</html>
# app.vue
data() {
return {
dynamic_css: ''
}
},
created() {
// fetch css from database, set as `this.dynamic_css`
},
watch {
dynamic_css: function(newValue) {
document.getElementById('app_style').innerHTML = newValue
}
}

Related

Load essential styles to shadow-dom

I love the idea of shadow dom styles encapsulation, but I'd like to include base styles to each shadow dom (reset, typography, etc).
<head>
<link rel="stylesheet" href="core.css">
...
</head>
<my-component></my-component>
<script>
customElements.define('my-component', class MyComponent extends HTMLElement {
...
connectedCallback() {
this.shadow = this.attachShadow({mode: 'open'});
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'core.css');
// applying exiting "core.css" to current shadow dom
this.shadow.appendChild(linkElem);
}
});
</script>
Since core.css was called (linked) twice will it affect the performance?
You could try use Constructable Stylesheet Objects
With this approach you can define global styles and then use them with shadowRoot.adoptedStylesheets like the following:
import {
resetSheet,
headlinesSheet,
coreSheet,
} from '/style-system.js';
import {
myComponentStyles,
} from './styles.js';
// ...
connectedCallback() {
// Only compose styles once
if (!this.shadowRoot.adoptedStyleSheets.length) {
this.shadowRoot.adoptedStyleSheet = [
// global stylesheets
resetSheet,
headlinesSheet,
coreSheet,
// specific sheet for this component
myComponentStyles
]
}
}
The main advantages with this approach in comparison with the one you're using (creating link elements to each component) are:
You can share global styles to be used across multiple components defining them once
You only load the styles being used by that component being rendered and this scales performance because it lazily loads stylesheets
You can change those global styles dynamically (as they're a JS component) without the need of applying changes in multiple elements, making more decoupled changes
As this is a quite new approach, I'd recommend you read the following articles in order to create a more solid knowledge about Constructable Stylesheet Objects
Constructible Stylesheets
Why Would Anyone Use Constructible Stylesheets, Anyways?
Adopt a Design System inside your Web Components with Constructable Stylesheets
The browser will cache the request for core.css so there's not really a performance penalty, but since the stylesheet will be loaded asynchronously you might get a flash of unstyled content (FOUC) while the browser fetches the css for the first time.
One possible way to get around this is to preload the css file in your document <head>, so that it will (probably, as preloads are only 'hints' to the browser) be available by the time your Shadow DOM is parsed:
<link rel="preload" href="core.css" as="style">

Change in HTML attribute of web component not reflected in Vue component

I'm facing the below problem.
I have a pure web component:
<my-web-comp options='["op1", "op2"]' active-option="op2"></my-web-comp>
This renders as two tabs with the second one selected by default. When you click on the other, the active-option HTML attribute changes to op1 and you can actually see that the property is changing in the DOM if you open the DevTools.
However, I cannot detect the change in the Vue component where I am using the web component. I have:
<template>
<div>
<my-web-comp :options="options" :active-option="activeOption"></my-web-comp>
</div>
</template>
<script>
export default {
name: 'MyVueComponent',
data() {
return {
options: '["op1", "op2"]',
activeOption: "op2"
}
},
computed: {
testVar() {
console.log("activeOption", this.activeOption) <--------- THIS LINE
},
}
}
</script>
The marked line only gets fired on the first load of the Vue component (printing "op2"). After that, testVar never gets modified again, doesn't mind if I click on the other tab and I don't see nothing in the console.
What can I be missing? I think it can be something related with Vue reactivity system, but can't wonder what.
This happens because your web-component mutates copy not a reference of this variable (copy created by your web component is also not reactive). There are two ways to change this:
You can modify your web component to use getters and setters to change value of this variable
You can use MutationObserver. To detect changes in your web-component. This approach will not require changes in this web-component
If you choose approach with MutationObserver then create this observer in vue mounted life-cycle-hook

JSX HTML string variable displaying as text

I have a JSON file with a variable called htmlContent. I am trying to display this in a component but when it is rendered it shows it as text. I just want the text to be surrounded by an h2 tag.
MY JSON FILE
const BookData = {
data: [
{
id:"1",
pageHeader:"Contents",
htmlContent:`<h2>hello</h2>`,
definePrototypes:"",
exportComponent:"export default App;"
}
]
};
MY REACT COMPONENT
<section id='content'>
{props.htmlContent }
</section>
MY ACTUAL RESULT
<h2>hello</h2>
MY EXPECTED RESULT
hello
You need to use dangerouslySetInnerHTML to render html content in react, otherwise it will show as string
Change to be made in this element
<section id='content'>
{props.htmlContent }
</section>
Change the above block to
<section id='content' dangerouslySetInnerHTML={{ __html: props.htmlContent }}></section>
This should do the job. Check the link for details https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
Try this
<div contentEditable='true' dangerouslySetInnerHTML={{ __html: props.htmlContent }}></div>
The immediate effect of using innerHTML versus dangerouslySetInnerHTML is identical -- the DOM node will update with the injected HTML.
However, behind the scenes when you use dangerouslySetInnerHTML it lets React know that the HTML inside of that component is not something it cares about.
Because React uses a virtual DOM, when it goes to compare the diff against the actual DOM, it can straight up bypass checking the children of that node because it knows the HTML is coming from another source. So there's performance gains.
More importantly- if you simply use innerHTML, React has no way to know the DOM node has been modified. The next time the render function is called, React will overwrite the content that was manually injected with what it thinks the correct state of that DOM node should be.
Your solution to use componentDidUpdate to always ensure the content is in sync I believe would work but there might be a flash during each render.
Reference Dangerously Set innerHTML
If React is imported in the JS file you can just remove the ticks(`) from the JSON like this:
const BookData = {
data: [
{
id:"1",
pageHeader:"Contents",
htmlContent:<h2>hello</h2>,
definePrototypes:"",
exportComponent:"export default App;"
}
]
};

Vue.js workflow from Vue.js 1 to 2

I have followed the Vue.js lessons from laracasts when Vue.js 1 came out and I used to do something like this:
import Comments from './components/Comments.vue';
import Upload from './components/Upload.vue';
new Vue({
el: 'body',
components: {
Comments,
Upload,
Preview,
Algolia,
},
etc,
});
This allowed me to kind of 'sprinkle' components all over my application. I can no longer bind to the body though because Vue replaces the content and also throws an error message saying you shouldn't bind to the body or html.
I followed a couple of lessons for Vue.js 2 but how can I replicate this workflow in the Vue.js 2 manner? I loved just binding to the body and having the option to place a component here and there with the custom tags.
We use the same "sprinkling" approach and all we did was change it from 'body' to '#app'.
We also added a wrapping element inside that had this id to basically replicate body. (https://github.com/onespacemedia/project-template/blob/develop/%7B%7Bcookiecutter.repo_name%7D%7D/%7B%7Bcookiecutter.package_name%7D%7D/templates/base.html#L62)
<body>
<div id="app">
...
</div>
</body>
We use Jinja2 for our templating language and have found when a variable that doesn't resolve in Jinja2 it tanks Vue as well as i think Vue tries to use it.
I believe it takes everything inside #app after initial render and converts it to virtual dom. This doesn't effect anything from what i've seen though so you can happily just add the wrapping class inside body and use it the same as Vue 1

Durandal and Knockout: do a css binding to body element

I am trying to make the body element of a Durandal app (a SPA framework which employs Knockout) by checking a module's state (which account type users are logged in as). In the viewmodel of the app's shell, I have the following code:
function bindingComplete() {
viewhelper.accountVisualTreatment();
}
And accountVisualTreatment is defined in the viewhelper module as such:
if (typeof(appsecurity.userInfo()) == 'undefined') {
$("body").addClass("notloggedin");
$(".container").addClass("notloggedin");
} else {
if (appsecurity.isUserInRole(['Account Manager']) && !appsecurity.isUserInRole(['Administrator'])) {
$("body").addClass("accountmanager");
$("nav").addClass("hidden");
} else {
$("body").removeClass();
$(".container").removeClass("notloggedin");
$("nav").removeClass("hidden");
}
}
Everything works fine if I refresh the pages when logged in as the diff account types. But as a SPA framework, there's no page refreshes while being used. Hence, the classes are not being applied as I want it to. How do I make it so the body, container, and nav elements' classes are being bound as I want?
Edit: I have tried to call viewhelper.accountVisualTreatment() in viewmodels of the landing page of the different account types but to not avail. Still needs page refresh.
Edit: I ugly-ly fixed it by applying a css binding to the container div in my shell.html
<div class="container" data-bind="css: viewhelper.accountVisualTreatment()"> ... </div>
Because you want to do this in the body (so globally). So my assumption is you want to update the css classes as soon the accountinfo changes. You could create a singleton observable with the accountinfo. When it changes you just subscribe to the observable.
The only problem is with elements which aren't in the dom. (like between loading pages). Because you use Durandal you have different hooks to change the dom with jQuery.
To do this in the viewmodels you should use attached() or compositionComplete() see: http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html. This is the callback you get from durandal when the dom is completly attached to the viewmodel and all the observables.
I would say you don't want to do this in every viewmodel. So I would suggest to hook on the router:navigation:composition-complete event on the router, see: http://durandaljs.com/documentation/api.html#class/Router/event/router:navigation:composition-complete. This will make sure you will get the callback for every new hash navigation.
You can register to the global event in the data-main where the app is started:
app.on('router:navigation:composition-complete').then(viewhelper.accountVisualTreatment());
I hope this helped.

Categories

Resources