How to update an AngularJS directive when using $route and partials - scope

http://jsfiddle.net/MTzJF/36/
The JSFiddle above is set up to explain the issue. But, basically:
HTML:
<breadcrumb></breadcrumb>
<div ng-view></div>
Angular Directive & Routing:
angular
.module('app', [])
.directive('breadcrumb', function() {
return {
restrict: 'E',
template: "<ul class='breadcrumb'><li ng-repeat='node in path'><a ng-href='{{node.url}}'>{{node.label}}</a></li></ul>",
replace: true,
controller: Ctrl1
}
})
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {
template: '<h1>{{pgTitle}}</h1>',
controller: Ctrl2
});
}]);
Controllers
function Ctrl1($scope) {
$scope.path = [{
label: 'Home',
url: '#/'}];
$scope.pgTitle = "Home"
}
function Ctrl2($scope, $routeParams) {
$scope.path = [{
label: 'Home',
url: '#/'},{
label: 'Node 2',
url: '#/node2'}];
$scope.pgTitle = "Node 2"
}
​
I expect that changing $scope.path in Ctrl2 will update the breadcrumb directive, but it's not happening. I have to believe it has something to do with their relative scopes, but simply don't understand it well enough to see what. I've read dozens of articles and StackOverflow posts on it, but nothing is specific enough to let me see what I'm missing.
I'm hoping someone can point me in the right direction.
Thanks much!
nz

The reason your fiddle is not working is because ( like you rightly identified ) of scope issue. Your Ctrl1 is the Controller that controls the scope for your directive. The directive is looking for a variable called path which is an array of path's. If we have a look at the path variable in that scope it seems to be containing just 1 value inside of it.
function Ctrl1($scope) {
$scope.path = [{
label: 'Home',
url: '#/'}];
$scope.pgTitle = "Home"
}
Now you wish to change this variable path in another controller Ctrl2. I am assuming that you are attempting to have the scope of Ctrl2 "inherit" from the scope of Ctrl1. To achieve this, first check on which element Ctrl2 is defined. Is that element ( html element ) a child of the element of Ctrl1 ?
From your HTML :
Element of Ctrl1 : <breadcrumb></breadcrumb>
Element of Ctrl2 : <div ng-view></div>
For Ctrl2 to be child of Ctrl1 : your HTML structure should look like :
<breadcrumb>
<div ng-view></div>
</breadcrumb>
If we make this change to your code, it doesnt work yet. This is because when angular looks at the directive for <breadcrumb> it has no idea what it should do with the stuff that is inside of that node. <breadcrumb> is a html node. Since it is a node, it can contain content / other nodes inside of it. When you replace this breadcrumb node with the template, you should also give angular instructions to the effect : "If you find stuff inside of me, put it here". This is how you do it.
Modify your directive code to be :
.directive('breadcrumb', function() {
return {
restrict: 'E',
template: "<div><ul class='breadcrumb'><li ng-repeat='node in path'><a ng-href='{{node.url}}'>{{node.label}}</a></li></ul><div ng-transclude></div></div>",
replace: true,
transclude : true,
controller: Ctrl1
}
})
There are few differences / changes here.
Added an attribute called transclude to the directive object and set it to true.
Wrap the whole template so that it can be returned as a single HTML element.
Specify the place where you want the contents of the to go. Notice the ng-transclude. That is the place where the contents go.
You will notice now that the content is getting replaced now. Except the path is not getting updated with the path value from the child controller. Have a look at this https://stackoverflow.com/a/14049482/1057639 answer. It is an amazing explanation of scope / prototypical inheritance. Basically, your write to path is creating a new path variable in the child scope that is overshadowing the parent path ( which is being used by the directive).
Your best bet in this scenario is to create an addPath function in the parent scope and use it ( in child scopes ) to add new Path when you define new subviews.
Here is a fiddle that does all these.

Related

handlebars - add content to head of view from partial

