
Carson Sievert || Using tagQuery() from {htmltools} to modify HTML snippets in R || RStudio
00:00 Introduction 00:45 Motivating example - enabling front-facing camera as an input for fileInput() 01:55 Breaking down the return value of fileInput() 04:16 Design philosophy of fileInput() 07:27 tagAppendAttributes() overview 11:05 tagQuery() basics 12:00 Quick overview of the htmltools package 13:18 How tagQuery() is used to append attributes 20:54 How tagQuery() is used to append children 23:45 Using tagQuery() on an actionButton() Learn more about tagQuery here: https://rstudio.github.io/htmltools/articles/tagQuery.html Read up on tagAppendAttributes() here: https://shiny.rstudio.com/reference/shiny/latest/tagAppendAttributes.html And learn more about the htmltools package here: https://rstudio.github.io/htmltools/index.html Got questions? The RStudio Community site is a great place to get assistance: https://community.rstudio.com/ Content: Developer (@winston_chang) Animation, design, and editing: Jesse Mostipak (@kierisi)
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
We'll just start out with this basic thing where I can use file input to upload anything I'd like, which is cool. But this pull request was asking if we could add a capture attribute to file input to enable, what was it again Barrett, to basically enable the front facing camera? Yeah to enable the camera input. There is already an accepts argument to file input, there was not the capture attribute.
Right, so file input has a handful of arguments and accept allows us to upload various different types of files, is that roughly correct? So we could say accept is image slash star so that you can only upload images, you wouldn't be able to upload a csv or something like that. I see, so I say accept video slash star or image slash star. Now if I run this again you can do your, I can upload my pngs but I can't upload anything else, yeah like pdfs.
Breaking down fileInput()
Cool, okay. Right, so and if we actually look at the return value of file input, it's a pretty big complicated HTML tag that's being returned. So there's like an outer div which is the actual Shiny input container that you'll see around a lot of Shiny input functions and that's just kind of a class to let the Shiny JavaScript know like hey this is an input container that has input value that we should send back to the server that you could access in the server code. But you know there's all this sort of complicated markup and then finally at the end of the day there's an input tag that is the actual HTML element that enables the file uploading.
So essentially we have this accept argument on file input that gets routed to this accept attribute on the input tag. So we have this kind of general problem with a lot of like Shiny input functions where we're basically wrapping HTML tags and in some cases you know you might want to customize certain attributes on particular tags on this return value and we might not sort of out of the box provide you the tools to be able to customize like a very particular attribute on a very particular tag in this return value. So this person was kind of requesting like hey there's this capture attribute on an HTML input tag that allows you to enable the front-facing camera on a mobile phone. So this would allow him to kind of do like a facial recognition thing or allow somebody to like capture a video of themselves and then upload it to a Shiny app.
Design philosophy of fileInput()
And we've been working hard on tools to basically allow you to more generally take the return value of something like file input and target a very particular say HTML elements like the input element within this return structure and do something like append an attribute which is effectively what this user is asking for is hey could you add an argument to file input to basically customize the capture attribute on this input tag. So you know we're trying to avoid going down this sort of rabbit hole of trying to cover every HTML attribute on every sort of Shiny UI function you could imagine and providing better tools for doing this sort of thing where this user could instead of just having the file input here basically basically do something like this where
instead of having this is the return value where I have this input where it's a type text class form control placeholder read only. And the input above it as well. Yes this is the actual input that's relevant. Good pointing that out.
tagAppendAttributes() overview
We can use this tag append attributes which effectively does some CSS selector magic to say within this HTML structure that's being returned by this first argument here find the first input tag so it'll go in and find this input tag here and append an attribute of capture user. So that's adding this part here. We could even do because it selected actually both input tags because there's no distinguishing marker we could do id foo on the CSS. Right right right. So on the CSS selector this when I just type out input it's basically going to look for a tag name of input. That's just kind of how CSS selectors work. But there's some special characters that you can add to CSS selectors where I could tack on a hashtag or pound sign and then an id name. So as Barrett pointed out I could do pound sign foo and that would make sure that instead of the selector matching both input tags notice how the capture user is only being added to this input with an id of foo instead of this one as well.
So yeah with tag append attributes this is this is actually a fairly recent feature that we've added to tag append attributes. In the past you've had to kind of maybe some of you have already done something like this where if you actually look at the structure like the R representation of this HTML it's a big mess of complicated list structures that you get back. And there's a very good reason why we do this. It's you know by having like an actual formal structure to these HTML tags it allows us to compose them in interesting ways and do sort of magical things like this tag append attributes. But this is not something you want to work directly with. So like way early on in HTML tools you might have you know had to do something like x dollar let me reach into the children of this top level div tag and then look at what's in there. It's a list of three where the first list is a label the second list is a div that holds the input tag. So let me actually reach into the second child and I actually want to get into the children of this div. So now I'd have to do this. And now I'm getting closer to getting to this actual input that I want. But now I need to reach into this first item get into the children.
So as you can see here this is not something you want to actually we've part of what inspired some of this work is we've seen a lot of code out in the ecosystem doing this kind of stuff to do custom UI work. And we wanted to make it a lot less confusing how to do something like this and also make your code a lot less brittle to breaking changes that we make to kind of the HTML that we're returning in these functions. So I've had to go out and actually you know fix cases like this because we wanted to slightly tweak the actual HTML that we're returning where this is like making very strong assumptions about the relative ordering of these tags and you know working with an API that's like not super public and something we don't really want to encourage you to actually be doing. So it's a lot better to like actually reach in and get that subset of HTML that you're looking for and modify it using something like this. Yeah like placing a needle within the haystack in a very specific location versus rummaging through and going I hope and then leaving it.
Yeah like placing a needle within the haystack in a very specific location versus rummaging through and going I hope and then leaving it.
And so actually what powers this CSS selector argument and the ability to kind of like subset down find a particular subset of this HTML and then modify it is powered underneath the hood by sort of a lower level tool that you can if you're kind of a power user and need to do something a little bit more sophisticated than something like tag append attributes would provide. We also have this API called tag query.
tagQuery() basics
And if you've ever used jQuery in JavaScript this API will look very similar to what you already do with jQuery. So we're not really inventing a new wheel here we're just kind of borrowing some of the best ideas of what makes jQuery or what made jQuery such a great project. And you can find this article here at rstudio.github.io slash htmltools. This is a fairly new website we only have one article as of now that gets into tag query but hopefully we'll get some more documentation surrounding htmltools.
Quick overview of the htmltools package
And if you're kind of confused like if you haven't really heard of htmltools before htmltools provides kind of the HTML foundation for Shiny. So when something like file input returns all of this HTML structuring all of that all of those data structures and that writing of HTML logic is all done by this sort of lower level htmltools package. And you might have never had to do something like library htmltools before even if you've written custom HTML because Shiny re-exports functions from this package. So you might have written like library Shiny and then like create a custom HTML div or something like that because you get those functions when you load Shiny. But if you load htmltools you'll get some other functions like tag query, tag append attributes, some other functions for doing like lower level HTML tool thing or HTML things that you know not a whole lot of Shiny apps will be doing.
How tagQuery() is used to append attributes
So with tag query the main idea is that you can pass it either a tag object like a div or you know equivalently something like the return value of file input. I could pass directly into tag query. And there's kind of two main components to this return structure. One is this it's actually a class with methods. So this is showing you if I called the all tags class I basically get the return value that I fed into this tag query. So it might not be clear why it's set up this way right now but eventually we're going to like modify the input value essentially in this tag query object.
And then the other method is this selected tags method which will allow me to do something like if I want to find all the input tags essentially what we had before with CSS selector equal to input. The equivalent way of doing this with tag query is to say dollar find input. And that is going it's not going to update the all tags output of this tag query object but it will essentially subset down the selection from being you know by default the input that I gave it. It will update the selected tags to both of the input tags. So now I could do something like chain a call to selected tags and now I have a list of length two containing both of the input tags that I might want to do something with. And I don't think I can just kind of on a whim explain better what tag query actually does. What tag query actually provides then sort of this vignette. So I do recommend reading through this. But kind of the first portion of this is all about this like find method and similar methods like children where find will find it'll traverse essentially all of the descendants of the top level tag and find all occurrences of HTML tags that match the CSS selector that you've given it.
So that's why I was able to use find here and it traverses everything and it reaches into like several descendants down into this input tag here. But if you wanted to do something like for some reason only consider the direct children which would be this label div and this other div. If I search for children there's going to be no selection associated with that that matches the input right. But if I wanted to like find a label tag there is a direct child label tag here so that will match on this label.
So in the case of an action button like it's a simple like one single button tag which allows us to kind of design this function interface as routing all additional arguments that you might want to provide to this button tag. So that allows you to do things like my action button with an ID of foo a label here and that creates this button HTML with an ID a type and some default classes to give it like default bootstrap styling. But say I want to add like a custom inline style of like I want to increase the font size to basically two units of line height. You can do something like that with action button because it basically is like doing the lower level tags dollar button and providing a style to that directly. Since we sort of take the dot dot dot and route it directly to the tags button it's essentially the same interface. So in the case of action button it's a very thin wrapper where you don't really need this sort of ability to you know take the return value and then modify attributes. You can just do that in one shot directly when calling action button.
And you can you know since these again is just kind of taking any sort of input value and feeding it into button. Yeah if I wanted to I could pass another tag object into that and like I don't know create I don't know just an empty div. Oh actually that's for icon. I think I'd have to explicitly say icon is no what is that.
Yeah so that is kind of an unfortunate thing about the way this is set up. The way this is set up is you would have to make sure like what was happening here is this tags dollar div. Instead of going into the dot dot dot here since I hadn't specified an icon it was treating it like the third argument here. But if I wanted to like take a tag object and have it run through this dot dot dot I could you know fully specify those and make sure that it's going to the dot dot dot there and then it shows up in my HTML. Again like the tag query append attributes CSS selector stuff isn't super useful for this but it's more useful in a case where you have like a lot of like a big complicated HTML tag structure and you need to get at a particular subset of that. We did basically just cover you know with the tag and tag append attributes you have the ability to find subsets with the CSS selector and then ability to append attributes.
How tagQuery() is used to append children
But there's more that you can do than just appending attributes. You can also do things like append children. So and you'll notice these other this other append function also has a CSS selector argument. So you could do a similar thing where you know again here's my file input if I wanted to if I wanted to say insert another div within this input group I could search for a class of input group and append a child tag of just a div and I'll just give it a class of my custom class and I should load htmltools. Oh thank you. And now I actually get this div of class my custom class inserted by default I guess in as like the last child within this input group tag.
So kind of like there's basically two main ways you might want to modify the HTML either like mess with the attributes on the tag or add or remove tags. So if it's as simple as wanting to like add an attribute or modify an attribute or append a single child like this you can use these sort of higher level tag append childs or append attributes. But as some of these examples get into you can use this tag query to do more sophisticated stuff. So for example if I wanted to add a child to a div to do more sophisticated stuff and let me try to find an example of good to like remove class add class with like button success instead of button default.
Using tagQuery() on an actionButton()
Right yeah so let's try out tag query with this action button. So by default this button tag is going to be selected and as Barrett was saying let me actually save this as an object called btn. Then when I type dollar I'll see all of the available methods on this tag query class. So we've already seen stuff like find and children. There's also siblings parent and parents and this kind of helps you traverse that tree where say you like went to a particular subset but then you actually want to go back to a parent or find a sibling tag. You can use those methods but he was mentioning maybe I want to for some reason take off this btn default class. I could do something like remove this btn default class and this will actually modify the btn object and we do that for performance reasons. That's why this is kind of like even like a lower level developer kind of more developer facing tool than this API where it's not modifying the input object here. It's giving you basically a copy of that input value as its return value whereas using tag query more directly like this actually will modify the input tags that you give it.
So yeah so now I've removed btn defaults and now I could add a class of let's say btn success and now it's appended to the end here. And maybe to make this a little bit more motivating I'll just show you what this something like this would actually do where that's interesting. I thought by default actually it was going to come up as a blue color but that is only going to come up as a blue color if I add a class of btn primary and that is a bootstrap specific thing where again this is kind of like a hard to discover sort of feature where shiny comes with bootstrap and bootstrap has these sort of semantic css classes where this btn and btn defaults like gives that default sort of white coloring with a border around it based on bootstrap's defaults but it's not actually based on bootstrap's defaults but if you add an additional btn primary class that will give it this sort of blue background and border color that makes it kind of pop out.
So once you're kind of aware of those kind of things and like more familiar with how like bootstrap works or if you're using some other framework with shiny knowing about these kind of tricks to like add or remove classes from the return value will help you kind of be able to leverage kind of the scaffolding or the high level like widgets that shiny provides to you but also be able to do like some more custom theming things with it.

