I am trying to make the body element of a Durandal app (a SPA framework which employs Knockout) by checking a module's state (which account type users are logged in as). In the viewmodel of the app's shell, I have the following code:
function bindingComplete() {
viewhelper.accountVisualTreatment();
}
And accountVisualTreatment is defined in the viewhelper module as such:
if (typeof(appsecurity.userInfo()) == 'undefined') {
$("body").addClass("notloggedin");
$(".container").addClass("notloggedin");
} else {
if (appsecurity.isUserInRole(['Account Manager']) && !appsecurity.isUserInRole(['Administrator'])) {
$("body").addClass("accountmanager");
$("nav").addClass("hidden");
} else {
$("body").removeClass();
$(".container").removeClass("notloggedin");
$("nav").removeClass("hidden");
}
}
Everything works fine if I refresh the pages when logged in as the diff account types. But as a SPA framework, there's no page refreshes while being used. Hence, the classes are not being applied as I want it to. How do I make it so the body, container, and nav elements' classes are being bound as I want?
Edit: I have tried to call viewhelper.accountVisualTreatment() in viewmodels of the landing page of the different account types but to not avail. Still needs page refresh.
Edit: I ugly-ly fixed it by applying a css binding to the container div in my shell.html
<div class="container" data-bind="css: viewhelper.accountVisualTreatment()"> ... </div>
Because you want to do this in the body (so globally). So my assumption is you want to update the css classes as soon the accountinfo changes. You could create a singleton observable with the accountinfo. When it changes you just subscribe to the observable.
The only problem is with elements which aren't in the dom. (like between loading pages). Because you use Durandal you have different hooks to change the dom with jQuery.
To do this in the viewmodels you should use attached() or compositionComplete() see: http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks.html. This is the callback you get from durandal when the dom is completly attached to the viewmodel and all the observables.
I would say you don't want to do this in every viewmodel. So I would suggest to hook on the router:navigation:composition-complete event on the router, see: http://durandaljs.com/documentation/api.html#class/Router/event/router:navigation:composition-complete. This will make sure you will get the callback for every new hash navigation.
You can register to the global event in the data-main where the app is started:
app.on('router:navigation:composition-complete').then(viewhelper.accountVisualTreatment());
I hope this helped.
Related
I'm currently learning vue.js and i'm struggling with the communication between parent and child components.
I'm trying to build two components (in separate files), a "accordion-container" and "accordion". The idea ist to then use them something like that on pages:
<accordion-container>
<accordion :title="'Accordion n1'">Insert HTML code here</accordion>
<accordion :title="'2nd Accordion'">Insert HTML code here</accordion>
</accordion-container>
Code for the container:
<template #closeAccordions="closeOtherAccordions">
<div class="accordion-container"><slot></slot></div>
</template>
<script>
export default {
props:['title'],
methods:{
closeOtherAccordions: function(){
console.log('Emit from child component received')
},
},
data: function() {
return {
}
}
};
</script>
Code for the accordions:
<template>
<div class="accordion" v-bind:class="{ open: isOpen }" :data-title="title">
<div class="title" #click="toggleAccordion">
<p>{{title}}</p>
</div>
<div class="content"><slot></slot></div>
</div>
</template>
<script>
export default {
props:['title'],
methods:{
toggleAccordion: function(){
this.isOpen = !this.isOpen
this.$emit('closeAccordions')
}
},
data: function() {
return {
isOpen: false
}
},
};
</script>
On the accordion i'm trying to emit "closeAccordions" (with the method toggleAccordion())
Then on the parent (accordion-container) i'd like to "listen" for that emit (with :closeAccordions="closeOtherAccordions"), and then execute a method on the parent.
But that method does not get called when i click the accordions.
Is my idea even possible? (Open to other ideas :) )
It won't work that way. The parent component cannot directly communicate to any components rendered within its slots via events, props, or by any other means that can only be achieved at the site where the slot contents are directly rendered (the container component doesn't control this).
When you are designing a component and you put a <slot> in the template, all you are doing is designating an insertion point within the template that users of the component can inject their own content.
You have 4 options:
(Advanced) Write the render function by hand and override the rendered slot vnodes to inject your own event listeners, props, etc.
Expose an API using scoped slots where you pass some data or methods to the slot which the user of the component would have to hook up in order for the component to operate correctly. Users of the component would have to remember to hook everything up correctly between the container and each accordion, so it's not ideal in this situation, but in general it is useful when you want to leave some of the functionality up to the user as to how the parent and children should operate.
Don't use events to communicate between the container and accordions, instead the accordions can call methods on the container directly via this.$parent.
Use provide/inject to allow the container to provide an API that each accordion can inject and use.
(3) is the recommended approach in this situation. The container and accordion components should be tightly coupled here. The accordion component can (and should) only be used directly within the container component, so it's OK if they communicate directly like that.
// Change this
this.$emit('closeAccordions')
// To this
this.$parent.closeOtherAccordions()
For more complicated components, (4) might be better.
This may seem like an obscure question, but I am having issues with my angular inheriting scripts via an invoke script.
The generalization for this would be as follows:
We have a custom browser that will create a button when a custom "property" is added to an input object on our webpage(where the angular resides). When that property is added (similar to class and ID), it will put a button next to the Input object.
The issue I am having is that it will only work on objects outside of the ng-view.
The code works fine if the input object is located in my main index html file (before ng-view is called).
Is there a way to make it so that the custom scripts can be inherited in, what I believe, is the controllers?
Any tips are greatly appreciated.
EDIT: Here are some code snips that are currently being used to make this functionality possible.
The Browser Side:
string invokeScript = "(function(){function i(n,t){t.parentNode.insertBefore(n,t.nextSibling)}function r(n,t){var o={scannableInputId:n.id,scannerType:t},u=document.createElement(\"span\"),f=document.createAttribute(\"class\");f.value=\"input-group-btn\";u.setAttributeNode(f);var r=document.createElement(\"button\"),s=document.createTextNode(\"scan\"),e=document.createAttribute(\"class\");e.value=\"btn btn-primary\";r.appendChild(s);r.setAttributeNode(e);r.addEventListener(\"click\",function(){window.external.notify(JSON.stringify(o))});u.appendChild(r);i(u,n)}var n=document.querySelectorAll(\"[data-barcode-scan]\");for(var t in n)n.hasOwnProperty(t)&&r(n[t],n[t].getAttribute(\"data-barcode-scan\").toLowerCase())})();";
if (Web != null)
{
await Web.InvokeScriptAsync("eval",
new[]
{
invokeScript
});
}
The Website Side:
<h2>
Item (Demo): <input id="scannedValueDemoTextBox"
name="scannedValueDemoTextBox"
ng-model="scannedValueDemo"
type="text"
data-barcode-scan="single"
style="width: 200px"/>
The "data-barcode-scan="single" is what should trigger the button to appear, which works outside the ng-view/controllers.
How can I use ng-include in such way that it's content will be loaded only once?
Here is what I have:
<div data-ng-if="%condition-1%" data-ng-include="%url-1%"></div>
<div data-ng-if="%condition-2%" data-ng-include="%url-2%"></div>
<div data-ng-if="%condition-3%" data-ng-include="%url-3%"></div>
...
In my case only one condition is true at some moment of time.
And any condition can change its value many times during page lifetime.
So ng-include will load the same content again and again.
How can I tell Angular to process ng-include only once - when the appropriate condition becomes true for the first time?
Loading them all at once will kill the page because every template is large and heavy.
Also there is no strict sequence of condition changes, for example, condition-3 may never become true during page lifetime - I'd like not to load url-3 content at all in this case.
Thanks!
UPDATE
Yes, template is already on cache. But it has a complicated internal structure like references to external images, iframes and so on - all this things are reloading each time when I'm using ng-include.
You have many solutions but only 2 come to my mind at the moment
1° Replace the ng-if for a ng-show, as the ng-if deletes the dom and all children scopes available, forcing the framework to make the request once again, while if you were using ng-show, the dom would only be hidden and the request would have only be made once.
2° If you do need to use ng-if and the content from the server is static, you could cache it on the javascript layer by manually accesing the $templateCache service provided by angular, or if the content you wish to load is html, you could either use the $templateCache service on the javascript layer or use the ng-template tag to preload that data.
Example:
<script id="url/you/want.html" type="text/ng-template">
<div>I am preloaded dom that responds to the url/you/want.html
requests made by this application
</div>
</script>
Cheers
How about using only one ng-include and using some logic in the controller to switch which source to use using a binding? This way only one will ever be loaded at a time.
Controller
function($scope) {
$scope.activeTemplate = null; //some default or even null
$scope.$watch('condition', function(newvalue) {
//whatever logic you need to switch template
if (newvalue == 'condition1') {
$scope.activeTemplate = '/path/to/condition1.html';
} else if (newvalue == 'condition2') {
$scope.activeTemplate = '/path/to/condition2.html';
} else {
$scope.activeTemplate = '/path/to/default.html';
}
});
}
This way only one template will ever be loaded at a time, and you've reduced the number of bindings from 3 to 1. (however you have added a watch so effectively from 3 to 2 maybe)
I am working on creating an Angular service that will append a simple notification box to the DOM and display it, without having to add HTML code and write the logic to hide and show it when necessary.
The service is called $notify and is used as below:
$notify.error( "this is an error", {position: "bottom-left"} );
The service will use angular.element to build the notification box and add it to the DOM. All of this works great. However, I am also using ngAnimate and animate.css to have the notification smoothly slide in on show and slide out upon closing. I've verified that the animations work if I simply paste the notification HTML code into my page but will not work when the code is added dynamically via the service. Do items have to be in the DOM at document load for ngAnimate to work? I've verified that the Angular service is loaded and properly inserting the HTML code but no animations are being applied. Here's what the HTML and CSS look like.
HTML:
<div class="simple-notify simple-notify-top-left simple-notify-info" ng-if="toggle">
<simple-notify-header>Hello!<span class='simple-notify-dismiss pull-right' ng-click='doSomething()'>×</span></simple-notify-header>
<simple-notify-body>Some bogus text here!</simple-notify-body>
</div>
CSS/LESS:
.simple-notify {
&.ng-enter {
display:none;
animation: #animate-enter #animation-length;
-webkit-animation: #animate-enter #animation-length;
}
&.ng-enter-active {
display:block;
}
&.ng-leave {
animation: #animate-leave #animation-length;
-webkit-animation: #animate-leave #animation-length;
}
}
Thanks!!!
You should never modify elements on the page from a service, this is what a directive is for. You should create an attribute directive on your body HTML element and in that place your logic. You can use $rootScope.$broadcast from your $notify service and $scope.$on in your directive. Alternatively, you can scrap the $notify service altogether and just use $rootScope.$broadcast as it can accept as many arguments as you want.
Lastly, you'll need to use the $compile service to make your HTML run properly after you've added it to the body. The $compile is what turns raw DOM into code Angular will process.
From inside your directive's scope.$on handler in the link function, you'd have something like.
$compile('<div>template code here</div>')(scope, function(cloned, scope){
element.append(cloned);
});
But you'd also need a cleanup as well. You might just add the code once as the directive's template and show/hide it with different content instead of adding/removing the DOM.
I'm new to AngularJS, and I'm dealing with a problem while implementing jQuery custom content scroller into my application.
I need the scroller to update, when I update the content with Angular, for this the scroller has an update method. My problem is, that I don't know where to call it. The markup for the content is the following:
<div class="scroll-list nice-scrollbars">
<ul class="gallery clearfix">
<li class="extra-alt" ng-repeat="item in relatedItems.data">
...
</li>
</ul>
</div>
I was trying to call the update method in success branch of Angular's $http.post:
$scope.relatedItems = $http.post($scope.url, {
'filterType': 'related', 'film': id
}).success(function() {
$(".nice-scrollbars").mCustomScrollbar('update');
});
This didn't work, I think it's because when the success method is called, the view content is not updated yet (I could see it using an alert function, the data appeared after closing the dialog box)
The only way I was able to make the scrollbars work was using the scroller's advanced property for watching the changes in the content (these are the options passed to the scrollbar):
var scrollbarOpts = {
scrollButtons:{
enable:true
},
advanced:{
updateOnContentResize: true
//#TODO: get rid of this, correctly find where to call update method
}
}
This is bad practice, as this options reduces the performance of the whole script.
I would like to know, where is the correct place to call jQuery methods needed to update DOM as needed, or how is such binding to view changes done correctly in AngularJS?
DOM manipulation should be done in a directive (not a controller). The directive should $watch() your model for changes, and the watch callback should perform the jQuery DOM manipulation. Sometimes $evalAsync is needed to run the jQuery code after Angular has updated/modified the DOM (but before the browser renders. Use $timeout if you want do perform some action after the browser renders). See this answer, where I provided a fiddle showing how to use a directive to $watch() a model property, and I used $evalAsync in a mock fetch() function.
For your particular case, I suggest you first try the following, in a directive:
scope.$watch("relatedItems.data", function() {
$(".nice-scrollbars").mCustomScrollbar('update');
});
and if that doesn't work, try this:
scope.$watch("relatedItems.data", function() {
scope.$evalAsync( // you might need to wrap the next line in a function, not sure
$(".nice-scrollbars").mCustomScrollbar('update')
);
});