Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoped CSS Doesn't work on dynamic content #559

Closed
ramakrishnamundru opened this issue Jan 5, 2017 · 24 comments
Closed

Scoped CSS Doesn't work on dynamic content #559

ramakrishnamundru opened this issue Jan 5, 2017 · 24 comments

Comments

@ramakrishnamundru
Copy link

When using scoped CSS the styles and the elements are given data- attributes. But when content is added Dynamically it won't have those data- attributes. So the styles are not applying to the dynamic content.

I think it's better to add a data- attribute to the root element of the template and add the styles as the children of that attribute instead of adding attributes to individual elements..

@kazupon
Copy link
Member

kazupon commented Jan 7, 2017

Thank you for your filling!

Can you provide the minimum reproduction code please?

@ramakrishnamundru
Copy link
Author

I don't know how to reproduce it in a fiddle but look at this image.
scoped_css

here I am using a jquery plugin lc_switch which adds content to replace default checkbox.

the dynamic content is not getting the data- attributes.

so the scoped css styles are not applied

@ramakrishnamundru
Copy link
Author

I created a repo showing this issue.

https://github.com/ramakrishnamundru/Vue-loader-scoped

just remove scoped for the plugin css and see changes

@kazupon
Copy link
Member

kazupon commented Jan 10, 2017

Thank you for your codes!

scoped CSS with vue-loader & vue core can not give scopeId to the dynamic content with jQuery etc.
This is hacky even though, you can implement with _scopedId options.

src/components/Hello.vue

    mounted: function() {
      $('#switch').lc_switch('ON', 'OFF', this.$options._scopeId);
    }

src/assets/LC-switch/lc_switch.js

