I'm building a Durandal SPA that may benefit from a cacheViews setting of true or false, depending on the individuals usage of the app.
This is how I currently have it set in shell.html:
<div class="page-host" data-bind="router: { cacheViews:true }"></div>
And in a child router like in samples/index.html:
<div>
<!--ko router: { cacheViews:false }--><!--/ko-->
</div>
1) Can these be changed at runtime? If so, how?. Does it matter that one is using data-bind and the other is using ko comment style?
2) How granular is this value? Is it per "router" or per "route"? As you can see, I have a parent router and a child router, so there are 2 places in my html code where I can set cacheViews. From my testing, it appears as if they are independent of each other. Can anyone confirm? Can I set this value on individual routes like /#page1, /#page2, /#samples/list, etc?
3) Because the page event life-cycle is different between true/false I need to have some specific logic in my vm depending on this value. How can I retrieve it for the current route?
Thanks
1) Can these be changed at runtime? If so, how?
With any knockout binding, the binding is recomputed if the bound value triggers subscriptions. That means cacheViews can be changed at runtime if it is an observable.
I'm not sure if the right way to do this is ko.observable({ cacheViews: false }) or { cacheViews: ko.observable(false) }. In fact, I believe which one will work is somewhat dependent on the version of knockout you're running, but one of them will work.
2) Does it matter that one is using data-bind and the other is using
ko comment style?
No.
3) How granular is this value? Is it per "router" or per "route"?
Per router. More specifically, per binding.
4) Can I set this value on individual routes like /#page1, /#page2,
/#samples/list, etc?
There's nothing out of the box that lets you set cacheViews per route. cacheViews is processed in the compose binding, which is being called under the hood in the router and so you would need to hook in there. However, Durandal is excellent about exposing hooks at every part of the lifecycle for custom logic. I'm sure with a little digging you can come up with your own customization to handle this.
5) Because the page event life-cycle is different between true/false I
need to have some specific logic in my vm depending on this value. How
can I retrieve it for the current route?
You would want to create the observable from (1) in your viewModel and have your associated view read it, like any other observable in knockout. Then, you will be able to access the current value directly off the viewModel. For example:
viewModel
viewModel = {
settings: ko.observable({
cacheViews: false
})
}
view
<!-- ko router: settings -->
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));
}
}
It may sound like a pedantic question. Sorry :)
I have a case like this... here's my router definition:
Router.map(function() {
this.resource('gobernadores', { path: '/gobernadores' }, function() {
this.resource('gobernador', { path: '/:id_estado' }, function() {
this.route('simulacion', { path: '/simulacion' }),
this.route('info', { path: '/info' })
})
});
this.route("login");
this.route("bienvenido");
});
In the "gobernadores" route, I have list of provinces. You can see it's a nested layout. In that same page, we're showing the currently-selected province (that's the gobernador route). Inside the template for that gobernador route, I have a tab, with two elements..., one showing the route "simulacion", and the other one showing the template of route "info" (of that province).
Now, the problem: as user jumps from one province to another province (by clicking the navigation menu on the left side of the screen), I want to keep in memory, the tab that was currently selected, for each province.
So, if the user is currently seeing the result of simulacion of province X, and then he clicks on the link to go to province Y (where he will be presented with "info" of province Y), and then he goes back to province X, I want the app to take the user back to the screen he was seeing (the simulacion of province X).
You can't have that information stored in the controller (GobernadorController), because I can see that controllers can't keep state, it's stateless.
So..., I have to move that info into the model of the route (GobernadorRouteModel)...
My doubt: is it okay? Why my doubt? Because of this: http://emberjs.com/guides/concepts/core-concepts/
It says:
MODELS
A model is an object that stores persistent state. Templates are
responsible for displaying the model to the user by turning it into
HTML. In many applications, models are loaded via an HTTP JSON API,
although Ember is agnostic to the backend that you choose.
ROUTE
A route is an object that tells the template which model it should
display.
This GobernadorRouteModel is not something I persists in the backend. I have no intention to do that. So, am I violating the general advice for a good EmberJS app?
Or in other words: "persistent" here doesn't have to mean "something you save into DB", right? It's just "something you want to keep around..., eventhough only during the session of the app, in the memory".
Thanks in advance,
Raka
You can't have that information stored in the controller (GobernadorController), because I can see that controllers can't keep state, it's stateless.
This might be where your problem arises. Controllers are not stateless. Controllers in Ember are singletons and keep their state throughout the lifecycle of the app. However, this is going to change in Ember 2.0. To quote from that RFC:
Persistent state should be stored in route objects and passed as initial properties to routable components.
So if you're trying to be forward-compatible, that is the approach I would take. In my opinion, models should really only be used for persistent state (persistent meaning it's persisted between page loads). To keep session state, I would do as the RFC says and keep that state in the routes and inject it into the controllers during the resetController hook.
Or if you don't want to be that fancy and you don't care about forward-compatibility, just have a global Session object that you store state in. That's how I currently do it and it works quite well. (Although we will probably move away from it.)
TL;DR: No, I don't think you're using models for their intended purpose.
How can I pass computes into components, such that changing the value in selected in one component will affect a value in a different component.
Example
http://jsbin.com/feleko/1/edit?html,js,console,output
I'm trying to set it up so that selecting a value in the first select changes the options available in the second. I think listening for dom change events should be straightforward, but I don't seem to be getting a compute I can update, or have access to the parent scope in order to use an attribute name to update it. Likewise the max value isn't an active object that receives updates.
I've found an obtrusive way. can.mustache provides a data helper that puts the current context on the element's data.
<select {{data 'context'}} value="a">
Then in the init event I can capture the element data and assign it to the scope so it's available to scope functions.
events: {
init: function(el, opt) {
opt.scope.context = el.data('context')
}
}
Looking up values is then possible if awkward.
this.context[this.attr('value')]
So Iโm looking to make some routes within my super cool can.js application. Aiming for something like thisโฆ
#!claims ClaimsController - lists claims
#!claims/:id ClaimController - views a single claim
#!claims/new ClaimController - creates a new claim
#!claims/:id/pdf - do nothing, the ClaimController will handle it
#!admin AdminController - loads my Administrative panel with menu
#!admin/users - do nothing, the AdminController will handle it
#!admin/settings - do nothing, the AdminController will handle it
So how might we do this?
โclaims routeโ: function() { load('ClaimsController'); },
โclaims/:id routeโ: function() { load('ClaimController'); },
โadminโ: function() { load(โAdminControllerโ); },
Cool beans, weโre off. So what if someone sends a link to someone like...
http://myapp#!claims/1/pdf
Nothing happens! Ok, well letโs add the route.
โclaims/:id/pdf routeโ: function() { load('ClaimController'); },
Great. Now that link works. Here, the routerโs job is only to load the controller. The controller will recognize that the pdf action is wanted, and show the correct view.
So pretend Iโve loaded up a claim claims/:id and I edit one or two things. Then I click the Print Preview button to view the PDF and change my route to claims/:id/pdf.
What should happenโฆ the Claim Controller is watching the route and shows the pdf view.
What actually happensโฆ the router sees the change, matches the claims/:id/pdf route we added, and reloads the Claim Controller, displaying a fresh version of the claim pulled from the server/cache, losing my changes.
To try and define the problem, I need the router to identify when the route changes, what controller the route belongs to, and if the controller is already loaded, ignore it. But this is hard!
claims //
claims/:id // different controllers!
claims/:id //
claims/:id/pdf // same controller!
We could just bind on the "controller" change. So defining routes like can.route(':controller') and binding on :controller.
{can.route} controller
// or
can.route.bind('controller', function() {...})
But clicking on a claim (changing from ClaimsController to ClaimController) won't trigger, as the first token claim is the same in both cases.
Is there a convention I can lean on? Should I be specifying every single route in the app and checking if the controller is loaded? Are my preferred route urls just not working?
The following is how I setup routing in complex CanJS applications. You can see an example of this here.
First, do not use can.Control routes. It's an anti-pattern and will be removed in 3.0 for something like the ideas in this issue.
Instead you setup a routing app module that imports and sets up modules by convention similar to this which is used here.
I will explain how to setup a routing app module in a moment. But first, it's important to understand how can.route is different from how you are probably used to thinking of routing. Its difference makes it difficult to understand at first, but once you get it; you'll hopefully see how powerful and perfect it is for client-side routing.
Instead of thinking of urls, think of can.route's data. What is in can.route.attr(). For example, your URLs seem to have data like:
page - the primary area someone is dealing with
subpage - an optional secondary area within the page
id - the id of a type
For example, admin/users might want can.route.attr() to return:
{page: "admin", subpage: "users"}
And, claims/5 might translate into:
{page: "claims", id: "5"}
When I start building an application, I only use urls that look like #!page=admin&subpage=users and ignore the pretty routing until later. I build an application around state first and foremost.
Once I have the mental picture of the can.route.attr() data that encapsulates my application's state, I build a routing app module that listens to changes in can.route and sets up the right controls or components. Yours might look like:
can.route.bind("change", throttle(function(){
if( can.route.attr("page") == "admin" ) {
load("AdminController")
} else if(can.route.attr("page") === "claims" && can.route.attr("id") {
load("ClaimController")
} else if ( ... ) {
...
} else {
// by convention, load a controller for whatever page is
load(can.capitalize(can.route.attr("page")+"Controller")
}
}) );
Finally, after setting all of that up, I make my pretty routes map to my expected can.route.attr() values:
can.route(":page"); // for #!claims, #!admin
can.route("claims/new", {page: "claims", subpage: "new"});
can.route("claims/:id", {page: "claims"});
can.route("admin/:subpage",{page: "admin"});
By doing it this way, you keep your routes independent of rest of the application. Everything simply listens to changes in can.route's attributes. All your routing rules are maintained in one place.
I'm working on a simple CRUD proof of concept with Rails/Backbone/JST templating. I've been able to find a lot of examples up to this point. But after much searching and reading, I've yet to find a good example of how to handle these scenarios:
info message: new item successfully added to list (shown on list screen)
info message: item successfully deleted from list
error message: problem with field(s) entry
field level error message: problem with entry
The Backbone objects are:
Collection (of "post" Models) -> Model ("post" object) -> List/Edit/New Views (and a JST template for each of these views)
So, I'm looking for a high level description of how I should organize my code and templates to achieve the level of messaging desired. I already have a handle on how to perform my validation routine on the form inputs whenever they change. But not sure what do with the error messages now that I have them.
Here is the approach I'm considering. Not sure if it's a good one:
Create a "Message" Model, which maps to a "View", which is a sub-view (if that's possible) on my existing views. This view/model can display page level messages and errors in the first three scenarios I mention above. Not sure if it's feasible to have a "sub-view" and how to handle the templating for that. But if it's possible, the parent templates could include the "message" sub-template. The message view could show/hide the sub-template based on the state of the message model. Feasible? Stupid?
For the fourth scenario, the model validation will return an error object with specific messages per each erroneous field each time a "model.set" is called by form field changes. I don't want to interrupt the "model.set" but I do want to display the error message(s) next to each field. I want to know how to factor my edit/new template and Post model/view in such a way that I don't violate the MVC pattern. I.e. I don't want to put references to DOM elements in the wrong plage.
Sorry if this is vague. If you're inclined to help, let me know what code snippets could be helpful (or other details) and I'll provide them.
You create a global eventbus. When ever an error appears trigger an event. Your view that should show the message listen to the events on this eventbus. Doing so, your error message view dont needs to know all of your collection and vice versa. The eventbus is simple:
var eventBus = _.extend({}, Backbone.Events);
Add it to your collection and trigger it when ever add was called:
var myCollection = Backbone.Collection.extend({
initialize: function([],eventbus){
this.bind('add', function(obj){eventbus.trigger('added', obj)}
}
})
Take also a look at the article: http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/