Knockout SPA: Different Layouts(Master pages) - javascript

I'm building SPA application with knockout.js .
Basically what i do is bind as current page a ko component depending on what is route.
It looks something like
<div id="currentPage" data-bind="component: { name: currentRoute.page,
attr: currentRoute }>
</div>
This is for the current page and the whole picture whit the layout looks like:
<html>
<head></head>
<body>
<div data-bind="component: {name: "nav"}></div>
<div data-bind="component: {name: "aside"}></div>
<div id="currentPage" data-bind="component: { name: currentRoute.page,
attr: currentRoute}">
</div>
</body
</html>
The problem is that i dont have one layout.. the other one looks like this (pseudo used)
<html>
<body>
<wrapper>
<currentPage>
</wrapper>
</body
</html>
So basically the first layout is not direct parent of the currentPage module but the second is..
The variants for dynamic layout changing that i can think of are
Specify the layout components in each page.. But i dont think it is good idea cause i will be writing the same code over and over and will not be able to persist the state in the layout component because of when i change the next page the layout will be recreated( not fully but enough to loose the state )
The layout is separate from the currentPage component so only the currentComponent binding will be changed.. this is perfect for persisting state in layout but not good when i have variant where i want different layout for example which is wrapper around the componentBinding..
I'll be very happy if some one shares fresh ideas how to solve such problems.

In knockout if any part of view is to be dynamically changed you got to have bindings attached to the view. You say that the outer layout of the page is dependent on the inner page being displayed, so logically I consider it part of that page view definition. That is why I would not look for some special way to handle this case.
If I am not missing something, the reasonable thing to do in this case is to just include layout in each page. You could then handle repetitive code by using "traditional" approach using knockout templates (especially Note 5 on dynamic templates from http://knockoutjs.com/documentation/template-binding.html) and code extraction.
Completely other approach you might want to take is to put all layouts in master page and then control them by using bindings like "if" and "visible".

Related

EmberJS template component suddenly not rendering on page

I'm following this Ember tutorial and I've suddenly run into an issue where my rental-listing.hbs template component stops rendering. It started when I began implementing the map service.
I don't understand where this could be happening. I've copied the code from parts of the GitHub repository that I thought were relevant but to no avail.
I have a rental.hbs template that looks like so:
<div class="jumbo">
<div class="right tomster"></div>
<h2>Welcome!</h2>
<p>We hope you find exactly what you're looking for in a place to stay.</p>
{{#link-to "about" class="button"}}
About Us
{{/link-to}}
</div>
{{outlet}}
Which in turn has a template component called rental-listing.hbs:
<article class="listing">
<a
onclick={{action "toggleImageSize"}}
class="image {{if this.isWide "wide"}}"
role="button"
>
<img src={{this.rental.image}} alt="">
<small>View Larger</small>
</a>
<div class="details">
<h3>{{link-to this.rental.title "rentals.show" this.rental class=this.rental.id}}</h3>
<div class="detail owner">
<span>Owner:</span> {{this.rental.owner}}
</div>
<div class="detail type">
<span>Type:</span> {{rental-property-type this.rental.category}} - {{this.rental.category}}
</div>
<div class="detail location">
<span>Location:</span> {{this.rental.city}}
</div>
<div class="detail bedrooms">
<span>Number of bedrooms:</span> {{this.rental.bedrooms}}
</div>
</div>
<LocationMap #location={{this.rental.city}}/>
</article>
The only thing I have added to the above is the line <LocationMap #location={{this.rental.city}}/> but it still doesn't work if I remove it.
The console shows me no errors and I can actually see I am getting the three dummy objects I want from Mirage:
So I'm definitely getting the objects and from what I see I'm doing everything necessary in the templates to render it but they aren't. Should I be looking somewhere else?
Are you able to provide a link to your example? By having each piece of the ember application you mention it would be best to answer definitely. I can give a general answer with strategies for debugging the template.
The conventions behind ember.js make understanding the "whys" frustrating at first and possibly opaque. Ember's handlebars implementation governs how values are populated and accessed within templates using very specific rules. Ember treats templates (handlebars files) differently depending on whether it is for a route or component. Component's have an isolated context and must receive values by explicit passing in or dependency injection. Then, you can use such values in a component's template by accessing those properties with {{this.somePassedInValue}}.
In the super-rentals app, it appears the rental index route invokes the components responsible for displaying the individual units. I found this in app/templates/rentals/index.hbs.
<li><RentalListing #rental={{rentalUnit}} /></li>
The route template iterates over the list of filteredResults. Each of these is the rentalUnit. A good first step would be to use the {{log}} helper to print out that the value of rentalUnit to ensure it is what you expect.
Alternatively, you could try cloning https://github.com/ember-learn/super-rentals and applying the changes you want to make step by step from the master branch. By doing so, you could easily undo a single change to see what caused something to not show up as expected.
<LocationMap #location={{this.rental.city}}/>
to be written as below
<LocationMap #location={{this.rentals.city}}/>
may be typo error.
also repeat this for there place in that template.
Because the model name in the console is rentals not rental

Clarifying AngularJS theory snippet from Tutorial

Amongst great theory snippets from Step 3 of AngularJS Tutorial, that passage left me wondering:
The scope, that glues our controller and template
together into a dynamic view, is not isolated from other parts of the
page. What this means is that a random, unrelated change in a
different part of the page (e.g. a property-name conflict) could have
unexpected and hard-to-debug side effects on our view.
(unquoted part 1 from the same link was very much clear)
I couldn't imagine a reallife code example illustrating the issue shown in the quoted text. Can you show me such an example?
My own guess is based on inherited scopes:
<!doctype html>
<html lang="en" ng-app="phonecatApp">
<head>
...
</head>
<body>
<div ng-controller="PhoneListController">
{{secretObject.dontDareToTouchThat}}
<div ng-controller="PhoneListTwoController">
<ul ng-click="touchThat()">
<li ng-repeat="phone in phones" >
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</body>
</html>
Controllers' logic:
'use strict';
angular.module('phonecatApp', [])
.controller('PhoneListController', function PhoneListController($scope) {
$scope.secretObject = {
dontDareToTouchThat: 'I"m pure and beautiful'
}
}).controller('PhoneListTwoController', function PhoneListTwoController($scope) {
$scope.touchThat = function(){
$scope.secretObject.dontDareToTouchThat = 'Derp';
}
$scope.phones = [ ... ];
});
But I'm not sure about it at all, as the possible actions of PhoneListTwoController don't look like "random, unrelated change in a different part of the page". One scope is right inside the other, manipulating the outer scope, and I think the authors meant something different, like two sibling scopes messing with each other.
So, again, I ask you to illustrate the quoted passage with the relevant code example.
This does indeed refer to scope inheritance and its (often not straightforward) consequences. In case you haven't seen it already, here is a great write-up: Understanding Scopes
It can get much more tricky than what you think :)
Especially (as the tutorial mentions) for large, real-world apps, where different teams work on different parts of the app or where certain parts remain untouched for months.
To show but a very simple, "realistic-ish" example (which again is nothing near as complex as a large app):
Imagine that you are starting your own business; an e-shop. You want to start small, so you only have phones and tablets for now.
All you need is a basic layout - a header, a nav-bar and your content area:
My Cool e-Shop
----------------------------
[Phones] [Tablets]
----------------------------
<CONTENT HERE>
You set up two routes - one for phones, one for tablets - and decide to encapsulate each page's content as a component-like directive. E.g. the #/phones route will have a template like <phone-list></phone-list> and the phoneList directive will look like this (unfortunately you had never heard of isolate scopes):
.directive('phoneList', function phoneListDirective() {
// DDO
return {
template:
'<h2>Phones</h2>' +
'<ol>' +
'<li ng-repeat="phone in phones">' +
'<b>{{ phone.name }}</b> (OS: {{ phone.os }})' +
'</li>' +
'</ol>',
scope: true,
link: phoneListPostLink
};
// Functions - Definitions
function phoneListPostLink(scope) {
scope.phones = [
{id: 1, name: 'Samsung Galaxy', os: 'Android'},
{id: 2, name: 'Google Nexus', os: 'Android'},
{id: 3, name: 'Nokia Lumia', os: 'Windows'},
{id: 4, name: 'Apple iPhone', os: 'iOS'}
];
}
})
So far, so good. You have an almost identical route and directive for tablets and everything works fine.
Soon, your list of available phones and tablets grows and you need to add a filter feature. Piece of cake, you just add the following snippet to your directives' templates:
<div>
Filter:
<input type="search" ng-model="search.name" placeholder="Name..." />
<input type="search" ng-model="search.os" placeholder="OS..." />
</div>
<li ng-repeat="phone in phones | filter:search">
As simple as that, your users are able to search for phones and tablets by name and OS. Business is going great and life is peachy.
Fast-forward a few months and your site has grown, featuring several more sections and product categories. You decide that a "global search" widget would be a great addition for your nav-bar. All you need to do is add the following snippet to your main template:
<div class="search-widget">
<input type="search" ng-model="query" placeholder="Search the entire site..." />
<button ng-click="search(query)" ng-disabled="!query">Search</button>
</div>
(And of course implement a $scope.search() method on your main controller...)
The rest is history :P
In no time, sales start going down and you are out of business before you know it.
Here is a simple POC to see this in practice: Demo
tl;dr
Use isolate scopes and profit!
That passage was added by Georgios Kalpakas as commit #c2033d7 on May 24.
You might want to ask him your question.
The tutorial might be exaggerating here a little bit. At the least it is not really being very exact.
I've created a simple example on plunker which shows what kind of interference is possible and what is not.
The ng-controller directive does actually create a new child-scope. Variables on a scope are prototypically inherited by a child scope.
Referring to the plunker example this means that $scope.someVariable defined by controller 1 has no impact whatsoever on $scope.someVariable defined by controller 2 - (controller 1 is neither ancestor nor descendant of controller 2). It also means that the values set for $scope.someVariable cannot be overwritten by their parent controller which sets the same variable on its scope. The controller 3 which is also a descendant of parent controller does not set $scope.someVariable itself. In this case the plunker shows that the value set by parent controller takes effect in the view snippet controlled by controller 3. On the all child scopes of the parent controller scope someVariable will be available as
Object.getPrototypeOf($scope).someVariable
.
Nevertheless I agree with the tutorial, that using components which bind their state to their controller instance (which will be known as $ctrl in the template) instead of the scope directly has a lot of merits. Components have a clear import and export model. This makes them interchangeable and increases the chance for re-use.

angular.js: routing to an editor-version of the current article

I have some articles of the same category and I'm routing like this:
app.config(function($routeProvider) {
$routeProvider
.when('/chapter/:title', {
templateUrl: 'article.html',
controller: 'article'
});
});
article.html:
<h1>{{title}}</h1>
<p>{{content}}</p>
<button>Editor</button>
But now I want to get a route from every article of this chapter to an editor version to change the content. This editor-version could look like this:
editor_article.html:
<input type="text" value="{{title}}">
<textarea>{{content}}</textarea>
So what is the best way for routing the button of an article to the editor_article.html-template and load it with the same data?
There is no "best-way" here, and it all depends on your context.
Do all end-users have equal access to this (or put another way, should anybody who can open the dev-console be allowed to access this page)?
If not, you need to solve the login problem before you load most of the code for your SPA.
If login isn't a problem, or already taken care of, why do you need separate routes?
Do you need to cache these pages separately in-browser?
Are there other considerations beyond that?
Totally legitimate question; from an artistic perspective, perhaps you're hoping for page transitions which match other transitions you have for when you do change routes, or you are relying on routeParams for some logic that we don't know about.
But chances are good that all of the above things aside, you could simply have a button and a couple of ng-if statements, with directives.
<body >
<main ng-view></main>
</body>
<!-- template -->
<section >
<button
ng-click="article.toggleMode()">{{
article.editMode ? "View": "Edit"
}}</button>
<article-view
content="article.content"
ng-if="!article.editMode"
></article-view>
<article-edit
content="article.content"
onsave="article.save(content)"
ng-if="article.editMode"
></article-edit>
</section>
Using directives to define the two templates, using controllerAs:"article" in the example above.
Even that might be complicating it.

Data Binding Fails after app.setRoot('viewmodels/shell') for Durandal

On Durandal, I have a login page that's styled different from my other pages, so taking the advice I saw on some of the posts here, I've set up main.js to do a:
app.setRoot('viewmodels/login');
And on the login page, I provide the below method in login.js that resets the root:
loginRedirect: function() {
app.setRoot('viewmodels/shell');
}
The idea is that the user should go to the login page by default, and once logged in, I will invoke the method loginRedirect, which sets shell.html to be the root and thus reloads the content, and he should be able to navigate other pages from there.
Here's my login.html:
<div class="login">
<div data-bind="compose:'header-nav-plain.html'"></div>
<div class="container">
<!-- Content here-->
</div>
</div>
<div data-bind="compose:'viewmodels/footer'"></div>
<a class="go-inner-pages" data-bind="click: loginRedirect" href="#">Test link to go to 'inner' pages</a>
And here's my shell.html, that is the doorway to all other pages:
<div>
<div data-bind="compose:'viewmodels/header-nav'"></div>
<div class="container">
<div data-bind="compose:'viewmodels/site-nav'"></div>
<div data-bind="router"></div>
</div>
<div data-bind="compose:'viewmodels/footer'"></div>
</div>
You can see that shell.html is slightly different in structure from login.html, in that it binds to a different header-nav.html, and also a site-nav.html. (They share the same footer.html.) So on the login page, when I click on the test link that calls the loginRedirect method, it sets the root of the app to be shell.html, which will load the default page based on the parameters passed to router.map, as defined in shell.js:
activate: function(){
//Initializing router
router.map([
{ route: '', moduleId: 'viewmodels/dashboard', title: 'Dashboard', nav: true }
])
.buildNavigationModel();
//More code
return router.activate();
But, when I click on the test link and invoke loginRedirect, the new dashboard content is loaded, but the new header-nav and site-nav data binding didn't happen. I checked the inspector, and see that the structure has indeed changed to that of shell.html (as opposed to login.html), only data binding for header-nav and site-nav didn't happen. I.e. here's the new html after invoking loginRedirect:
<div>
<div data-bind="compose:'viewmodels/header-nav'"></div><!--header nav content not bound-->
<div class="container">
<div data-bind="compose:'viewmodels/site-nav'"></div><!--site nav content not bound-->
<div data-bind="router"><div class="durandal-wrapper" data-view="views/dashboard" data-active-view="true">
<!-- Dashboard content successfully loaded -->
</div><!--end data-bind="router"-->
</div><!--end .container-->
<div data-bind="compose:'viewmodels/footer'">
<!--footer content loaded-->
</div>
And from the Console (see below), one can see that while the shell and dashboard are bound, (even footer is bound a second time), header-nav and site-nav are not.
Does anyone have any idea why this is happening and what I might be doing wrong? Thanks in advance for your attention, hope to hear from someone soon. Cheers.
For view-only bindings, you would bind this way (we do the same as you, so I'm copying and pasting our code):
compose: {view: 'shellLeftFooter.html'}"
For views bound to viewModels, you bind this way:
compose: {model: 'viewmodels/navigation/shellNavigation'}
The default viewLocator will use the following convention:
If a view is specified with no model, then the view must be identified with the .html extension, as I have done in the former example, and it will not be bound to a viewModel;
If a model is specified with no view, as I have done in the latter example, then the model file is assumed to have a .js extension, and it is also assumed that a view exists in a folder named views under the app directory, with the same name as the model, but with an .html extension.
These aren't the only two conventions, but they are the ones germane to this discussion. To put the above in different words, make sure you have both a viewmodels and a views directory that are siblings of each other under the app directory. Make sure you give the viewModel and the view identical names, differing only in their extensions (.js for the former and .html for the latter).
I'm seeing all of your .html and .js files in the code you posted, so I'm sure what your intention is in each case (view only or model-view-viewModel). Also, your compose binding is not quite to standard.

Knockout.js - How to toggle between views

Alright, newbie Knockout question here.
In this example: http://learn.knockoutjs.com/WebmailExampleStandalone.html#Inbox
How does the mail detail view replace the folder list view?
That is, what feature causes the divs to be toggled? In inspecting the dom, I see that what happens is the div's are actually rendered empty when not displayed.
Can someone enlighten me? I know this is rather basic, but its the last piece I need to click into place for my understanding.
Just to be 100% clear: when you click on a row in the folder list, what causes the folder view to be emptied and the mail detail to display? Is it the with binding? (That doesn't seem right.)
You are on the right track with the with binding: in this example the views are changed using the with binding relaying on this feature of the binding:
The with binding will dynamically add or remove descendant elements depending on whether the associated value is null/undefined or not
So in the viewmodel code you will see something like this:
this.get('#:folder', function () {
self.chosenFolderId(this.params.folder);
self.chosenMailData(null);
$.get("/mail", { folder: this.params.folder }, self.chosenFolderData);
});
this.get('#:folder/:mailId', function () {
self.chosenFolderId(this.params.folder);
self.chosenFolderData(null);
$.get("/mail", { mailId: this.params.mailId }, self.chosenMailData);
});
So the functions which are "chaining" the view nulls out one of the properties while filling in the other which toggles the views defined as:
<!-- Chosen mail -->
<div class="viewMail" data-bind="with: chosenMailData">
...
<div/>
<!-- Mails grid -->
<table class="mails" data-bind="with: chosenFolderData">
</table>
This is not the nicest solution but don't forget that Knockout is a Databind/MVVM library and not a full blown SPA framework so it does not have concepts for layouting and higher level view composition.
However this could be made nicer with using the template binding:
<div id="mainView" data-bind="{template: {name: templateName, data: activeView}}">
</div>
And turning the views into templates:
<script type="text/html" id="ChosenMail">
<div class="viewMail">
...
<div/>
</script>
<script type="text/html" id="MailsGrid">
<table class="mails">
...
</table>
</script>
And in the routing only set the activeView property and lookup the corresponding template name for it:
this.get('#:folder', function () {
$.get("/mail", { folder: this.params.folder }, function(data) {
self.activeView(data);
self.templateName('ChosenMail');
});;
});
this.get('#:folder/:mailId', function () {
$.get("/mail", { mailId: this.params.mailId }, function(data) {
self.activeView(data);
self.templateName('MailsGrid');
});
});
But because this is quite much manual and error prone work I would use something like Durandal.js which is a real SPA framework and it was designed for this kind scenarios.
That is just a Demo on a light weight SPA scenario, with binding is just a inline template binding. Not very useful for a dynamic SPA. Like Nemesv suggests use the template binding.
The problem with the template binding is that its very verbose to use, I have addressed this in my Binding convention library (One of many features)
Instead of doing
<div id="mainView" data-bind="{template: {name: templateName, data: activeView}}">
</div>
You do
<div id="mainView" data-name="activeView">
</div>
My library will do the rest, check out the wiki on templates here
https://github.com/AndersMalmgren/Knockout.BindingConventions/wiki/Template-convention
And a little fiddle
http://jsfiddle.net/xJL7u/11/

Categories