$.fn.lc_switch = function(on_text, off_text, scopeId) {
  // ...

  // should be changed dynamic content with scopeId
  // e.g. cod_on_text & ckd_off_txt
  var on_label = (ckd_on_txt) ? '<div ' + scopeId + ' class="lcs_label lcs_label_on">'+ ckd_on_txt +'</div>' : '';
  var off_label = (ckd_off_txt) ? '<div ' + scopeId + ' class="lcs_label lcs_label_off">'+ ckd_off_txt +'</div>' : '';
 // ...

@kazupon kazupon closed this as completed Jan 10, 2017
@ramakrishnamundru
Copy link
Author

Maybe, But in this way I have to edit most of the plugin files as many will create dynamic content because lc_switch is just an example.

Is in't there a way to add scopeId to the root of the component and modify the css as the children of the root scopeId. Like:

<template>
<div data-scopeId>
<content>
</content>
</div>
</template>
 And modify css like:

[data-scopeId] content{ /* somestyles */ }

@LinusBorg
Copy link
Member

LinusBorg commented Jan 10, 2017

Something like this might work (untested):

Vue.directive('scope', {
  bind: updateScopeId,
  componentUpdated: updateScopeId
}

function updateScopeId(el, __, vNode) {
  const vm = vNode.context
  const scopeId = vm & vm.$options._scopeId
  if (scopeId) {
    vm.$nextTick(() => { // wait till DOM was updated
      setScopeRecursively(el, `v-${scopeId}`)
    })
  }
}

function setScopeRecursively(el, scopeString) {
  const nodes = el.children
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (node.dataset[scopeString] !== '') {
      node.dataset[scopeString] = ''
    }
   if (node.children.length > 0) {
     setScopeRecursively(node, scopeString)
    }
  }
}

Usage

<div v-scope>
  <!-- 
    if you add any elements in here with jQuery, 
    the directive will add the scope data attributes for you.
    it will also re-check whenever the component updated, in case Vue changed the DOM
  -->
</div>

Limitations

  • If your jQuery plugin adds any DOM Nodes after the component rendered and without any data change in the component triggering this update, the component will not re-render, and the directive will not know about thjis update. You might have to use vm.$forceUpdate() to make it update.

@ramakrishnamundru
Copy link
Author

@LinusBorg your code didn't work for me. the setScopeRecursively function is never executed.

But I think the way of adding scopeId to the root element and modifying CSS accordingly is better solution. Is there a way you can implement this in vue loader....

@LinusBorg
Copy link
Member

If you want to use a base base class on the root element and nested styled under it, you don't need scope, just use a unique classname.

@ramakrishnamundru
Copy link
Author

ramakrishnamundru commented Jan 11, 2017

@kazupon , @LinusBorg I there a way you can make vue-loader add and remove the style tag for scoped css along with the component i.e style tag added to dom when component created and removed when component destroyed,
Then there will be no such issue even with dynamic content as styles are automatically applied.
If thats not possible then atleast give option to add style tags inside the the template.

@ramakrishnamundru
Copy link
Author

@yyx990803 This is clearly a problem because any thing dynamic should have it's css applied globally. So, It will effect content of other components. So the solution I think of is to move the style tag with scoped attribute into the template, so that it will only appear when its needed. otherwise many jquery plugins css will have to be applied globally, which have a lot of problems.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 17, 2017

So the solution I think of is to move the style tag with scoped attribute into the template, so that it will only appear when its needed.

That CSS is still being applied globally - it would merely reduce the "risk's" probability a little, and at worst create a false sense of security because it works as long as two components with matching CSS are not in the document together at the same - until they are, because your changed something in your app - and now you wonder why CSS is screwed up.

So I still don't see how we could have truly scoped CSS applied by vue-loader for elements that it doesn't know of, because third party libs create them during runtime.

@ramakrishnamundru
Copy link
Author

That CSS is still being applied globally - it would merely reduce the "risk's" probability a little

If the style tag is removed from the dom then its content is also removed. and no styles will be applied to other components.
There is a discussion here:
angular/angular.js#2387

about style tags. I think it is possible to accommodate dynamic elements css by adding the style tag to template like this:

<template>
<root>
<content></content>
<link rel="stylesheet" type="text/css" href="link to scped css bundle">
</root>
</template>

just bundling all the files with scoped attribute and adding them to template will ensure that the css only comes when required and is removed with the component when switching route.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 17, 2017

If the style tag is removed from the dom then its content is also removed.

Of course, I don't deny that. But that CSS is still not scoped to the component, it's still applied to the whole document.

So if you have two component with conflicting CSS rules in the document at the same time, the CSS will be screwed up even with this method. So as I said above, it's a small improvement, but the "securtity" that it seems to bring is very brittle.

The ´discussion you linked to doesn't revolve around scoping issues either, AFAICT,

@ramakrishnamundru
Copy link
Author

OK, Now I understand your concern, and the clear meaning of scope.

Then is there any way you can add another mode like "bound" or something else which shows the css loaded is applied globally, but can also be moved with the template hence separating the confusion between the "security" the scoped css provides , and the "flexibility" the bound to the component css provides.

Because it is not just about third party libraries, but many people create dynamic content and the loss of styling to the content is a clear drawback to the loader.

@LinusBorg
Copy link
Member

LinusBorg commented Jan 17, 2017

Please open a new issue as a featue request with a detailed description of what you want, and why it's useful. This issue was about scoped CSS, we should start clean.

However, as a disclaimer: I don't think that his will have a high priority, because (aside from the fact that it's a very common practice to serve all CSS as one bundle initially) the focus of vue-loader files it to take care of CSS for Vue components, and those can be scoped, so there's no issue.

Its focus is not to support any kind of dynamic styling for any a third-party lib that might come with its own CSS in the component by some third party lib.

Because it is not just about third party libraries, but many people create dynamic content

I don't understand what kind of content you refer to here.

@ramakrishnamundru
Copy link
Author

I don't understand what kind of content you refer to here

I mean content like dynamic chat or task manager, editable tables etc..

And thanks I will file a new issue.

@LinusBorg
Copy link
Member

