I have a set of cascading dropdowns where the value in one defines the options for the next. For various reasons, it makes more sense for my application to represent this in the url path than url args. So I want urls like
mysite/#/Supermarkets/Produce/Apples
Where Supermarkets is not a dropdown but Produce and Apples are. The trick is that I don't know ahead of time that the path hash has three parts; the first part always exists, but the fact that there are two (or three or one or zero) dropdowns is only known after the first /Supermarkets/ part of the path resolves.
I'm not sure how to set this up with sammy. Does it even support this scenario?
You can make a route with parameters. The parameters then are accessed by the params object. I tried the code below which worked for me:
var app = $.sammy('header', function () {
//this allows us to use the sammy templates
this.use('Template');
//load header template
this.get('/Supermarkets/:type/:item', function (context) {
alert(this.params['type']);
alert(this.params['item']);
});
});
$(document).ready(function (){
app.run();
});
So now if the url is /Supermarkets/Produce/Apples I will get two alerts one for the type parameter which is Produce and one for the item parameter which returns Apples.
Related
After years of testing one global DOM for end-to-end testing, I'm finding it very difficult, if not impossible, to test web components that use slots. Before I explain the problem, I want to say that I cannot change the generated markup to improve things as they are.
<wc-1 attributes-etc="">
<wc-2 attributes-etc="">
<slot>
<wc-3 attributes-etc="">
<slot>
...eventually get to an input...
<input type="text" name="firstName" />
There are a buttload of nested web components from some kind of form builder, and there are also plenty of slots used. The web components have attributes but the slots never do, so I use the web component name for querying.
document.querSelector('wc-1')
.shadowRoot.querySelector('wc-2')
.shadowRoot.querySelector('slot')
// Yields <slot>...</slot>
All fine to this point and Cypress has a .shadow() command I used, but I'm testing with just devtools here to see all the properties the slot has.
document.querSelector('wc-1')
.shadowRoot.querySelector('wc-2')
.shadowRoot.querySelector('slot')
.shadowRoot
// Yields "null".
// I don't know how to get to the .lightDOM? of wc-2?
Any property I try ends up being null or having 0 elements in the returned value. Using other front-end tools and the global DOM, I can always cy.get('div[data-testid="the-nested-element-i-want"]').type('important words') in one command.
So my main question is: How do people test these things once web components start piling up? Or don't do this and just test the web components in isolation/unit tests since it's so hard to query the nested shadow DOMs?
The main goal is to eventually get to a form input to cy.get('input[name"firstName"]').type('John'). Can someone give me the chained docuement.querySelector() command to get to <wc-3> in my example?
The answer involves assignedNodes(): https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes
The assignedNodes() property of the HTMLSlotElement interface returns a sequence of the nodes assigned to this slot...
It made no difference for me to use that vs. assignedElements(). So, all you have to do is use that method once you've queried down to the slot you need. For my example, the answer is:
const wc-3 = document.querySelector('wc-1').shadowRoot
.querySelector('wc-2').shadowRoot
.querySelector('slot').assignedNodes()
.map((el) => el.shadowRoot)[0]
And then you can keep going down the chain...I know I only have one un-named slot, so that's why I grab it from the returned .map().
Props to this Q&A for pointing me on the right direction: Web components: How to work with children?
There will be no DOM content in your <slot>, as there is no DOM content moved to slots.
lightDOM content is reflected in slots, but remains invisible! in lightDOM.
(that is why you also style slotted content in lightDOM)
From the docs:
𝘾𝙤𝙣𝙘𝙚𝙥𝙩𝙪𝙖𝙡𝙡𝙮, 𝙙𝙞𝙨𝙩𝙧𝙞𝙗𝙪𝙩𝙚𝙙 𝙣𝙤𝙙𝙚𝙨 𝙘𝙖𝙣 𝙨𝙚𝙚𝙢 𝙖 𝙗𝙞𝙩 𝙗𝙞𝙯𝙖𝙧𝙧𝙚.
𝙎𝙡𝙤𝙩𝙨 𝙙𝙤𝙣'𝙩 𝙥𝙝𝙮𝙨𝙞𝙘𝙖𝙡𝙡𝙮 𝙢𝙤𝙫𝙚 𝘿𝙊𝙈; 𝙩𝙝𝙚𝙮 𝙧𝙚𝙣𝙙𝙚𝙧 𝙞𝙩 𝙖𝙩 𝙖𝙣𝙤𝙩𝙝𝙚𝙧 𝙡𝙤𝙘𝙖𝙩𝙞𝙤𝙣 𝙞𝙣𝙨𝙞𝙙𝙚 𝙩𝙝𝙚 𝙨𝙝𝙖𝙙𝙤𝙬 𝘿𝙊𝙈.
So to test if something is "in" a slot
you need to check for slot=? attributes on lightDOM elements
and double check if that <slot name=? > actually exists in shadowDOM
Or vice versa
Or hook into the slotchange Event, but that is not Testing
pseudo code:
for the vice-versa approach; can contain errors.. its pseudo code..
function processDOMnode( node ){
if (node.shadowRoot){
// query shadowDOM
let slotnames = [...node.shadowRoot.querySelectorAll("slot")].map(s=>s.name);
// query lightDOM
slotnames.forEach( name =>{
let content = node.querySelectorAll(`[slot="${name}"]`);
console.log( "slot:" , name , "content:" , content );
});
// maybe do something with slotnames in lightDOM that do NOT exist in shadowDOM
// dive deeper
this.shadowRooot.children.forEach(shadownode => processDOMnode(shadownode));
}
}
As we are switching to sitecore from a java platform, I have some questions regarding parameters. This is easily done in jsp but I can't find a solution for sitecore. (the implementation is done by external partners)
In my cshtml, I include other elements via the placeholder-function: #Html.Sitecore().Placeholder("Placholdername")
The elements included as a placeholder also can include other elements as placeholders.
So the question is: can I pass some parameters along with those placeholders?
Like my parent element has a certain variable set, for example "i = 5", and I want to pass this variable to the elements included as placeholders and also pass it to the elements included as placeholders in the placeholders?
Something like:
A includes B as a placeholder and passes "i=5" and B includes C as a placeholder and passes "i=5" so in C the value of "i" is "5" because "i" was set to "5" in A.
On out current coremedia platform I can simply use something like:
<cm:include self="${self}" view="asdf">
<cm:param name="i" value="5"/>
</cm:include>
Edit:
What I want to achieve is the following: For example I have the following structure: the page frame cshtml with a variable i=5, which then includes a grid as a placeholder and passes the variable to the grid. The grid then does some math like i=i+5 (which should equal 10) and then includes a teaser as a placeholder and passes the new i=10 to the teaser and so on..
You should set the value of i in the model. Then all the different views or partial views should inherit the same model.
You can assign parameters to renderings, but not to placeholders. Placeholders should be seen as a hole. You can dynamically put stuff in it but you can't assign parameters to it. There's a discussion of that here: https://sitecore.stackexchange.com/questions/2098/add-rendering-parameters-to-placeholder/2101
I can think of at least two approaches to solve your problem:
Although your question is a bit lacking in detail, it kind of looks like you don't necessarily need placeholders because you already know what you're going to render inside those spaces. If that's the case, then you can statically bind your MVC views instead of using placeholders. This is not a common practice, but it is mentioned in Sitecore's latest training material and elsewhere as a way to optimize when you don't need the flexibility of placeholders. This is normally done using the #Html.Sitecore.Rendering helper.
You could use a global variable of sorts by leveraging the good old HttpContext.Items collection.
You can add parameters to the ViewData dictionary, in a controller action method:
public ActionResult MyPage()
{
ContextService.Get().GetCurrent().ViewData.Add("MyKey", "MyValue");
return View();
}
Then, any View Renderings can access the parameter from ViewData:
#{
var value = ViewData["MyKey"].Value;
}
Or if you're using Controller Renderings, add some code to fetch the ViewData from parent pages, and add it to the current ViewData instance:
public ActionResult ChildRendering()
{
// Get any ViewData previously added to this ViewContext
var contextViewData = ContextService.Get().GetCurrent().ViewData;
contextViewData.ToList().ForEach(x => ViewData.Add(x.Key, x.Value));
return View();
}
Your ViewData contents will now be available in view files.
This is discussed in a little more detail here: https://chrisperks.co/2017/03/06/a-workaround-for-missing-viewdata-in-sitecore-mvc/
I'd like for changes in the URL to drive my application, and for changes in the application to change the URL, but not actually change state.
I have a route like this. The country/city example is a bit contrived, hopefully that doesn't confuse things. The relationship in the real application is somewhat hierarchical. Child views don't seem a fit though because there's no need for nested views.
$stateProvider.state( 'viewMap', {
url: '/viewMap/:country/:city',
templateUrl: 'pages/viewMap/viewMap.html',
controller: 'ViewMapController'
};
In ViewMapController, I can construct the page based on $stateParams.country and .city. As these values change, my application reacts and I want the url to stay in sync. I don't want to reload the whole page, however. I just want to update the url and push a history state on to the stack.
I understand I could manually construct a string:
updateUrl = function() {
window.location.hash = '#/viewMap/'+ $stateParams.country +'/'+ $stateParams.city
}
This feels fragile, as the way I build the string is separate from the way the framework parses it. I would prefer for the framework to build me a string based on the current params, but $state.href('.') describes the current route, which doesn't include $stateParams that haven't yet been activated/navigated to.
I've also looked at reloadOnSearch, but I think it only applies to query params.
Is there a better way to model this? It feels like I'm fighting the framework over something simple.
You can pass state params to $state.href function to get the complete URL
$state.href('.', $stateParams)
To generate arbitrary urls you can pass non-current params and/or configuration:
$state.href('.', {country:'usa',city:'sf'}, {absolute:true})
I'm loading data using jQuery (AJAX), which is then being loaded into a table (so this takes place after page load).
In each table row there is a 'select' link allowing users to select a row from the table. I then need to grab the information in this row and put it into a form further down the page.
$('#selection_table').on('click', '.select_link', function() {
$('#booking_address').text = $(this).closest('.address').text();
$('#booking_rate').text = $(this).closest('.rate').val();
});
As I understand it, the 'closest' function traverses up the DOM tree so since my link is in the last cell of each row, it should get the elements 'address' and 'rate from the previous row (the classes are assigned to the correct cells).
I've tried debugging myself using quick and dirty 'alert($(this).closest(etc...' in many variations, but nothing seems to work.
Do I need to do something differently to target data that was loaded after the original page load? where am I going wrong?
You are making wrong assumptions about .closest() and how .text() works. Please make a habit of studying the documentation when in doubt, it gives clear descriptions and examples on how to use jQuery's features.
.closest() will traverse the parents of the given element, trying to match the selector you have provided it. If your .select_link is not "inside" .address, your code will not work.
Also, .text() is a method, not a property (in the semantical way, because methods are in fact properties in Javascript). x.text = 1; simply overrides the method on this element, which is not a good idea, you want to invoke the method: x.text(1);.
Something along these lines might work:
var t = $(this).closest('tr').find('.address').text();
$('#booking_address').text(t);
If #booking_address is a form element, use .val() on it instead.
If it does not work, please provide the HTML structure you are using (edit your question, use jsFiddle or a similar service) and I will help you. When asking questions like this, it is a good habit anyways to provide the relevant HTML structure.
You can try using parent() and find() functions and locate the data directly, the amount of parent() and find() methods depends on your HTML.
Ex. to get previous row data that would be
$('#selection_table').on('click', '.select_link', function(){
$('#booking_address').text = $(this).parent().parent().prev().find('.address').text();
});
Where parent stands for parent element (tr), then prev() as previous row and find finds the element.
Is there a demo of the code somewhere? Check when are you calling the code. It should be after the 'success' of AJAX call.
Playing with the new(ish) url rewriting functionality for web forms, but I'm running into trouble trying to declare parameters as optional.
Here's the scenario. I've got a search function which accepts two parameters, sku and name. Ideally I'd like the URL for this search function to be /products/search/skuSearchString/nameSearchString. I also have various management pages that need to map to things like /products/management/ or /products/summary/. In other words, the last two parameters in the URL need to be optional - there might be one search string, or two, or none.
This is how I've declared my virtual URL:
Friend Const _VIRTUALURL As String = "products/{action}/{sku}/{*product}"
And added the following defaults:
Me.Defaults = New Web.Routing.RouteValueDictionary(New With {.sku = "/"})
Me.Defaults = New Web.Routing.RouteValueDictionary(New With {.product = "/"})
I have two problems with this setup. The most pressing is that the url seems to expect an sku parameter. So, /products/summary/ cannot be found but /products/summary/anyTextAtAll/ maps to the correct page. You get the same result whether the defaults are set to "/" or "". How do I ensure both sku and product parameters are optional?
The second is more a matter of interest. Ideally, I'd like the url to be able to tell whether or not it's got a product search string or a url search string. The obvious way to do this is to make one or the other default to a value I can just pick up and ignore, but is there a neater way of handling it?
I'm not sure I entirely understood the question, but I have some comments about what you've shown so far:
The manner in which you're setting defaults seems incorrect. You're first setting a default value dictionary with a value for "sku". You're then replacing the default value dictionary with a value for "product".
A default value of "/" is unlikely to be what you want. In this case it sounds like you want a default value of just "" (empty string).
Try something like:
Me.Defaults = New Web.Routing.RouteValueDictionary(New With {
.sku = "",
.product = "" })
My VB skills are rather weak, so the syntax I showed might not be exactly right.
I think that if you change both of these then you should be good to go.