I am using express-handlebars in my project and have the following problem:
Question
I want to be able to add <script> oder such tags to my overall views head from a partial that is called inside the view.
Example:
The view
{{#layout/master}}
{{#*inline "head-block"}}
<script src="some/source/of/script">
{{/inline}}
...
{{>myPartial}}
{{/layout/master}}
The view is extending another partial (layouts/master) that I use as a layout. It adds its content to that ones head block through the inline partial notation, which works fine
the Partial "myPartial
<script src="another/script/src/bla"></script>
<h1> HELLO </h1>
Now I would like that particular script tag in there to be added to my views head-block. I tried going via #root notation but can only reference context there. Not change anything.
I know I could use jquery or similar to just add the content by referencing the documents head and such. But I wanted to know if this is possible at all via Handlebars.
I do doubt it is in any way. But if you have any ideas or suggestions, please do send them my way! Many thanks!!!
UPDATE
This wont work if you have more than one thing injected into your layout / view. Since this happens when the browser loads the page, it creates some kind of raceconditions where the helpers has to collect the things that have to be injected into the parent file. If its not quick enough, the DOMTree will be built before the helper resolves. So all in all, this solution is NOT what I hoped for. I will research more and try to find a better one...
Here is how I did it. Thanks to Marcel Wasilewski who commented on the post and pointed me to the right thing!
I used the handlebars-extend-block helper. I did not install the package, as it is not compatible with express-handlebars directly (Disclaimer: There is one package that says it is, but it only threw errors for me)
So I just used his helpers that he defines, copied them from the github (I am of course linking to his repo and crediting him!) like so:
var helpers = function() {
// ALL CREDIT FOR THIS CODE GOES TO:
// https://www.npmjs.com/package/handlebars-extend-block
// https://github.com/defunctzombie/handlebars-extend-block
var blocks = Object.create(null);
return {
extend: function (name,context) {
var block = blocks[name];
if (!block) {
block = blocks[name] = [];
}
block.push(context.fn(this));
},
block: function (name) {
var val = (blocks[name] || []).join('\n');
// clear the block
blocks[name] = [];
return val;
}
}
};
module.exports.helpers = helpers;
I then required them into my express handlebars instance like so:
let hbsInstance = exphbs.create({
extname: 'hbs',
helpers: require('../folder/toHelpers/helpersFile').helpers() ,
partialsDir: partialDirs
});
Went into my central layout/master file that`is extended by my view Partial and added this to its <head> section
{{{block 'layout-partial-hook'}}}
(The triple braces are required because the content is HTML. Else handlebars wont recognize that)
Then in the partial itself I added things like so:
{{#extend "layout-partial-hook"}}
<link rel="stylesheet" href="/css/index.css"/>
{{/extend}}
And that did the trick! Thanks!!!

MEAN js dynamic title

how to add dynamic title for each pages of the MEAN js application. in the layout.server.js has defined the title as follows.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{title}}</title>
so how can we make dynamic title?
Some people might be mislead thinking that besides it is already dynamic, it can be changed and it is controlled by angular out of the box because of having an expression with {{ }} but that's not quite true.
In fact {{title}} could mean an expression that should be evaluated against scope.title, however if you take a deeper look at MEAN.js you will see that it is using the swig template engine which also uses {{ }} to define variables. In this case, {{title}} is NOT an angular expression, it is a swig variable which was passed via express/swig and it can be changed in the config/env/default.js (in MEAN.js 0.4.0).
If you want the title to be changed in the frontend (i.e. possible to change it within angular logic) you have to assign a scope variable to the title element or use a custom directive. Even if, at first, the title value is the one defined using express/swig, angular can take control afterwards and change it accordingly to your needs.
One solution could be to define the title in your angular states like this:
.state('some-state', {
url: '/someurl',
templateUrl: 'some-path-to-view',
data: {
title: 'My new title',
}
})
And then listen for the $stateChangeSuccess event to set the title:
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
console.log(toState.data.title); // Prints the new title to the console
// Set the title
});
EDIT: First paragraph rewritten for more coherence.
To add to the accepted question, in the MeanJS stack you can do the following:
Create a new view in the modules/core/client/views ex. title.client.view.html.
In the file title.client.view.html you can get the title by doing:
<div ng-controller="HeaderController">
<span>{{$state.current.data.pageTitle}}</span>
</div>
The HeaderController has a $state variable that contains the current state title specified in:
.state('some-state', {
url: '/someurl',
templateUrl: 'some-path-to-view',
data: {
title: 'My new title',
}})
Then to get the title in the modules/core/server/views/layout.server.view.html file you include title.client.view.html like this:
<div ng-include="'/modules/core/client/views/title.client.view.html'"></div>
This will render the title of a state dynamically as you navigate.
The MEAN.JS page title is already dynamic and can be found at modules/core/client/directives/page-title.client.directive.js:
function listener(event, toState) {
var applicationCoreTitle = 'MEAN.js',
separator = ' - ',
stateTitle = applicationCoreTitle + separator;
Whereas 'MEAN.js' is the default page title and can be changed accordingly.

Can a Url.Action specify a route in MVC5?

In our default layout we have our logo wrapped with a URL.Action to navigate back to the home screen. However, if we are in an Area, it tries to navigate to the home controller in that area, which doesn't exist.
Our home controller is Submission. Here is the link in the layout file:
<a class="navbar-brand custm-navbar-brand" href="#Url.Action("Index", "Submission")">
<img src="#Url.Content("~/Content/images/eed-main-logo.png")" alt="E-Editorial Discovery" width="335" height="56">
</a>
If I am in an area like this: /Admin/Security/Index
the above link tries to go to: /Admin/Submission/Index
but it should go to: /Submission/Index
I'm sure there's a better way, but I haven't found it yet.
Specify the area like you would a parameter. So your first line should be:
<a class="navbar-brand custm-navbar-brand" href="#Url.Action("Index", "Submission", new { Area = "AreaName" })">
You could specify the area in the Url.Action call but this can get messy.
Rather than having to specify the area, why not sort the route out itself so it knows to tie it down to that namespace:
context.MapRoute("ExpressCheckoutRoute",
"expresscheckout/stage/{stageName}/{id}",
new { controller = "ExpressCheckout", action = "Stage", area = "FrontEnd", id = UrlParameter.Optional },
new[] { "Web.Areas.FrontEnd.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;
This sorted the issue out for me and I no longer have to specify the area paramter (take note of the last 2 parts to that mapping).

New to Sails.js how can I route to a view without including the layout.ejs?

New to Sails.js I've read over the docs but I don't see a way to NOT include the layout.ejs located in /views/.
I'm trying to implement Angular into my app and when I go
$routeProvider.when('/',{
controller: 'HomeController',
templateUrl: '/home'
})
So then in my routes I expect to be able to do:
'GET /home': { view: 'angular/home.ejs' }
But what happens is it loads home.ejs but also injects layout.ejs so it becomes an endless loop of injecting layout.ejs loading angular scripts from layout, then trying to load home.ejs which loads layout.ejs so on so forth
So how can I do this?
I know I can put views in /asset/angular/home.html but I would like to have .ejs so that I can render a different view (e.g. user is not logged in) or something.
Any information on how I can render views without injecting layout.ejs would be great, thanks!
Update:
So by doing this:
'GET /home': {
view: 'home',
locals: {
layout: false
}
}
it makes it work, I don't know if there is a global way so I don't have to flag layout: false for each one
To disable layouts globally, you can put layout : false under /config/view.js.
To disable layout individually, leave /config/view.js alone and do something like this in your routes:
'GET /home': {
view: 'home',
locals: {
layout: false
}
}

Marionette, how to change view's template on fly

I'm doing a single page webApp with Marionette and RequireJs, basically it is a Layout view nested with a lots of ItemView and compositeView. I want the page change to a new design by one click, so I want to ask a best practice about changing the view's template.
say, I got my view like this, and its templates are included by requirejs's text plugin:
define([
'text!templates/designNo1/template.html'
], function(template) {
var MyView = Backbone.Marionette.ItemView.extend({
/*Template*/
template: template,
initialize: function() {
......
},
onRender: function() {
......
}
});
return SkillView;
});
every view and theirs subviews are defined like this. and their templates located in "template/designNo1" folder. now I got a set of new design located in "template/designNo2", and I want apply it to the page, but I found there is no chance to do this, because the views are already loaded by RequireJs and the template's path are hard-coded. of course, I can override the view's template when create the view's instance, but if I do so, I must load all the new design in the upper-module which create the instance, that looks bad, and the new design are keep coming, it gonna be a mess soon.
so, any advice?
From what I can tell, it sounds like you are wanting to change the template on a view depending on different circumstances. Marionette's ItemView has a getTemplate() function that is called when rendering it. You can specify a function that determines which template to render from there.
Another approach might be to simply change the template of the view and re-render it. You could do that on a click event easily:
HTML
<div class="content"></div>
<button class="btn">Change Template</button>
Javascript
var template1 = '<div>Template 1</div>';
var template2 = '<div>Template 2</div>';
var ItemView = Backbone.Marionette.ItemView.extend({
template: template1
});
var itemView = new ItemView({ el: '.content' });
itemView.render();
$('.btn').click(function(e) {
e.preventDefault();
itemView.template = template2;
itemView.render();
});

Resources