I mean content like dynamic chat or task manager, editable tables etc..

Well, either those are created with Vue, so you can use scoped CSS (so you have no problem), or they are created with third-party libs - like jQuery plugins. I don't see a third possibility that woudl make sense.

@aboutqx
Copy link

aboutqx commented May 31, 2018

I had the same problem,and just give up using scoped.

@ramakrishnamundru
Copy link
Author

@aboutqx you can use the deep selector as @yyx990803 mentioned here

@andredewaard
Copy link

I have some component that are filled with content from the backend as HTML and will be rendered with v-html. All the tags that are rendered are not getting the data-v-f3f3eg9 attributes and so the scoped styles wont apply on those tags.

@jrsmith17
Copy link

I don't see a third possibility that woudl make sense.

Vue bills itself as something you can transition into. Not having the scoped css apply to dynamic elements breaks that promise. e.g. when I dynamically add elements with d3. Yes, I can eventually transition to d3 + vue templates, but this means a lot more up front work when Vue should just work out of the box.

@Luke-Wilson
Copy link

I tried something similar to @LinusBorg's setScopeRecursively but this was throwing an error whenever the scopeId started with a letter (e.g v-b9231fa123 as opposed to v-123f1123a). This is because the DOMStringMap needs data attributes to be camelCasified in this instance.
Instead of setting the element's data attribute using node.dataset[scopeString] = "" as Linus suggested, I used node.setAttribute('data-${scopeString}', ""). This camelCasifies the string correctly (when the scopeId starts with a letter) and this worked for me.

Below is my version:

export const setScopeRecursively = function(targetEl, scopeId) {
  // This function will recursively travel down from the targetEl and apply the scopeId as a data attribute for CSS scoping.
  // scopeId should be a string (e.g. v-123f1123a)
  try {
    targetEl.setAttribute(`data-${scopeId}`,"")
  } catch(e) {
    console.log(e)
  }

  // Recursively call setScopeRecursively for any children.
  if (targetEl.hasChildNodes()) {
    const list = targetEl.children
    for (var item of list) {
      setScopeRecursively(item, scopeId)
    }
  }
}

@SamuelEarl
Copy link

I am using D3.js inside of Vue.js single file components. I was trying to style some elements that were added to the DOM after the Vue component was created and found that deep selectors work well for this. I wanted to share what I found in the hope that it helps someone.

To apply deep selectors, you start by selecting an element that exists in your <template> tag (e.g., an <svg> element), then you add three greater-than symbols (>>>), then you select the element that you want to style.

For example, if I want to change the colors of the axes (path) and tick marks (line) in a line chart to blue, then I would use this deep selector:

svg >>> path, line {
  stroke: blue;
}

Notice that I did not select any other child elements that were between the <svg> and the <path> or <line> elements. When using deep selectors, you only select the parent element that already exists in your <template> tag and then you select the child element that you want to style. You do not select any other child elements that are between those two elements.

This also means that you need to be careful when nesting styles. Make sure that you use the deep selectors properly and that you do not mix up any nested styles with deep selector styles. If you do mix them up, then your deep selector styles probably won’t work.

It is important to note that the <path> element is two levels below the <svg> element and the <line> element is three levels below the <svg> element. So if you are styling multiple child elements with the same set of style rules, then those child elements do not need to be at the same level in the DOM tree.

If you don’t see your deep selector changes in the browser, then refresh it
After saving your style changes, if you don’t see your deep selector styles applied in the browser, then you might need to manually refresh your browser. It appears that the deep selector style changes don’t always trigger a hot reload.

@kalnode
Copy link

kalnode commented Nov 3, 2021

If it helps anyone, you can have multiple <style>'s in a single file template.

For example...

MyTemplate.vue

<template></template>
<script></script>

<style scoped>
/* Rules scoped specifically to just the template */
</style

<style>
/* Rules that get applied globally, and will work on dynamic components */
</style>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants