Dynamically update AngularJS component definition - javascript

I'm looking for a way to dynamically update a component, I have an object containing the information about the component (name, functions, variables, services, template, bindings, etc.). Then I generate the component dynamically using this data. And I need to regenerate the component whenever the definition is changed.
angular.module('components', [])
.component('dynamicComponent', {
bindings,
template,
controller
})
angular.module('app', ['components'])
Is there a way to update the component that has been already registered on a module? or is there a way to update the module itself?
so far I've tried recreating the components module like so:
angular.module('components', [])
.component('dynamicComponent', {
...newData
})
but nothing happens.
I also tried recompiling the element after I recreated the module. Still nothing happens.
I also tried calling angular.bootstrap after creating the modules but I found out that is not possible without destroying the element bootstrapped. It's throwing an error saying the element was already bootstrapped.
My goal here is to dynamically update the component definition, on the fly, without reloading the page.

I was able to do this by destroying the bootstrapped element, recreating that element, and calling angular.bootstrap again with the new updated modules and components.
Here's an example,
The initial code:
<body>
<div id="app" ng-controller="MainController as $ctrl">
<dynamic-component data="$ctrl.data">
</dynamic-component>
</div>
</body>
angular.module('components', [])
.component('dynamicComponent', {
bindings: {...},
controller,
template
})
angular.module('app', ['components'])
.controller('MainController', ...)
angular.bootstrap(document.querySelector('#app'), ['app'])
And to update dynamic-component,
reset and destroy the bootstrapped element. Doing so will enable us to call angular.bootsrap on this element again
document.body.innerHTML = `
<div id="app" ng-controller="MainController as $ctrl">
<dynamic-component data="$ctrl.data">
</dynamic-component>
</div>
`
Define your modules.
Here, we can update the old dynamic-component however we want to. We can also add more components or modules.
angular.module('components', [])
.component('newDynamicComponent', {
bindings: ...newBindings,
controller: newController,
template: newTemplate
})
angular.module('app', ['components', ...otherModules])
.controller('MainController', ...)
angular.boostrap(document.querySelector('app'), ['app'])
Working example
If there's a better way, please let me know.

Related

My page turns blank when I try to use a controller in angular

