Is it possible to access elements within a Shadow DOM using Selenium/Chrome webdriver?
Using the normal element search methods doesn't work, as is to be expected. I've seen references to the switchToSubTree spec on w3c, but couldn't locate any actual docs, examples, etc.
Anyone had success with this?
The accepted answer is no longer valid and some of the other answers have some drawbacks or are not practical (the /deep/ selector doesn't work and is deprecated, document.querySelector('').shadowRoot works only with the first shadow element when shadow elements are nested), sometimes the shadow root elements are nested and the second shadow root is not visible in document root, but is available in its parent accessed shadow root. I think is better to use the selenium selectors and inject the script just to take the shadow root:
def expand_shadow_element(element):
shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
return shadow_root
outer = expand_shadow_element(driver.find_element_by_css_selector("#test_button"))
inner = outer.find_element_by_id("inner_button")
inner.click()
To put this into perspective I just added a testable example with Chrome's download page, clicking the search button needs open 3 nested shadow root elements:
import selenium
from selenium import webdriver
driver = webdriver.Chrome()
def expand_shadow_element(element):
shadow_root = driver.execute_script('return arguments[0].shadowRoot', element)
return shadow_root
driver.get("chrome://downloads")
root1 = driver.find_element_by_tag_name('downloads-manager')
shadow_root1 = expand_shadow_element(root1)
root2 = shadow_root1.find_element_by_css_selector('downloads-toolbar')
shadow_root2 = expand_shadow_element(root2)
root3 = shadow_root2.find_element_by_css_selector('cr-search-field')
shadow_root3 = expand_shadow_element(root3)
search_button = shadow_root3.find_element_by_css_selector("#search-button")
search_button.click()
Doing the same approach suggested in the other answers has the drawback that it hard-codes the queries, is less readable and you cannot use the intermediary selections for other actions:
search_button = driver.execute_script('return document.querySelector("downloads-manager").shadowRoot.querySelector("downloads-toolbar").shadowRoot.querySelector("cr-search-field").shadowRoot.querySelector("#search-button")')
search_button.click()
It should also be noted that the Selenium binary Chrome driver now supports Shadow DOM (since Jan 28, 2015) : http://chromedriver.storage.googleapis.com/2.14/notes.txt
Unfortunately it looks like the webdriver spec does not support this yet.
My snooping uncovered :
http://www.w3.org/TR/webdriver/#switching-to-hosted-shadow-doms
https://groups.google.com/forum/#!msg/selenium-developers/Dad2KZsXNKo/YXH0e6eSHdAJ
I am using C# and Selenium and managed to find an element inside a nestled shadow DOM using java script.
This is my html tree:
html tree
I want the url on the last line and to get it I first select the "downloads-manager" tag and then the first shadow root right below it.
Once inside the shadow root I want to find the element closest to the next shadow root. That element is "downloads-item". With that selected I can enter the second shadow root. From there I select the img item containing the url by id = "file-icon". At last I can get the attribute "src" which contains the url I am seeking.
The two lines of C# code that does the trick:
IJavaScriptExecutor jse2 = (IJavaScriptExecutor)_driver;
var pdfUrl = jse2.ExecuteScript("return document.querySelector('downloads-manager').shadowRoot.querySelector('downloads-item').shadowRoot.getElementById('file-icon').getAttribute('src')");
Normally you'd do this:
element = webdriver.find_element_by_css_selector(
'my-custom-element /deep/ .this-is-inside-my-custom-element')
And hopefully that'll continue to work.
However, note that /deep/ and ::shadow are deprecated (and not implemented in browsers other than Opera and Chrome). There's much talk about allowing them in the static profile. Meaning, querying for them will work, but not styling.
If don't want to rely on /deep/ or ::shadow because their futures are a bit uncertain, or because you want to work better cross-browser or because you hate deprecation warnings, rejoice as there's another way:
# Get the shadowRoot of the element you want to intrude in on,
# and then use that as your root selector.
shadow_root = webdriver.execute_script('''
return document.querySelector(
'my-custom-element').shadowRoot;
''')
element = shadow_root.find_element_by_css_selector(
'.this-is-inside-my-custom-element')
More about this:
https://github.com/w3c/webcomponents/issues/78
https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/68qSZM5QMRQ/pT2YCqZSomAJ
I found a much easier way to get the elements from Shadow Dom.
I am taking the same example given above, for search icon of Chrome Download Page.
IWebDriver driver;
public IWebElement getUIObject(params By[] shadowRoots)
{
IWebElement currentElement = null;
IWebElement parentElement = null;
int i = 0;
foreach (var item in shadowRoots)
{
if (parentElement == null)
{
currentElement = driver.FindElement(item);
}
else
{
currentElement = parentElement.FindElement(item);
}
if(i !=(shadowRoots.Length-1))
{
parentElement = expandRootElement(currentElement);
}
i++;
}
return currentElement;
}
public IWebElement expandRootElement(IWebElement element)
{
IWebElement rootElement = (IWebElement)((IJavaScriptExecutor)driver)
.ExecuteScript("return arguments[0].shadowRoot", element);
return rootElement;
}
Google Chrome Download Page
Now as shown in image we have to expand three shadow root elements in order to get our search icon.
To to click on icon all we need to do is :-
[TestMethod]
public void test()
{
IWebElement searchButton= getUIObject(By.CssSelector("downloads-manager"),By.CssSelector("downloads-toolbar"),By.Id("search-input"),By.Id("search-buton"));
searchButton.Click();
}
So just one line will give you your Web Element, just need to make sure you pass first shadow root element as first argument of the function "getUIObject" second shadow root element as second argument of the function and so on, finally last argument for the function will be the identifier for your actual element (for this case its 'search-button')
Until Selenium supports shadow DOM out of the box, you can try the following workaround in Java. Create a class that extends By class:
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.internal.FindsByCssSelector;
import java.io.Serializable;
import java.util.List;
public class ByShadow {
public static By css(String selector) {
return new ByShadowCss(selector);
}
public static class ByShadowCss extends By implements Serializable {
private static final long serialVersionUID = -1230258723099459239L;
private final String cssSelector;
public ByShadowCss(String cssSelector) {
if (cssSelector == null) {
throw new IllegalArgumentException("Cannot find elements when the selector is null");
}
this.cssSelector = cssSelector;
}
#Override
public WebElement findElement(SearchContext context) {
if (context instanceof FindsByCssSelector) {
JavascriptExecutor jsExecutor;
if (context instanceof JavascriptExecutor) {
jsExecutor = (JavascriptExecutor) context;
} else {
jsExecutor = (JavascriptExecutor) ((WrapsDriver) context).getWrappedDriver();
}
String[] subSelectors = cssSelector.split(">>>");
FindsByCssSelector currentContext = (FindsByCssSelector) context;
WebElement result = null;
for (String subSelector : subSelectors) {
result = currentContext.findElementByCssSelector(subSelector);
currentContext = (FindsByCssSelector) jsExecutor.executeScript("return arguments[0].shadowRoot", result);
}
return result;
}
throw new WebDriverException(
"Driver does not support finding an element by selector: " + cssSelector);
}
#Override
public List<WebElement> findElements(SearchContext context) {
if (context instanceof FindsByCssSelector) {
JavascriptExecutor jsExecutor;
if (context instanceof JavascriptExecutor) {
jsExecutor = (JavascriptExecutor) context;
} else {
jsExecutor = (JavascriptExecutor) ((WrapsDriver) context).getWrappedDriver();
}
String[] subSelectors = cssSelector.split(">>>");
FindsByCssSelector currentContext = (FindsByCssSelector) context;
for (int i = 0; i < subSelectors.length - 1; i++) {
WebElement nextRoot = currentContext.findElementByCssSelector(subSelectors[i]);
currentContext = (FindsByCssSelector) jsExecutor.executeScript("return arguments[0].shadowRoot", nextRoot);
}
return currentContext.findElementsByCssSelector(subSelectors[subSelectors.length - 1]);
}
throw new WebDriverException(
"Driver does not support finding elements by selector: " + cssSelector);
}
#Override
public String toString() {
return "By.cssSelector: " + cssSelector;
}
}
}
And you can use it without writing any additional functions or wrappers. This should work with any kind of framework. For example, in pure Selenium code this would look like this:
WebElement searchButton =
driver.findElement(ByShadow.css(
"downloads-manager >>> downloads-toolbar >>> cr-search-field >>> #search-button"));
or if you use Selenide:
SelenideElement searchButton =
$(ByShadow.css("downloads-manager >>> downloads-toolbar >>> cr-search-field >>> #search-button"));
This worked for me (using selenium javascript bindings):
driver.executeScript("return $('body /deep/ <#selector>')")
That returns the element(s) you're looking for.
For getting the filename of the latest downloaded file in Chrome
def get_downloaded_file(self):
filename = self._driver.execute_script("return document.querySelector('downloads-manager').shadowRoot.querySelector('#downloadsList downloads-item').shadowRoot.querySelector('div#content #file-link').text")
return filename
Usage:
driver.get_url('chrome://downloads')
filename = driver.get_downloaded_file()
And for configuring the option for setting the default download directory in selenium for chrome browser, where the corresponding file could be gotten:
..
chrome_options = webdriver.ChromeOptions()
..
prefs = {'download.default_directory': '/desired-path-to-directory'} # unix
chrome_options.add_experimental_option('prefs', prefs)
..
Related
I'm trying to switch to an iframe, but, i must wait for a script to be done before switching.
As i asked here : Selenium switch to the wrong iframe when closing another one and answered it, i can force the switch to the parent frame and use the same idea to switch to a frame with or without any script.
The problem is that forcing the switch isn't a good way to do it. It's similar as using a Thread.sleep(500) because i know that it take approximately 500ms to execute the script.
So i'm trying to wait for jQuery and JavaScript to be done by using :
// Wait for JS
public static ExpectedCondition<Boolean> waitForJSToLoad() {
return new ExpectedCondition<Boolean>() {
#Override
public Boolean apply(final WebDriver driver) {
return ((JavascriptExecutor) driver).executeScript("return document.readyState").toString().equals("complete");
}
};
}
// Wait for jQuery
public static ExpectedCondition<Boolean> waitForAjaxResponse() {
return new ExpectedCondition<Boolean>() {
#Override
public Boolean apply(final WebDriver driver) {
try {
final JavascriptExecutor js = (JavascriptExecutor) driver;
return js.executeScript(
"return((window.jQuery == null )||(window.jQuery != null && jQuery.active === 0))").equals(true);
} catch (final WebDriverException e) {
LOG.info("Error while waiting for jQuery");
return false;
}
}
};
}
But all the return statement are true, so i get a TimeOutException because JS and jQuery is already done when i switch to the frame but it's false.
The script looks like that :
function _optimizeSize() {
var $popin = $('#iframePopin'),
// Some var init
$dialog.transition({top: optimizeTop});
$popin.transition({height: contentHeight + actionBarHeight + popinPaddingHeight});
$iFrame.transition({height: contentHeight});
}
I looked at some similar case, but i couldn't find any answer to my problem.
I'm currently using WebDriver.js to run some automated testing on Browserstack. My goal is to take all the hrefs in a given div, open them and check the title of the page where the link is pointing to. To do that I'm doing the following:
for (var i = 0; i < hrefs.length; i++) {
var href = hrefs[i],
site_name = sites_names[i];
driver.get(href);
driver.getTitle().then(function(title) {
assert.equal(title, site_name);
});
}
My problem is that get and getTitle are both asynchronous methods and thus when I'm calling getTitle the page is already changed and thus the assertion is failing. What is the best pattern/solution for this kind of sitations?
If I am reading your question correctly, your assertions are failing because the assert happens before the page is completely loaded in the browser? I think you require a "wait" logic around your getTitle call as mentioned here
var webdriver = require('selenium-webdriver');
var driver = new webdriver.Builder().withCapabilities(webdriver.Capabilities.chrome()).build();
driver.get('http://www.google.com');
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.name('btnG')).click();
driver.wait(function() {
return driver.getTitle().then(function(title) {
return title === 'webdriver - Google Search';
});
}, 1000);
driver.quit();
If you want to just check the title of all the links present in you page you can change your driver to
WebDriver driver = new HtmlUnitDriver();
This is just a good practice.... not mandatory (to increase the speed of execution). After checking the title you can change your driver back to the original.
Use the below code to iterate valid href one by one and assert it accordingly
List<WebElement> allLinks = driver.findElements(By.tagName("a")); // use tagname according to the need
for (WebElement link : allLinks) {
if (link.getAttribute("href") != null && link.getText().isEmpty()==false) { //again modify the condition here accordingly
driver.get(link.getAttribute("href"));
driver.getTitle().then(function(title) {
assert.equal(title, site_name);
});
}
}
I'm working on an application that uses some client-side templates to render data and most of the javascript template engines return a simple string with the markup and it's up to the developer to insert that string into the DOM.
I googled around and saw a bunch of people suggesting the use of an empty div, setting its innerHTML to the new string and then iterating through the child nodes of that div like so
var parsedTemplate = 'something returned by template engine';
var tempDiv = document.createElement('div'), childNode;
var documentFragment = document.createDocumentFragment;
tempDiv.innerHTML = parsedTemplate;
while ( childNode = tempDiv.firstChild ) {
documentFragment.appendChild(childNode);
}
And TADA, documentFragment now contains the parsed template. However, if my template happens to be a tr, adding the div around it doesn't achieve the expected behaviour, as it adds the contents of the td's inside the row.
Does anybody know of a good way of to solve this? Right now I'm checking the node where the parsed template will be inserted and creating an element from its tag name. I'm not even sure there's another way of doing this.
While searching I came across this discussion on the w3 mailing lists, but there was no useful solution, unfortunately.
You can use a DOMParser as XHTML to avoid the HTML "auto-correction" DOMs like to perform:
var parser = new DOMParser(),
doc = parser.parseFromString('<tr><td>something returned </td><td>by template engine</td></tr>', "text/xml"),
documentFragment = document.createDocumentFragment() ;
documentFragment.appendChild( doc.documentElement );
//fragment populated, now view as HTML to verify fragment contains what's expected:
var temp=document.createElement('div');
temp.appendChild(documentFragment);
console.log(temp.outerHTML);
// shows: "<div><tr><td>something returned </td><td>by template engine</td></tr></div>"
this is contrasted to using naive innerHTML with a temp div:
var temp=document.createElement('div');
temp.innerHTML='<tr><td>something returned </td><td>by template engine</td></tr>';
console.log(temp.outerHTML);
// shows: '<div>something returned by template engine</div>' (bad)
by treating the template as XHTML/XML (making sure it's well-formed), we can bend the normal rules of HTML.
the coverage of DOMParser should correlate with the support for documentFragment, but on some old copies (single-digit-versions) of firefox, you might need to use importNode().
as a re-usable function:
function strToFrag(strHTML){
var temp=document.createElement('template');
if( temp.content ){
temp.innerHTML=strHTML;
return temp.content;
}
var parser = new DOMParser(),
doc = parser.parseFromString(strHTML, "text/xml"),
documentFragment = document.createDocumentFragment() ;
documentFragment.appendChild( doc.documentElement );
return documentFragment;
}
This is one for the future, rather than now, but HTML5 defines a <template> element that will create the fragment for you. You will be able to do:
var parsedTemplate = '<tr><td>xxx</td></tr>';
var tempEL = document.createElement('template');
tempEl.innerHTML = parsedTemplate;
var documentFragment = tempEl.content;
It currently works in Firefox. See here
The ideal approach is to use the <template> tag from HTML5. You can create a template element programmatically, assign the .innerHTML to it and all the parsed elements (even fragments of a table) will be present in the template.content property. This does all the work for you. But, this only exists right now in the latest versions of Firefox and Chrome.
If template support exists, it as simple as this:
function makeDocFragment(htmlString) {
var container = document.createElement("template");
container.innerHTML = htmlString;
return container.content;
}
The return result from this works just like a documentFragment. You can just append it directly and it solves the problem just like a documentFragment would except it has the advantage of supporting .innerHTML assignment and it lets you use partially formed pieces of HTML (solving both problems we need).
But, template support doesn't exist everywhere yet, so you need a fallback approach. The brute force way to handle the fallback is to peek at the beginning of the HTML string and see what type of tab it starts with and create the appropriate container for that type of tag and use that container to assign the HTML to. This is kind of a brute force approach, but it works. This special handling is needed for any type of HTML element that can only legally exist in a particular type of container. I've included a bunch of those types of elements in my code below (though I've not attempted to make the list exhaustive). Here's the code and a working jsFiddle link below. If you use a recent version of Chrome or Firefox, the code will take the path that uses the template object. If some other browser, it will create the appropriate type of container object.
var makeDocFragment = (function() {
// static data in closure so it only has to be parsed once
var specials = {
td: {
parentElement: "table",
starterHTML: "<tbody><tr class='xx_Root_'></tr></tbody>"
},
tr: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
thead: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
caption: {
parentElement: "table",
starterHTML: "<tbody class='xx_Root_'></tbody>"
},
li: {
parentElement: "ul",
},
dd: {
parentElement: "dl",
},
dt: {
parentElement: "dl",
},
optgroup: {
parentElement: "select",
},
option: {
parentElement: "select",
}
};
// feature detect template tag support and use simpler path if so
// testing for the content property is suggested by MDN
var testTemplate = document.createElement("template");
if ("content" in testTemplate) {
return function(htmlString) {
var container = document.createElement("template");
container.innerHTML = htmlString;
return container.content;
}
} else {
return function(htmlString) {
var specialInfo, container, root, tagMatch,
documentFragment;
// can't use template tag, so lets mini-parse the first HTML tag
// to discern if it needs a special container
tagMatch = htmlString.match(/^\s*<([^>\s]+)/);
if (tagMatch) {
specialInfo = specials[tagMatch[1].toLowerCase()];
if (specialInfo) {
container = document.createElement(specialInfo.parentElement);
if (specialInfo.starterHTML) {
container.innerHTML = specialInfo.starterHTML;
}
root = container.querySelector(".xx_Root_");
if (!root) {
root = container;
}
root.innerHTML = htmlString;
}
}
if (!container) {
container = document.createElement("div");
container.innerHTML = htmlString;
root = container;
}
documentFragment = document.createDocumentFragment();
// start at the actual root we want
while (root.firstChild) {
documentFragment.appendChild(root.firstChild);
}
return documentFragment;
}
}
// don't let the feature test template object hang around in closure
testTemplate = null;
})();
// test cases
var frag = makeDocFragment("<tr><td>Three</td><td>Four</td></tr>");
document.getElementById("myTableBody").appendChild(frag);
frag = makeDocFragment("<td>Zero</td><td>Zero</td>");
document.getElementById("emptyRow").appendChild(frag);
frag = makeDocFragment("<li>Two</li><li>Three</li>");
document.getElementById("myUL").appendChild(frag);
frag = makeDocFragment("<option>Second Option</option><option>Third Option</option>");
document.getElementById("mySelect").appendChild(frag);
Working demo with several test cases: http://jsfiddle.net/jfriend00/SycL6/
Use this function
supports IE11
has not to be xml-conform e.g. '<td hidden>test'
function createFragment(html){
var tmpl = document.createElement('template');
tmpl.innerHTML = html;
if (tmpl.content == void 0){ // ie11
var fragment = document.createDocumentFragment();
var isTableEl = /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
tmpl.innerHTML = isTableEl ? '<table>'+html : html;
var els = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
while(els[0]) fragment.appendChild(els[0]);
return fragment;
}
return tmpl.content;
}
The solution from #dandavis will accept only xml-conform content in ie11.
I dont know if there are other tag which must be taken into account?
I'm using Webdriver through JBehave-Web distribution (3.3.4) to test an application and I'm facing something quite strange:
I'm trying to interact with a modalPanel from Richfaces, which gave me a lot of problems because it throws ElementNotVisibleException. I solved it by using javascript:
This is the code in my page object which extends from org.jbehave.web.selenium.WebDriverPage
protected void changeModalPanelInputText(String elementId, String textToEnter){
makeNonLazy();
JavascriptExecutor je = (JavascriptExecutor) webDriver();
String script ="document.getElementById('" + elementId + "').value = '" + textToEnter + "';";
je.executeScript(script);
}
The strange behaviour is that if I execute the test normally, it does nothing, but if I put a breakpoint in the last line (in Eclipse), select the line and execute from Eclipse (Ctrl + U), I can see the changes in the browser.
I checked the JavascriptExecutor and the WebDriver classes to see if there was any kind of buffering, but I couldn't find anything. Any ideas?
EDIT
I found out that putting the thread to sleep for 1 second it makes it work, so it looks some kind of race condition, but cannot find out why...
This is the way it "works", but I'm not very happy about it:
protected void changeModalPanelInputText(String elementId, String textToEnter){
String script ="document.getElementById('" + elementId + "').value = '" + textToEnter + "';";
executeJavascript(script);
}
private void executeJavascript(String script){
makeNonLazy();
JavascriptExecutor je = (JavascriptExecutor) webDriver();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
je.executeScript(script);
}
Putting the wait in any other position doesn't work either...
First idea:
Ensure that target element is initialized and enumerable. See if this returns null:
Object objValue = je.executeScript(
"return document.getElementById('"+elementId+"');");
Since you're using makeNonLazy(), probably just add the target as a WebElement member of your Page Object (assuming Page Factory type of initialization in JBehave).
Second idea:
Explicitly wait for the element to be available before mutating:
/**
* re-usable utility class
*/
public static class ElementAvailable implements Predicate<WebDriver> {
private static String IS_NOT_UNDEFINED =
"return (typeof document.getElementById('%s') != 'undefined');";
private final String elementId;
private ElementAvailable(String elementId) {
this.elementId = elementId;
}
#Override
public boolean apply(WebDriver driver) {
Object objValue = ((JavascriptExecutor)driver).executeScript(
String.format(IS_NOT_UNDEFINED, elementId));
return (objValue instanceof Boolean && ((Boolean)objValue));
}
}
...
protected void changeModalPanelInputText(String elementId, String textToEnter){
makeNonLazy();
// wait at most 3 seconds before throwing an unchecked Exception
long timeout = 3;
(new WebDriverWait(webDriver(), timeout))
.until(new ElementAvailable(elementId));
// element definitely available now
String script = String.format(
"document.getElementById('%s').value = '%s';",
elementId,
textToEnter);
((JavascriptExecutor) webDriver()).executeScript(script);
}
I've been trying to find a way to programmatically add a link to an external CSS file to the <head> tag from markup within the <body> tag in Castle MonoRails and NVelocity view engine. Anyone know how this can be done?
I need to resolve this problem as the page I work on can consist of many "widgets", and each widget would optimally acquire additional assets such as JS and CSS, rather than putting the <link ..> within the body tag and risk rendering problems.
The solutions posted here are not properly working. A better approach is to add your scripts to httpContext.Current.Items
Using same structure as #jishi:
in your view or your viewcomponent, you would be able to invoke something like this:
$Style.Add("/static/style1.css")
and in your layout (head-section):
$Style.Render()
Your helper consists of 2 simple methods using httpcontext to store and retreive the list of files.
public class StyleHelper
{
public static void Add(string file) {
if (string.IsNullOrWhiteSpace(file))
return;
var obj = HttpContext.Current.Items["AssetFiles"] as List<string>; // get the collection which might be empty
if (obj == null || obj.Count == 0) {
IList<string> list = new List<string>();
list.Add(file.ToLower());
HttpContext.Current.Items.Add("AssetFiles", list); // adds your first asset to your collection and returns
return;
}
if (obj.Contains(file.ToLower()))
return; // asset is already added
// new asset ready to be added to your collection
obj.Add(file.ToLower());
HttpContext.Current.Items.Add("AssetFiles", obj);
}
public string Render() {
var obj = HttpContext.Current.Items["AssetFiles"] as List<string>;
if (obj == null || obj.Count == 0)
return ""; // you never added anything to the collection. Nothing more to do.
// not using linq here for the sake of readability:
string res = string.Empty;
foreach (var item in obj) {
res = res + string.Format("<link rel=\"stylesheet\" {0} />", item);
}
return res;
}
}
In your controller (preferably base controller) add this:
[Helper( typeof( StyleHelper ), "Style" )]
public class YourController
Are you talking about adding it to the layout from a view? Because the view is rendered before the layout, meaning that if you create a Helper that would take care of the rendering of style blocks, that would solve your issue.
Meaning, that in your view or your viewcomponent, you would be able to invoke something like this:
$Style.Add("/static/style1.css")
and in your layout (head-section):
$Style.Render()
Here is an example helper (inheriting AbstractHelper is optional):
public class StyleHelper : AbstractHelper
{
private readonly HashedSet<string> sheets = new HashedSet<string>();
public void Add( string styleSheet )
{
Add( styleSheet, "all" );
}
public void Add( string styleSheet, string media )
{
this.sheets.Add( String.Format( "media=\"{0}\" href=\"{1}\"", media, styleSheet ) );
}
public string Render()
{
var str = new StringBuilder();
this.sheets.ToList().ForEach( sheet => str.AppendLine( String.Format( "<link rel=\"stylesheet\" {0} />", sheet ) ) );
return str.ToString();
}
}
AbstractHelper is part of Castle.Monorail.Framework.Helpers. You do not need to inherit that unless you want to utilize the functionality exposed by that abstract implementation. Then, add it to your controller, either your basecontroller or a specific controller:
[Helper( typeof( StyleHelper ), "Style" )]
public class YourController
CaptureFor
I found out there's a component that does exactly what I was looking for here.
In the view:
#capturefor(capturefortest)
Default way of printing. Use this for unique variables, such as title.
#end
#blockcomponent(CaptureFor with "id=capturefortest" "append=before")
This will append before 1.
#end
#blockcomponent(CaptureFor with "id=capturefortest" "append")
This will append after 1.
#end
#blockcomponent(CaptureFor with "id=capturefortest" "append")
This will append after 1 and 3.
#end
#blockcomponent(CaptureFor with "id=capturefortest")
Overrides everything defined before this.
#end
Layout / MasterPage:
$capturefortest
Escaping #:
When putting #, e.g. in a jQuery ID selector, inside a #blockcomponent(CaptureFor), you'll get an error. This can be avoided by setting some globally available variables in AssetFilter and using ${HASH} to print #. Single quotes and quotes are also nice to have:
controllerContext.PropertyBag.Add("HASH", "#");
controllerContext.PropertyBag.Add("Q", '"');
controllerContext.PropertyBag.Add("sq", "'");
Now you can safely do:
#blockcomponent(CaptureFor with "id=capturefortest" "append=before")
<script type="text/javascript">
jQuery('#container').html('Awesomeness!')
</script>
#end
It's possible thanks to Javascript! Here is how I would append style to <head>:
<script type="text/javascript">
var cssNode = document.createElement('link');
cssNode.setAttribute('rel', 'stylesheet');
cssNode.setAttribute('href', 'http://path.to/your/file.css');
cssNode.setAttribute('type', 'text/css');
document.getElementsByTagName('head')[0].appendChild(cssNode);
</script>