Using AngularJS "copy()" to avoid reference issues - javascript

I'm displaying a list of items, each of which has an "edit"-button next to it. A click on that opens an angular ui modal window and the user can change some properties of the specific item.
Now, what bugged me was that when typing in this edit-window, the specific item in the list of items reflected the changes immediatly. I only wanted it to update when the user clicked 'ok' in the modal, and to not change at all if the user chose 'cancel'.
My workaround uses copy to make a, well, copy of the chosen item that then serves as model for the view:
var modalInstance = $modal.open({
templateUrl: 'scripts/app/views/editBond.html',
controller: function ($scope, $modalInstance, bond) {
$scope.bond = angular.copy(bond);
$scope.ok = function () {
$modalInstance.close($scope.bond);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
},
resolve: {
bond: function () {
return bond;
}
}
});
Is using angular.copy() appropriate to avoid such issues? Is this a scope issue at all?

Yep, using angular.copy() is absolutely appropriate here. If you want something more advanced you might want to checkout angular-history

Related

Angular Bootstrap Modal binding issue

I'm working with Angular Bootstrap and actually I'm trying to update correctly my model using a Modal.
Here is the very simple code:
controller:
function open(room) {
var roomModal = $uibModal.open({
templateUrl: 'room-modal.html',
controller: 'RoomModalController',
controllerAs: 'modal',
resolve: {
room: room
}
});
roomModal.result.then(function (response) {
RoomsService.update({
roomId: response._id
}, response).$promise (etc...);
});
}
Modal Controller:
var vm = this;
vm.room = room;
vm.save = function () {
$uibModalInstance.close(vm.room);
};
vm.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
Basically I'm resolving the Room to get a few information about it and then if needed I wish to update a few information about the room within the modal.
It is working fine unless I do not want to update some information and I click "close".
What happen is: if I updated a few information and then I click "close" the information has not been updated on the database (OK) but has been updated in the main view... Because Angular bind the Modal information to the main view...
It is quite weird because I'm passing those information to a separate scope (vm) and unless I do not click save I should not expect this behavior...
What I'm doing wrong here?!?
In your RoomModalController deep copy the room object to prevent when updating that the model is also updated.
vm.room = angular.copy(room);
Now this object will take care of the modal binding, and will not interfere when changed to your root scope vm.room object.
To finalize saving this data, you have to save the vm.root modal object to your database, and also update the root scope vm.room object according these changes made in the modal.

Shared variables and multiple controllers AngularJS

I have multiple controllers on a small app I'm writing, and I have successfully shared a 'selected' variable between the controllers like so.
app.service('selectedEmployee', function () {
var selected = null;
return
{
getSelected: function() {
return selected;
},
postSelected: function(employee) {
selected = employee;
}
};
});
I have a side nav bar with a list of employees. When I click on an employee I call the postSelected function then the getSelected to set $scope.selected.
$scope.selectEmployee = function(employee) {
//Calling Service function postSelected
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
I have a third controller for my main content area, and this is where I don't understand what to do. I want information from the selected employee to be displayed, but angular is compiling the whole page before the first employee has a chance to get set as selected, and subsequent selections of an employee aren't reloading the main content page (because I haven't told them to I think). Here's my main content controller:
app.controller('mainContentController', ['$scope','selectedEmployee',
function ($scope, selectedEmployee) {
$scope.selected = selectedEmployee.getSelected();
console.log($scope.selected);
}
]);
My main content view is very simple right now
<h2>{{selected.firstName}}{{selected.lastName}}</h2>
My question is how I can tell one controller to effectively update its partial view so that when I select an employee it displays information.
GitLab repo
Don't rely on messy broadcasts if your goal is simply to display & modify the data in the controller's template.
Your controllers do NOT need to "know" when the Service or Factory has updated in order to use it in the template as Angular will handle this for you, as you access the data via dot notation. This is the important concept which you should read more about.
This Fiddle shows both ways of accessing the data, and how using the container object in the template causes Angular to re-check the same actual object on changes - instead of the primitive string value stored in the controller:
http://jsfiddle.net/a01f39Lw/2/
Template:
<div ng-controller="Ctrl1 as c1">
<input ng-model="c1.Bands.favorite" placeholder="Favorite band?">
</div>
<div ng-controller="Ctrl2 as c2">
<input ng-model="c2.Bands.favorite" placeholder="Favorite band?">
</div>
JS:
var app = angular.module("app", []);
app.factory('Bands', function($http) {
return {
favorite: ''
};
});
app.controller('Ctrl1', function Ctrl1(Bands){
this.Bands = Bands;
});
app.controller('Ctrl2', function Ctrl2(Bands){
this.Bands = Bands;
});
First of all lets start by good practices, then solve your problem here...
Good Practices
At least by my knowledge, i dont intend to use services the way you do... you see, services are more like objects. so if i were to convert your service to the way i normally use it would produce the following:
app.service('selectedEmployee', [selectedEmployeeService])
function selectedEmployeeService(){
this.selected = null;
this.getSelected = function(){
return this.selected;
}
this.postSelected = function(emp){
this.selected = emp;
}
}
You see there i put the function seperately, and also made the service an actual object.. i would reccomend you format your controller function argument like this... If you want to disuss/see good practices go here. Anways enough about the good practices now to the real problem.
Solving the problem
Ok The Andrew actually figured this out!! The problem was:that he need to broadcast his message using $rootScope:
$rootScope.$broadcast('selected:updated', $scope.selected);
And then you have to check when $scope.selected is updated.. kinda like $scope.$watch...
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
After that it autmoatically updates and works! Hope this helped!
PS: Did not know he anwsered already...
So after much research and a lot of really great help from Dsafds, I was able to use $rootScope.$broadcast to notify my partial view of a change to a variable.
If you broadcast from the rootScope it will reach every child controller and you don't have to set a $watch on the service variable.
$scope.selectEmployee = function(employee) {
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
$rootScope.$broadcast('selected:updated', $scope.selected);
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
And in the controller of the main content area
function ($scope) {
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
}
I don't think you have to pass the data directly, you could also just as easily call selectedEmployee.getSelected()
$rootScope also has to be included in the Parent controller and the broadcasting controller.

Best way to toggle Booleans between $rootScope and and a controller

I have a navbar directive which sits above ng-view. It utilises the $rootScope to trigger events to show buttons in certain views.
I am trying to add a button to the directive template which will switch a boolean in a controller for a particular view. The view shows a period of time and each period has a particular boolean that I want to switch from the directive.
The boolean value is saved in a local storage object which is initialized when each iteration of this particular view is loaded.
First, the value needs to be communicated to the directive so the button can display as being set to true or false. When the switch is toggled, the value of that boolean needs to make its way from the directive, through the $rootScope, to the controller and then be saved in the storage object.
When the view is changed, the whole process needs to repeat. The switch needs to be able to be switched on and off multiple times, obviously.
At present, I am emitting the value from the controller to the $rootScope and then listening for that value in the directive link function.
However, what is the best way to get that $rootScope value BACK into the controller. I tried setting up a $rootScope.$watch in the controller which appeared to work on any single page but when navigating between different time periods, the $rootScope value of the boolean was not resetting properly.
I tried resetting the value in the controller initialization as follows:
$rootScope.booleanValue = false;
but this didn't work.
I have also tried the following:
$scope.$on('$routeChangeSuccess', function (next, current) {
$rootScope.booleanValue = false;
});
but I can't get the whole cycle to work properly. It still seems as though the value of the property in the $rootScope is not resetting from the view before and is then carrying over when an adjacent pay period view is loaded.
I hope this makes sense. I will save you from too much code as I think the basic idea is here.
What you are trying to do is share state from your navbar directive (an isolate scope) and your view's controller. I recommend you use a factory provider service to share that state:
angular.module('myApp').factory('navbarState', function (){
return {started: false}
});
In your navbar directive, inject the service and store the state in that service:
angular.module('myApp').directive('navigationBar', [
'$rootScope',
'navbarState',
//'NavigationStackService',
//'NavigationBarService',
function ($rootScope, navbarState) {
function link(scope, element) {
scope.startEditMode = function(){
console.log("Edit clicked");
navbarState.started=true;
//NavigationBarService.hideNavigationEdit();
//NavigationBarService.showNavigationDone();
};
scope.finishEditMode = function(){
console.log("Done clicked");
navbarState.started=false;
//NavigationBarService.hideNavigationDone();
//NavigationBarService.showNavigationEdit();
};
}
return {
templateUrl: 'templates/navigation-bar.html',
restrict: 'E',
scope: {},
link: link
};
}
]);
In your view controller, retrieve the service, put it on the controller's scope, and use it in your template.
angular.module('myApp').controller('controller2', function(navbarState) {
console.log("view controller2 started");
var vm = this;
vm.navState = navbarState;
vm.message = "hello from ct2";
});
The DEMO on JSFiddle.

Angular: how to make a "search" take you to another route and display results?

I have a main page with a nav, and each nav option takes you to another route. It all looks like a single page app, but each "page" has it's own route and controller.
My problem is that I want to put a search box in the navbar. When someone uses the searchbox, I want to take the user to the "search" route and then display the results. I'm having a lot of trouble figuring out these two issues:
Where do I store this "searchbox" logic? E.g. when someone searches, they choose the type of search from a dropdown, then the search query in the inputbox. I have special logic to automatically choose which dropdown value based on the value typed in the inputbox.
How do I redirect to the
"search" route and display the results based on the input from the
previous page?
It's probably clear I'm a newby to Angular. I'm happy to work out the details, but I'm mainly looking to understand how to structure the solution to this problem. Thanks in advance for your help.
What I love about Angular the most is the amount of options you can apply.
Your goal can be reached either by using a service. A service is a singleton class which you can request from controllers. Being a singleton what ever value you store in the service is available to all controllers. You can than either $watch for value change, use $broadcast to notify data change or use $routeParams to send data with route change.
A service is built as follows :
The following assume you have a global module var named 'app'
app.service('myService', function(){
var myValue;
this.getMyValue = function(){
return myValue;
};
this.setMyValue = function(value){
myValue = value;
};
});
Then you request a service from a controller like you request an angular service such as $scope.
app.controller('myController', ['$scope', 'myServce', function($scope, myService){
$scope.myValue = myService.getMyValue();
//Example watch
$scope.$watch('myValue',function(){
//Search criteria changed!!
}, true);
}]);
Angular is terrific..have fun coding
Basically you would want an own state for your search page, so this is where we begin (I expect you to use the ui-router and not Angulars built in router):
.state('search', {
url: "/search",
templateUrl: "pages/search.html",
controller: 'SearchController as ctrl',
params: { searchString: {} }
})
As you can see, I've defined an additional parameter for the search string that is not part of the URL. Of course, if you like, you could change that and move the parameter to the URL instead:
.state('search', {
url: "/search/:searchString",
templateUrl: "pages/search.html",
controller: 'SearchController as ctrl'
})
The actual search input is pretty straight forward as well, because it's only HTML:
<input type="text" ng-model="searchString" on-key-enter="ctrl.goSearch(searchString)">
The function for the state change has to be placed in the controller for the primary template (e.g. the controller of your navigation bar if the search is located there):
var vm = this;
vm.goSearch = goSearch;
function goSearch(searchString) {
$state.go('main.search', { searchString: searchString });
}
Of interest is also the on-key-enter directive that I've added:
angular.module('your.module')
.directive('onKeyEnter', OnKeyEnter);
function OnKeyEnter() {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
scope.$apply(function (){
scope.$eval(attrs.onKeyEnter);
});
event.preventDefault();
}
});
};
}
On pressing the enter-key, it will call the function you supply as attribute value. Of course you could also use a button with ng-click instead of this directive, but I think it simply looks better.
Last, but not least, you need a Search Controller and a HTML template for your search page, which I won't give to you, as it is up to you what you display here. For the controller, you only need to know how you can access the search string:
angular.module('your.module')
.controller('SearchController', SearchController);
SearchController.$inject = ['$scope', '$stateParams'];
function SearchController($scope, $stateParams) {
$scope.searchString = $stateParams.searchString;
/* DO THE SEARCH LOGIC, e.g. database lookup */
}
Hope this helps to find the proper way. :)

How do I get the Back Button to work with an AngularJS ui-router state machine?

I have implemented an angularjs single page application using ui-router.
Originally I identified each state using a distinct url however this made for unfriendly, GUID packed urls.
So I have now defined my site as a much simpler state-machine. The states are not identified by urls but are simply transitioned to as required, like this:
Define Nested States
angular
.module 'app', ['ui.router']
.config ($stateProvider) ->
$stateProvider
.state 'main',
templateUrl: 'main.html'
controller: 'mainCtrl'
params: ['locationId']
.state 'folder',
templateUrl: 'folder.html'
parent: 'main'
controller: 'folderCtrl'
resolve:
folder:(apiService) -> apiService.get '#base/folder/#locationId'
Transition to a Defined State
#The ui-sref attrib transitions to the 'folder' state
a(ui-sref="folder({locationId:'{{folder.Id}}'})")
| {{ folder.Name }}
This system works very well and I love its clean syntax. However, as I am not using urls the back button does not work.
How do I keep my neat ui-router state-machine but enable the back button functionality?
Note
The answers that suggest using variations of $window.history.back() have all missed a crucial part of the question: How to restore the application's state to the correct state-location as the history jumps (back/forward/refresh). With that in mind; please, read on.
Yes, it is possible to have the browser back/forward (history) and refresh whilst running a pure ui-router state-machine but it takes a bit of doing.
You need several components:
Unique URLs. The browser only enables the back/forward buttons when you change urls, so you must generate a unique url per visited state. These urls need not contain any state information though.
A Session Service. Each generated url is correlated to a particular state so you need a way to store your url-state pairs so that you can retrieve the state information after your angular app has been restarted by back / forward or refresh clicks.
A State History. A simple dictionary of ui-router states keyed by unique url. If you can rely on HTML5 then you can use the HTML5 History API, but if, like me, you can't then you can implement it yourself in a few lines of code (see below).
A Location Service. Finally, you need to be able manage both ui-router state changes, triggered internally by your code, and normal browser url changes typically triggered by the user clicking browser buttons or typing stuff into the browser bar. This can all get a bit tricky because it is easy to get confused about what triggered what.
Here is my implementation of each of these requirements. I have bundled everything up into three services:
The Session Service
class SessionService
setStorage:(key, value) ->
json = if value is undefined then null else JSON.stringify value
sessionStorage.setItem key, json
getStorage:(key)->
JSON.parse sessionStorage.getItem key
clear: ->
#setStorage(key, null) for key of sessionStorage
stateHistory:(value=null) ->
#accessor 'stateHistory', value
# other properties goes here
accessor:(name, value)->
return #getStorage name unless value?
#setStorage name, value
angular
.module 'app.Services'
.service 'sessionService', SessionService
This is a wrapper for the javascript sessionStorage object. I have cut it down for clarity here. For a full explanation of this please see: How do I handle page refreshing with an AngularJS Single Page Application
The State History Service
class StateHistoryService
#$inject:['sessionService']
constructor:(#sessionService) ->
set:(key, state)->
history = #sessionService.stateHistory() ? {}
history[key] = state
#sessionService.stateHistory history
get:(key)->
#sessionService.stateHistory()?[key]
angular
.module 'app.Services'
.service 'stateHistoryService', StateHistoryService
The StateHistoryService looks after the storage and retrieval of historical states keyed by generated, unique urls. It is really just a convenience wrapper for a dictionary style object.
The State Location Service
class StateLocationService
preventCall:[]
#$inject:['$location','$state', 'stateHistoryService']
constructor:(#location, #state, #stateHistoryService) ->
locationChange: ->
return if #preventCall.pop('locationChange')?
entry = #stateHistoryService.get #location.url()
return unless entry?
#preventCall.push 'stateChange'
#state.go entry.name, entry.params, {location:false}
stateChange: ->
return if #preventCall.pop('stateChange')?
entry = {name: #state.current.name, params: #state.params}
#generate your site specific, unique url here
url = "/#{#state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
#stateHistoryService.set url, entry
#preventCall.push 'locationChange'
#location.url url
angular
.module 'app.Services'
.service 'stateLocationService', StateLocationService
The StateLocationService handles two events:
locationChange. This is called when the browsers location is changed, typically when the back/forward/refresh button is pressed or when the app first starts or when the user types in a url. If a state for the current location.url exists in the StateHistoryService then it is used to restore the state via ui-router's $state.go.
stateChange. This is called when you move state internally. The current state's name and params are stored in the StateHistoryService keyed by a generated url. This generated url can be anything you want, it may or may not identify the state in a human readable way. In my case I am using a state param plus a randomly generated sequence of digits derived from a guid (see foot for the guid generator snippet). The generated url is displayed in the browser bar and, crucially, added to the browser's internal history stack using #location.url url. Its adding the url to the browser's history stack that enables the forward / back buttons.
The big problem with this technique is that calling #location.url url in the stateChange method will trigger the $locationChangeSuccess event and so call the locationChange method. Equally calling the #state.go from locationChange will trigger the $stateChangeSuccess event and so the stateChange method. This gets very confusing and messes up the browser history no end.
The solution is very simple. You can see the preventCall array being used as a stack (pop and push). Each time one of the methods is called it prevents the other method being called one-time-only. This technique does not interfere with the correct triggering of the $ events and keeps everything straight.
Now all we need to do is call the HistoryService methods at the appropriate time in the state transition life-cycle. This is done in the AngularJS Apps .run method, like this:
Angular app.run
angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->
$rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
stateLocationService.stateChange()
$rootScope.$on '$locationChangeSuccess', ->
stateLocationService.locationChange()
Generate a Guid
Math.guid = ->
s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
"#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"
With all this in place, the forward / back buttons and the refresh button all work as expected.
app.run(['$window', '$rootScope',
function ($window , $rootScope) {
$rootScope.goBack = function(){
$window.history.back();
}
}]);
Back
After testing different proposals, I found that the easiest way is often the best.
If you use angular ui-router and that you need a button to go back best is this:
<button onclick="history.back()">Back</button>
or
<a onclick="history.back()>Back</a>
// Warning don't set the href or the path will be broken.
Explanation:
Suppose a standard management application.
Search object -> View object -> Edit object
Using the angular solutions
From this state :
Search -> View -> Edit
To :
Search -> View
Well that's what we wanted except if now you click the browser back button you'll be there again :
Search -> View -> Edit
And that is not logical
However using the simple solution
<a onclick="history.back()"> Back </a>
from :
Search -> View -> Edit
after click on button :
Search -> View
after click on browser back button :
Search
Consistency is respected. :-)
If you are looking for the simplest "back" button, then you could set up a directive like so:
.directive('back', function factory($window) {
return {
restrict : 'E',
replace : true,
transclude : true,
templateUrl: 'wherever your template is located',
link: function (scope, element, attrs) {
scope.navBack = function() {
$window.history.back();
};
}
};
});
Keep in mind this is a fairly unintelligent "back" button because it is using the browser's history. If you include it on your landing page, it will send a user back to any url they came from prior to landing on yours.
browser's back/forward button solution
I encountered the same problem and I solved it using the popstate event from the $window object and ui-router's $state object. A popstate event is dispatched to the window every time the active history entry changes.
The $stateChangeSuccess and $locationChangeSuccess events are not triggered on browser's button click even though the address bar indicates the new location.
So, assuming you've navigated from states main to folder to main again, when you hit back on the browser, you should be back to the folder route. The path is updated but the view is not and still displays whatever you have on main. try this:
angular
.module 'app', ['ui.router']
.run($state, $window) {
$window.onpopstate = function(event) {
var stateName = $state.current.name,
pathname = $window.location.pathname.split('/')[1],
routeParams = {}; // i.e.- $state.params
console.log($state.current.name, pathname); // 'main', 'folder'
if ($state.current.name.indexOf(pathname) === -1) {
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(
$state.current.name,
routeParams,
{reload:true, notify: false}
);
}
};
}
back/forward buttons should work smoothly after that.
note: check browser compatibility for window.onpopstate() to be sure
Can be solved using a simple directive "go-back-history", this one is also closing window in case of no previous history.
Directive usage
<a data-go-back-history>Previous State</a>
Angular directive declaration
.directive('goBackHistory', ['$window', function ($window) {
return {
restrict: 'A',
link: function (scope, elm, attrs) {
elm.on('click', function ($event) {
$event.stopPropagation();
if ($window.history.length) {
$window.history.back();
} else {
$window.close();
}
});
}
};
}])
Note: Working using ui-router or not.
The Back button wasn't working for me as well, but I figured out that the problem was that I had html content inside my main page, in the ui-view element.
i.e.
<div ui-view>
<h1> Hey Kids! </h1>
<!-- More content -->
</div>
So I moved the content into a new .html file, and marked it as a template in the .js file with the routes.
i.e.
.state("parent.mystuff", {
url: "/mystuff",
controller: 'myStuffCtrl',
templateUrl: "myStuff.html"
})
history.back() and switch to previous state often give effect not that you want. For example, if you have form with tabs and each tab has own state, this just switched previous tab selected, not return from form. In case nested states, you usually need so think about witch of parent states you want to rollback.
This directive solves problem
angular.module('app', ['ui-router-back'])
<span ui-back='defaultState'> Go back </span>
It returns to state, that was active before button has displayed. Optional defaultState is state name that used when no previous state in memory. Also it restores scroll position
Code
class UiBackData {
fromStateName: string;
fromParams: any;
fromStateScroll: number;
}
interface IRootScope1 extends ng.IScope {
uiBackData: UiBackData;
}
class UiBackDirective implements ng.IDirective {
uiBackDataSave: UiBackData;
constructor(private $state: angular.ui.IStateService,
private $rootScope: IRootScope1,
private $timeout: ng.ITimeoutService) {
}
link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);
function parseStateRef(ref, current) {
var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
if (preparsed) ref = current + '(' + preparsed[1] + ')';
parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
if (!parsed || parsed.length !== 4)
throw new Error("Invalid state ref '" + ref + "'");
let paramExpr = parsed[3] || null;
let copy = angular.copy(scope.$eval(paramExpr));
return { state: parsed[1], paramExpr: copy };
}
element.on('click', (e) => {
e.preventDefault();
if (this.uiBackDataSave.fromStateName)
this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
.then(state => {
// Override ui-router autoscroll
this.$timeout(() => {
$(window).scrollTop(this.uiBackDataSave.fromStateScroll);
}, 500, false);
});
else {
var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
this.$state.go(r.state, r.paramExpr);
}
});
};
public static factory(): ng.IDirectiveFactory {
const directive = ($state, $rootScope, $timeout) =>
new UiBackDirective($state, $rootScope, $timeout);
directive.$inject = ['$state', '$rootScope', '$timeout'];
return directive;
}
}
angular.module('ui-router-back')
.directive('uiBack', UiBackDirective.factory())
.run(['$rootScope',
($rootScope: IRootScope1) => {
$rootScope.$on('$stateChangeSuccess',
(event, toState, toParams, fromState, fromParams) => {
if ($rootScope.uiBackData == null)
$rootScope.uiBackData = new UiBackData();
$rootScope.uiBackData.fromStateName = fromState.name;
$rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
$rootScope.uiBackData.fromParams = fromParams;
});
}]);

Categories

Resources