I have a problem with angular to integrate a controller to my page. The page becomes blank as soon as I try to integrate it.
<div class="container-fluid" ng-app="ods-widgets" ng-controller="myCtrl" >
<ods-dataset-context context="cont" cont-domain="https://data.rennesmetropole.fr" cont-dataset="{{dataset}}">
</ods-dataset-context>
</div>
<script>
var app = angular.module("ods-widgets", []);
app.controller("myCtrl", function($scope) {
$scope.dataset= "statistiques-de-frequentation-du-site-rennes-metropole-en-acces-libre";
});
</script>
Without the controller:
http://jsfiddle.net/5c0xr8f4/13/
With the controller:
http://jsfiddle.net/8796ueyL/
ods-dataset-context is a component (https://github.com/opendatasoft/ods-widgets).
it's a component that I import via CDN.
I just want to control what is inside the cont-dataset
I looked into the library that you mentioned in your comment. It seems that the issue is that ods-widgets is already an angular module that is being imported via the CDN. If you name your own angular module with the same name, you are effectively overwriting this existing module that you have imported. So what you want to do is declare your own angular module and import ods-widgets as a dependency. You can take a look at the Fiddle for a working sample, but the important part is this one:
angular.module("myApp", ['ods-widgets']);
And in your HTML update the ng-app reference:
<div class="container-fluid" ng-app="myApp" ng-controller="myCtrl" >

How can I create an array from the value of the same directive on different pages in Angular JS?

I am using Angular 1.5.7 and am trying to see if I can push the value of an attribute within a directive used on several different pages to an array that lives in the controller.
I am pretty sure that I need to used transclusion in order to do this but I am stuck. Below is a simplified version of what I have so far:
about.html
<div ng-controller="MainCtrl as ctrl"
<div cd-header mypage="About">
<div>About Page</div>
</div>
</div>
contact.html
<div ng-controller="MainCtrl as ctrl"
<div cd-header mypage="Contact">
<div>Contact Page</div>
</div>
</div>
header.html
<div>{{mypage}}
<div ng-transclude></div>
</div>
cd-header.js
var cdHeader = function() {
return {
scope: {
mypage: "#"
},
transclude: true,
templateUrl: 'header.html',
link: function(scope) {
// Not sure but I think I might need a function here
}
}
}
module.exports = cdHeader;
MainCtrl.js
var MainCtrl = function($scope) {
var nav = [];
// Not sure how items that are pushed to the nav get to this point
}
module.exports = MainCtrl;
main.js
var app = angular.module("myapp", [
'about',
'contact',
])
.controller('MainCtrl', MainCtrl)
.directive('cdHeader', cdHeader)
I am able to get the value of the mypage attribute in the directive as well as its transcluded <div> to appear in the header but only for the current page in view.
The part I am missing is how to get all of the mypage values from each page into the header regardless of the current page in view. I am somewhat new to Angular and have read a lot but have not come across anything that explains how this can be done. Maybe this is achieved with a service? If so, I'm not sure how to go about it.
To clarify with a visual. This is what I see:
But this is what I want to see:
As you rightly pointed out, there are several ways to do it.
Perhaps you can pass the array from the MainCtrl as an attribute to the directive. For instance, nav-array="nav". However, before that, you need to set the array nav as such
var $scope = this;
this.nav = [];
The second option is to consume a service. Create an angular service, pass it as a dependency in the directive, and consume it.
Lets create an array in MainCtrl as $scope.headers = ['about':'About','contact':'Contact','home':'Home'] or create a factory/service to share the headers data and in header.html use ng-repeat to display the header name according to myPage value like below
<div ng-repeat="page in headers[myPage]">{{page}}
<div ng-transclude></div>
</div>

How to simplify angular-ui-router nested state with many combinations of sub states?

Here is the mock up of my app:
"Left side bar" is for directories, while the "main content" is for files. The files are enumerated from the selected directory ([list mode]).
The "left side bar" is to be reused for 2 other purposes:
display form to create new directory ([create mode])
display form to edit selected directory ([edit mode])
Similarly, the main content could also be in [list mode], [edit mode], and [create mode]. So, in total there would be 3 x 3 possible combinations.
Using ng-switch, this one can be modeled quite easily.
<div class="left-bar">
<div ng-switch on="directory.mode">
<div ng-switch-when="list"></div>
<div ng-switch-when="create"></div>
<div ng-switch-when="edit"></div>
</div>
</div>
<div class="main-content">
<div ng-switch on="files.mode">
<div ng-switch-when="list"></div>
<div ng-switch-when="create"></div>
<div ng-switch-when="edit"></div>
</div>
</div>
However, I wish to model this using angular-ui router. I am new to angular-ui, and the state model I could think of now is something like:
.state('main.folder-list.file-list', views: {'left-sidebar':{templateUrl:'directory-list.html'}, 'main-content':{templateUrl:'file-list.html'}})
.state('main.folder-list.file-edit', ...)
.state('main.folder-list.file-create', ...)
.state('main.folder-edit.file-list', ...)
.state('main.folder-edit.file-edit', ...)
.state('main.folder-edit.file-create', ...)
.state('main.folder-create.file-list', ...)
.state('main.folder-create.file-edit', ...)
.state('main.folder-create.file-create', ...)
One important requirement: when the mode of "left side bar" is switched, the content of "main content" shouldn't be changed (it should still be in the same mode as before), and vice versa.
How to simplify that?
First think first, remember that angular-ui router able to handle nested state. I usually use .state('root'), .state('root.app'), .state('root.app.specific'), etc. With that characteristics of angular-ui router, we can simply set $scope at root state like $scope.data = { status: 'ok' } and in root's child, we still able to call that $scope.data.
Also if your create, list, edit is a generic "class" or singleton. You can also make it simple using angular controller inheritance in your controller type $controller('[parent-controller]', {$scope: $scope});. So your parent-controller's $scope like method that they have etc. will be able to called inside your new controller's $scope. Angular-ui router's nested state is similar to this. For example:
Application.controller('AppCtrl', ['$scope', '$controller', function($scope, $controller) {
$controller('AdminCtrl', {$scope: $scope});
}]);
In that example my AppCtrl will have the AdminCtrl's $scope.

Angular.js - Two Concurrent But Exclusive Routes ($routeProvider)

<div id="header">
Header
</div>
<div id="wrapper">
<div id="slider" ng-view>
Slider
</div>
<div id="mainWindow" ng-view>
Main window
</div>
</div>
http://jsfiddle.net/abehnaz/k9Y4f/
I am working with a webapp that has two major views. The first is a main window (standard stuff), and the second is a "slider". The slider is more than a slide out menu (sometimes called a "shelf"). It has routing and secondary information associated with it.
In what seems to be the normal Angular paradigm, I would use a single ng-view in the main window, and that would populate the main window with the results of the $routeProvider's view template.
However, here I want to be able to ALSO have the slider have its own route. Angular does not seem to support this. I was thinking that I could potentially hack the functionality in by making a client side URL of the form:
www.app.com/MAINWINDOW/someView1/someView2/SLIDER/someViewa
Am I on the right track with this, or is there another functionality of Angular that I can use?
I would not recommend two routeParameters. Make your sidebar a service and put a controller on your wrapper, slider, and mainWindow. By having the ng-controller on #wrapper as well as the children, you can share data between the controllers (and in turn, inject the service into those controllers. I have done this and it was very successful. See an example below. In my code, my ng-view and routing returned everything within the <div ng-app="myApp">. You could easily put the ng-view in the MainCtrl and trigger routes from the sidebar.
In your template:
<div ng-app="myApp">
<div ng-view>
//Produced by routing
<div id="wrapper" ng-controller="RootCtrl">
<div id="Sidebar" ng-controller="SidebarCtrl">
...
<div ng-click="setUnit(mod.number, unit.number)">Unit 1</div>
...
</div>
<div id="mainWindow" ng-controller="MainCtrl">
</div>
</div>
</div>
</div>
In your javascript:
A service:
myApp.service('questionsSvc', function(Modules) {
var QuestionsSrc = {};
QuestionsSrc.questions = [];
var getQuestionsForModAndUnitFn = function(mod, unit) {
...bunch of code to have it populate QuestionSrc with questions
};
return {
getQuestionsForModAndUnit: getQuestionsForModAndUnitFn,
Questions: QuestionsSrc
};
});
For Controllers:
function RootCtrl($scope) {
$scope.data = {};
}
function SidebarCtrl($scope, Modules, questionsSvc) {
$scope.setUnit = function (mod, unit) {
questionsSvc.getQuestionsForModAndUnit(mod, unit);
$scope.data.questions = questionsSvc.Questions.questions;
//$scope.data.questions was used in the MainCtrl window to do an ng-repeat on the json array of questions bound to this model.
}
};
function MainCtrl($scope){
...whatever you need to manipulate the code...
}
With this example, I am sharing information across Controllers. The magic is with the $scope.data = {}; from the RootCtrl. It allows me to attach questions to the $scope based on an action in the Sidebar (clicking a label) and use that same $scope in the MainCtrl to display the questions in a pretty format. With what I just showed, I did not have to use $routeParameters to pass variables to go to another page (such as module and quiz) but I could have done so as you asked but would have had the sidebar ng-click change routes.
Hope this helps.
One solution is to use Angular UI router:
AngularUI Router is a routing framework for AngularJS, which allows
you to organize the parts of your interface into a state machine.
Unlike the $route service in Angular core, which is organized around
URL routes, UI-Router is organized around states, which may optionally
have routes, as well as other behavior, attached.
States are bound to named, nested and parallel views, allowing you to
powerfully manage your application's interface.
Just to give a flavor for how you could use AngularUI Router- It supports multiple named views (which you can read more about under "Multiple & Named Views" in their docs). So for instance you can use ui-view with names:
<div id="wrapper">
<div id="slider" ui-view="slider">
Slider
</div>
<div id="mainWindow" ui-view="main">
Main window
</div>
</div>
Then within config you can attach states to various routes and specify what each view should display for that state.
myapp.config(function($stateProvider, $urlRouterProvider){
$stateProvider
.state('index', {
url: "/index",
views: {
"Slider": {
templateUrl:"route1.viewA.html"
},
"Main": {
templateUrl:"main1.viewA.html"
}
}
})
.state('slider2', {
url: "/slider2",
views: {
"Slider": {
templateUrl:"route2.viewA.html"
},
"Main": {
templateUrl:"main1.viewA.html"
}
}
})
There's a variety of other ways you could use AngularUI Router though. For instance, you may be able to get away with just using nested routes- which they recommend trying first.
Here's a couple more good reference material you might check out:
http://joelhooks.com/blog/2013/07/22/the-basics-of-using-ui-router-with-angularjs/
http://www.ng-newsletter.com/posts/angular-ui-router.html

Fix race condition with independent AngularJS controller

In my Angular application I have a main view that changes according to the route, and then a sidebar that is displayed no matter what. My HTML looks like this:
<div id="container">
<div id="sidebar" ng-controller="SidebarCtrl">(sidebar code)</div>
<div ng-view id="content">Loading...</div>
</div>
And the javascript is loaded in this order:
Vendor.js (angular, etc)
App.js (my application)
The SidebarCtrl is defined in App.js. However right when Angular loads it sees the ng-controller directive and tries to assign it to the sidebar right off the bat, leading to a "argument SidebarCtrl is not a function" error unless app.js is cached. I was thinking of having the sidebar in a separate file and loading it before Vendor.js, but the sidebar is dependent on some services defined in App.js (namely, my custom auth service):
MyApp.controller('SidebarCtrl',
['$scope', '$location', 'auth', function($scope, $location, auth) {
(...)
}])
So either way I'm at a loss.
I'm sure there is a better way of setting this up...any ideas? Can I define SidebarCtrl as a simple function but still have access to the auth service?
Thanks
You can dynamically add the ng-controller attribute to the sidebar when your app loads:
var $self = angular.element('.sidebar');
$self.attr('ng-controller', window.USER_LOGGED_IN ? 'sidebarCtrl' : 'noUserCtrl');
angular.bootstrap(angular.element('body'));
This method is useful when the controller to be used depends on the application state. In your case, angular.bootstrap should do the trick.
Source

Categories

Resources