Angularize SharePoint Client Side Taxonomy Picker - angularjs

The Patterns and Practices team has released a client side taxonomy picker for use when integrating with SharePoint. It works well, but uses jQuery and my SharePoint App is built in Angular... which seems to be a growing trend. I would like to leverage the client side taxonomy picker in Angular and was unsure of how best to achieve this. Here is a link to the component: https://github.com/OfficeDev/PnP/tree/dev/Components/Core.TaxonomyPicker
I am thinking it would be a directive, or is there a non-directive manner to replace (aka, how does Angular manage a replace/initialization) as they are doing here:
HTML:
<input type="hidden" id="taxPickerGeography" />
jQuery Function that gets the Current Context and creates the Taxonomy Picker
$(document).ready(function () {
var context;
context = SP.ClientContext.get_current();
$('#taxPickerGeography').taxpicker({
isMulti: false,
allowFillIn: false,
termSetId: '89206cf2-bfe9-4613-9575-2ff5444d1999'
}, context);
});
I don't need the script loading components as illustrated in the example provided by the PnP team, as I have these already embedded in my App.

Given the challenges of making a "responsive" Managed Metadata field, I built the following using the JavaScript Object Model to retrieve terms and then push them for use in an Array. This includes retrieving Synonyms.
// Query Term Store and get terms for use in Managed Metadata picker stored in an array named "termsArray".
var termsArray = [];
function execOperation() {
// Current Context
var context = SP.ClientContext.get_current();
// Current Taxonomy Session
var taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
// Term Stores
var termStores = taxSession.get_termStores();
// Name of the Term Store from which to get the Terms. Note, that if you receive the following error "Specified argument was out of the range of valid values. Parameter name: index", you may need to check the term store name under Term Store Management to ensure it was not changed by Microsoft
var termStore = termStores.getByName("TermStoreName");
// GUID of Term Set from which to get the Terms
var termSet = termStore.getTermSet("TermSetGUIDHere");
var terms = termSet.getAllTerms();
context.load(terms);
context.executeQueryAsync(function () {
var termEnumerator = terms.getEnumerator();
while (termEnumerator.moveNext()) {
var currentTerm = termEnumerator.get_current();
var guid = currentTerm.get_id();
var guidString = guid.toString();
var termLabel = currentTerm.get_name();
// Get labels (synonyms) for each term and push values to array
getLabels(guid, guidString, termLabel);
}
// Set $scope to terms array
$scope.$apply(function () {
$scope.termsArray = termsArray;
});
}, function (sender, args) {
console.log(args.get_message());
});
// Get labels (synonyms) for each term and push values to array
function getLabels(termguid, guidString, termLabel) {
var clientContext = SP.ClientContext.get_current();
var taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext);
var termStores = taxSession.get_termStores();
// The name of the term store. Note, that if you receive the following error "Specified argument was out of the range of valid values. Parameter name: index", you may need to check the term store name under Term Store Management to ensure it was not changed by Microsoft
var termStore = termStores.getByName("TermStoreName");
// GUID of Term Set from which to get the Terms
var termSet = termStore.getTermSet("TermSetGUIDHere");
var term = termSet.getTerm(termguid);
var labelColl = term.getAllLabels(1033);
clientContext.load(labelColl);
clientContext.executeQueryAsync(function () {
var labelEnumerator = labelColl.getEnumerator();
var synonyms = "";
while (labelEnumerator.moveNext()) {
var label = labelEnumerator.get_current();
var value = label.get_value();
synonyms += value + " | ";
}
termsArray.push({
termName: termLabel,
termGUID: guidString,
termSynonyms: synonyms
});
}, function (sender, args) {
console.log(args.get_message());
});
}
};
// Execute function
execOperation();

Related

Highlight text as you type in textarea Reactjs

I need to perform a behavior in FrontEnd but I don't know how to do it: Inside the textarea I have to put a background on certain keywords like "+project", "#context", while the user types, as if it were a markup text similar to testing tools for Regex.
Its not the complete solution, but you can adapt this example:
https://jsfiddle.net/julmot/hdyLpy37/
It uses the markjs library:
https://markjs.io/
Here is the javascript code:
// Create an instance of mark.js and pass an argument containing
// the DOM object of the context (where to search for matches)
var markInstance = new Mark(document.querySelector(".context"));
// Cache DOM elements
var keywordInput = document.querySelector("input[name='keyword']");
var optionInputs = document.querySelectorAll("input[name='opt[]']");
function performMark() {
// Read the keyword
var keyword = keywordInput.value;
// Determine selected options
var options = {};
[].forEach.call(optionInputs, function(opt) {
options[opt.value] = opt.checked;
});
// Remove previous marked elements and mark
// the new keyword inside the context
markInstance.unmark({
done: function(){
markInstance.mark(keyword, options);
}
});
};
// Listen to input and option changes
keywordInput.addEventListener("input", performMark);
for (var i = 0; i < optionInputs.length; i++) {
optionInputs[i].addEventListener("change", performMark);
}

Routing in SAPUI5: How to implement passing of URL? Model data not initialy loaded

My goal is to write a SAPUI5 Fiori app with routing support. One mail goal is to have passable URLs. For example in an E-Mail like "please approve this: link". The link is an URL matched by my rounting config, e.g.index.html#/applicants/8.
I use a typical sap.m.SplitApp kind of application. Clicking a list item in masterview changes the URL to index.html#/applicants/[id of entry in JSON]. I can click on the list, my defined routes are getting matched and the apps loads the (applicant) data as expected.
However, and here comes my question, this doeas not work when using an URL directly, say pasting [my url]/index.html#/applicants/8 into my browser. The app is launched but no detail data is loaded. I have to click on another list item again to get the data.
Actually, the controller is called when passing the URL, but it seems the model is not initiated and undefined. My JSON model is bound in the createContent function of my Component.js
// Update 2015-05-14
The problems seems to be around the getData() function. I have the model, it has the entries, but getData() returns undefined for the first time my app is loaded. I recently read getData() is deprecated. How should I improve my coding below?
// Component.js
ui5testing.Component.prototype.createContent = function(){
// create root view
var oView = sap.ui.view({
id : "app",
viewName : "ui5testing.view.Main",
type : "JS",
viewData : {
component : this
}
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oView.setModel(oModel);
[...]
return oView;
});
// Master controller
handleApplicantSelect : function (evt) {
var oHashChanger = sap.ui.core.routing.HashChanger.getInstance();
var context = evt.getParameter("listItem").getBindingContext();
var path = context.getPath();
var model = this.getView().getModel();
var item = model.getProperty(path);
oHashChanger.setHash("applicants/" + item.id);
},
// Detail controller
onInit: function() {
this.router = sap.ui.core.UIComponent.getRouterFor(this);
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},
_handleRouteMatched : function(evt){
var objectId = evt.getParameter("arguments").id;
var model = this.getView().getModel();
var data = model.getData()["applicants"];
var pathId;
if (data) {
for (var i = 0; data.length; i++) {
if ( objectId == data[i].id ) {
pathId = i;
break;
}
}
var sPath = "/applicants/" + pathId;
var context = new sap.ui.model.Context(model, sPath)
this.getView().setBindingContext(context);
}
},
As you've figured out that getData() returns undefined for the first time, which means the model data is still not yet loaded. So you can make use of attachRequestCompleted method of the model & fire an event from the component & listen to that event in the detail controller to ensure the routerPatternMatched() gets executed only after the data is loaded.
//Component.js
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oModel.attachRequestCompleted(jQuery.proxy(function(){
this.fireEvent("MockDataLoaded"); // fireEvent through component
},this));
oView.setModel(oModel);
//Detail controller
onInit : function(){
this.router = sap.ui.core.UIComponent.getRouterFor(this);
var oComponent = this.getOwnerComponent();
oComponent.attachEvent("MockDataLoaded",jQuery.proxy(function(){
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},this));
}
Or the simplest & but the dirty way would be to make an synchronous request instead of an async request to load data.
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData(""model/mock_applicants.json",{bAsync:false});
oView.setModel(oModel);

get nested object length in an angularFire array in Firebase

I have a Firebase structure like this:
user {
uid {
Lessons {
lid1 {
Title: ...
}
lid2 {
Title: ...
}
}
}
}
I want to use AngularFire to convert user as array so I can filter them using Angular like this:
var usersRef = new Firebase($rootScope.baseUrl + "users");
var userListfb = $firebase(usersRef).$asArray();
The problem is, I also need the number of child of the Lessons object. When I log the userListfb, it is an array. But inside the array, the Lessons node still an object. I can not user length to get its length. What is the correct way to find out the number of child of the Lessons Node with Firebase AngularFire?
Edit 1
According to Frank solution, I got an infinite loop (digest circle error from AngularJS).
The problem is, I will not know the "uid" key. I need to loop it in the first array to get the uid into the second firebaseArray.
Let's say I have a ng-repeat="user in users" in the view and call this on view level in each repeat:
{{getLessonLength(user.uid)}}
Then in the controller, I have this function:
$scope.users = $firebaseArray($scope.usersRef);
$scope.getLessonLength = function (uid) {
var userRef = $rootScope.baseUrl + "users/" + uid + "/lessons/";
var lessonsNode = $firebaseArray(new Firebase(userRef));
return lessonsNode.length;
}
}
And it throw this error: Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
All I want it is something like var lessonsCount = snapshot.child('lessons').numChildren() in regular Firebase .on('child_added' ...), the numChildren() function in FirebaseArray. Please help!
AngularFire contains quite some code to ensure that an ordered collection in your Firebase maps correctly to a JavaScript array as Angular (and you) expect it.
If you have a reference to a specific user, you can just create a new sync ($firebase) and call $asArray on that.
var usersRef = new Firebase($rootScope.baseUrl + "users");
var userListfb = $firebase(usersRef).$asArray();
var uid1LessonsRef = userRef.child('uid1').child('Lessons');
var uid1LessonsArray = $firebase(uid1LessonsRef).$asArray();
uid1LessonsArray.$loaded().then(function(arr) {
console.log('Loaded lessons, count: '+arr.length);
});
The data will only be synchronized once, no matter how many references you create to it.

Is it good practice to manage view instantiation in a router?

So this is my first Backbone project and I'm wondering if I'm doing things in the best way. My app basically has two states, one of them displays a search box and the other displays a search box with a table under it. My router has routes for searching and for the initial landing page with just the search view. When the user types in the query the router navigates to the search route and the table view is added to the page. This is my router:
app.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search: function(address, age, rad){
app.statusView || (app.statusView = new app.StatusView());
app.searchView || (app.searchView = new app.SearchView());
app.trigger('status:loading');
app.Practices.fetch({
reset: false,
success: function() {
app.searchView.setElement($('#search-box')).render();
var searchQuery = new app.SearchQueryModel({age: age, coords: address.split(","), radius: rad});
if (!app.tableView){
app.tableView = new app.TableView({model: searchQuery});
} else {
app.tableView.model = searchQuery;
app.tableView.refresh();
};
}
});
app.trigger('status:clear');
},
index: function() {
app.statusView = new app.StatusView();
app.searchView = new app.SearchView();
app.footerView = new app.FooterView();
app.searchView.setElement($('#search-box')).render();
}
});
As you can see my views are instantiated in the index route and then the same views are used when you search, unless the user is going directly to the search page in which case the views are instantiated there. I'd be surprised if this wasn't very sub-optimal because it seems clumsy to be checking if the view already exists in the search route. Is there a better way of doing things?
Lets say its not bad, but there is one better approach.
As for now you router is in charge of hook-up URL with app astatus and also for view and model control. The second may be detached from Router, so you will need Controller abstraction, but Backbone does not provide Controller "from the box".
But this is not the problem, you can use plugin or take a look at Controller realization in Marionette.js
The main idea here is to split responsibilities between app part correctly:
1) Router - keeps routes and hook up URL with controller action
2) Controller - manage views and models (create, delete, fetch and so on)
3) View - listen to model and DOM events and render data
4) Model - provide actual data and work with data.
First of all welcome to Backbone. It is a lovely framework which can allow you to make things as beautiful or ugly as you'ld like. Your question is about where view instantiation should be, in terms of good practices. Of course it seems sort of wrong to do it there as it violates the Law of Demeter by handling both url routing and view instantiation.
But the views have to be run from somewhere right? If not the router then where?
So I have two responses:
If your app is simple and you just want to play with backbone then you're probably going to be fine. A lot of people let single page app frameworks complicate otherwise simple apps. I'm not trying to be lazy, but where you have it now is the natural beginner's choice in Backbone. If this is your case then stop here.
If you want to use the full power of backbone to custom make a framework then read on.
So my setup is designed to be able to start a new project using some boilerplate functions and create only a few classes which are specific to the new app. Route handling and all of that kind of thing seems low-level enough to me that it should be just part of some configuration that I don't want to look at often. The upshot is that my router looks like this:
define([
'autorouter'
], function(AutoRouter){
var AppRouter = AutoRouter.extend({
autoRoutes: {
":page" : "routeDirect",
":page/:object" : "routeDirect",
":page/:object/:action" : "routeDirect",
"": "routeDirect"
}
});
return AppRouter;
});
Then for each new project I have a file where I keep the non-default routes, for instance:
define(function(require){
return {
"schedule" : require('screens/schedule')
, "logout" : require('screens/logout')
, "login" : require('screens/login')
, "create" : require('screens/create')
, "upload" : require('screens/upload')
, "select" : require('screens/selection')
, "inventory" : require('screens/inventory')
, "describe" : require('screens/description')
}
});
I put each screen into it's own file (using requirejs for the multi-file dependency management). The extra variables get passed through to the screen.
Each screen is the brain for a particular user experience and is responsible for loading views and maybe handling some events while that screen is active.
If that seems like an interesting setup then here is how I did it:
For the router itself I use a boilerplate class which I borrowed from Derick Bailey with some slight modifications:
define([
'jquery', 'underscore', 'backbone'],
function($, _, Backbone) {
var AutoRouter = Backbone.Router.extend({
constructor: function(options){
Backbone.Router.prototype.constructor.call(this, options);
var that = this;
that.app = options.app;
if (this.autoRoutes){
that.processAutoRoutes(options.app, that.autoRoutes);
}
},
processAutoRoutes: function(app, autoRoutes){
var method, methodName;
var route, routesLength;
var routes = [];
var router = this;
for(route in autoRoutes){
routes.unshift([route, autoRoutes[route]]);
}
routesLength = routes.length;
for (var i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = app[methodName];
router.route(route, methodName, method);
}
}
});
return AutoRouter;
});
I never have to look at it, but I do need to pass it an app instance. For example:
this.appRouter = new AppRouter({app : this});
Finally my route direction function:
define(function(require){
var pathParser = function(path){
return Array.prototype.slice.call(path);
}
var pathApply = function(path, routes, context){
var pathArray = pathParser(path);
var primary = pathArray[0];
if (routes.hasOwnProperty(primary)){
routes[primary].apply(context, pathArray.slice(1));
} else {
routes["default"].apply(context, pathArray.slice(1));
}
}
return function(path){
//NOTE PLEASE that this references AutoRouter
//Which has an app property
var oApp = this.app;
var pathRoutes = _.extend(require('urls'), {
"default" : require('screens/default')
});
pathApply(arguments, pathRoutes, oApp);
};
});
So, did I make things better? Well if you're doing something very simple with just a screen or two, then you certainly don't want to build this sort of setup from scratch. But if you're like me, and you want to be able to quickly produce new projects then having some boilerplate like the two classes above allows for one JSON object to tell the app which routes I should send to which screens. Then I can have all of the logic in the appropriate places, allowing separation of concerns. Which is why I think Backbone is so pleasant.
My understanding of your problem is that you are triggering a route each time you are hitting search.
If this is how you are doing it, then use view events hash (used to capture and handle events that happen in a view) for search.Don't use routes. Define an events hash in the view and have a callback to handle the search.
var myAppEventBus = _.extend({},Backbone.Events);
var myAppController = {
function : search(options) {
// create an instance of the collection and do a fetch call passing the
// search parameters to it.
var searchResultsCollection = new SearchResultsCollection();
// pass search criteria, the success and error callbacks to the fetch
// method.
var that = this;
searchResultsCollection.fetch(
{
data:that.options,
success : function() {
// Pass the fetched collection object in the trigger call so that
// it can be
// received at the event handler call back
var options = {
"searchResultsCollection" : that.searchResultsCollection;
};
myAppEventBus.trigger("search_event_triggered",options);
},
error : function() {
// do the error handling here.
}
}
);
}
};
// Application Router.
var MyAppRouter = Backbone.Router.extend({
routes : {
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search : function(searchParams) {
// Fetch the query parameters and pass it to the view.
var routeSearchExists = false;
var searchOptions = {};
var options = {};
if(searchParams) {
routeSearchExists = true;
// If search params exist split and set them accordingly in
// the searchOptions object.
options.searchOptions = searchOptions;
}
// Create and render the search view. Pass the searchOptions
var searchView = new SearchView(options);
searchView.render();
// Create and render an instance of the search results view.
var searchResultsView = new SearchResultsView();
searchResultsView.render();
// If there are search parameters from the route, then do a search.
if(routeSearchExists) {
searchView.search();
}
}
});
// The main view that contains the search component and a container(eg: div)
// for the search results.
var SearchView = Backbone.View.extend({
el : "#root_container",
searchOptions : null,
initialize : function(options) {
// Intialize data required for rendering the view here.
// When the user searches for data thru routes, it comes down in the
// options hash which can then be passed on to the controller.
if(options.searchOptions) {
this.searchOptions = options.searchOptions;
}
},
events : {
"search #search_lnk":"initSearch"
},
initSearch : function(event) {
event.preventDefault();
var searchOptions = {};
// Fetch the search fields from the form and build the search options.
myAppController.search(searchOptions);
},
search : function() {
if(this.searchOptions) {
myAppController.search(searchOptions);
}
}
});
// The view to display the search results.
var SearchResultsView = Backbone.View.extend({
searchResultsCollection : null;
initialize : function(options) {
// Handling the triggered search event.
myAppEventBus.on("search_event_triggered",this.render,this);
},
render : function(options) {
//search results collection is passed as a property in options object.
if(options.searchResultsCollection)
//Render your view.
else
// Do it the default way of rendering.
}
});
SearchView is the root view that contains the search component and a container like div to hold the search results.
SearchResultsView displays the result of a search.
When search option is clicked, the event callback (initSearch) gets the entered search data.
The search method on myAppController object is invoked and the search query is passed.
An instance of the search collection is created and fetch is invoked passing it the search query and also the success and error callback.
On success, a custom backbone event is triggered along with the fetched collection.
The callback(render method in SearchResultsView) for this event is invoked.
The callback renders the results of the search.
When loading in the router an instance for both the views can be created(the results view will be empty) and attached to the dom.
If you wish to search by multiple query strings at the url then I would suggest you to use the following route.
search?*queryString.
In the route callback make a call to a utility function the splits the querystring and returns you a search object and pass on the search string to the view.

How to define a Backbone.Collection to be a property but not an attribute of a Backbone.Model?

Say I have a web application which allows to filter a set of items and say that I'm using backbone.js in the web frontend.
Quite naturally I ended up creating a Filter which extends Backbone.Model and a SearchResultList extending Backbone.Collection. The Filter has some attributes like searchTerm, dataFrom, dateTo... Filter also has a method called applyFilter. Filter.applyFilter should call searchResultList.fetch, thus updating/filtering the search results.
The question is, how to best initialize searchResultList as a property of Filter instead of being an attribute in Backbone terms?
I would not like to create the SearchResultList in Filter.initialize as SearchResultList is not necessarily "owned" by Filter.
For now I ended up passing the searchResultList object as an option to Filter.initialize, but that feels a little bit awkward to me.
I doubt that it is a good idea to let Filter.applyFilter call SearchResultList.fetch. However, there needs to be a method of Filter which when called triggers a new request somehow. (Listening to change events of Filter is not an option either as the user is supposed to change filter options in multiple steps and decides to apply the filter manually via a button click)
I have a Collection for performing searches. Most of my collections extend this Search Collection.
It has an object in it holding parameters for searching. The user of the collection can call setSearchParam, and I have a method buildSearchString that will build the search part of the URL to call (I use the Matrix parameters in my REST urls).
window.SearchCollection = Backbone.Collection.extend({
initialize : function() {
this.params = {}; // holds parameters for searching
},
// Set a search param
setSearchParam: function(param, value) {
if (typeof value === 'undefined') return;
if (typeof this.params === 'undefined') this.params = {};
this.params[param] = value;
},
setSearchParams : function(paramMap) {
_.extend(this.params, paramMap);
},
// Build Matrix params for search. This could just as easily be a standard URL string
buildSearchString: function() {
var search = [];
var i = 0;
for (var key in this.params) {
var value = this.params[key];
if (utils.isEmpty(value)) continue;
search[i++] = ";";
search[i++] = key;
search[i++] = "=";
search[i++] = value;
}
return search.join('');
}
});
Then when I create a collection, I extend it:
window.BookingCollection = window.SearchCollection.extend({
model: Booking,
url: function(){
var urlString = BASE_REST_URL + "bookings/";
// Build the matrix params for the REST URL
urlString += this.buildSearchString();
return urlString;
}
});
Then, a user of this collection can use it like so:
var bookings = new BookingCollection();
bookings.setSearchParam("name", "hello");

Resources