I'm getting back into web development a bit after having been kind of out of it for the past 10 years or so, and I'm overwhelmed by all the new technologies that I'm having to catch up with, ASP.NET, MVC, jQuery, SPA, Knockout, etc. I don't know the second thing about jQuery and my experience with ASP.NET is very limited. I have a little familiarity with ASP.NET WebForms, but MVC (and the rest) is totally new to me.
After seeing how many technologies there were, and not knowing which route to explore in my new project, I saw that Hot Towel seems to be a template that combines all the latest stuff into one nice package, so I decided to get the Hot Towel template and start an ASP.NET MVC4 SPA project with it.
Now I'm trying to integrate with our in-house UI framework (which has been developing without me over the past few years). I decided to try to update the Details page in the Hot Towel template to have some content. I added a simple <span>, and all's well and good. But if I try to add what I understand to be a jQuery-widget-based component (?), I get nothing. Even for the simplest test of adding content via jQuery, I get nothing:
<section>
<h2 class="page-title" data-bind="text: title"></h2>
<span>Test this</span>
<div id="testDiv"></div>
<script type="text/javascript">
$("#testDiv").append("Testing");
</script>
</section>
I see the span, but not the modified div. And I can't see any of this content in the source ("View source") or the IE9 console (not surprising given the nature of SPA, but what should I do about it?). And the Visual Studio Page Inspector seems to be totally useless (can't get past the splash screen).
What is the proper method of adding elements to the UI under the HotTowel/jQuery/MVC/SPA/KockoutJS/Breeze/Durandal model? All these new frameworks are driving my crazy.
Edit some more details: The jQuery stuff works fine when I move it to the main page of the SPA, but when I have it on the Details "page" it doesn't work. I suspect it has something to do with the SPA nature of this application and how the content of alternate views are delivered not as an entire page, but as updated content for the main page.
Edit after further investigation, I have discovered the existence of a view model named "detail" which is probably related to this detail view code I have posted. This is the code from the view model:
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});
The script is probably executing but cannot find the div. To correct manipulate div put your jquery code to in a function and trigger that function using attached/compositionComplete callback for duranadal 2.0 or viewAttached callback for durandal 1.x
1.x link - https://github.com/BlueSpire/Durandal/blob/master/docs/1.2/Composition.html.md#view-attached
2.0 link - http://durandaljs.com/documentation/Hooking-Lifecycle-Callbacks/
// in your detail view model, if using durandal 1.x
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title,
viewAttached : function(view){
// view is the root element of your detail view and is passed in
// by durandal
$(view).append("Testing");
}
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});
// in your detail view model, if using durandal 2.0, you have two options
define(['services/logger'], function (logger) {
var title = 'Details';
var vm = {
activate: activate,
title: title,
attached : function(view, parent){
// view is the root element of your detail view
// and is passed in by durandal
$(view).append("Testing first method");
},
compositionComplete: function(view, parent){
// view is the root element of your detail view
// and is passed in by durandal
$(view).append("Testing second method");
}
};
return vm;
//#region Internal Methods
function activate() {
logger.log(title + ' View Activated', null, title, true);
return true;
}
//#endregion
});
Related
I hope its not to harsh to ask not to mince matters.
Here we go:
I have a problem developing a custom Plugin for Shopware 5.
I already have a working plugin which lists orders for certain criteria.
Now I want a Button (which i already have) in the toolbar of this grid-window.
The Button should open the Batch Process Window which is already available in the native "Order" Window of shopware.
Q: How Can I open this app with the selected Ids of my grid?
Heres what I have:
[...]
createToolbarButton: function () {
var me = this;
return Ext.create('Ext.button.Button', {
text: 'Batch Processing Orders',
name: 'customBatchProcessButton',
cls: 'secondary',
handler: function () {
me.onClickCustomBatchProcessButton(me);
}
});
},
onClickCustomBatchProcessButton: function(me){
var thisGrid = me.getTransferGrid();
var records = thisGrid.getSelectionModel().getSelection();
console.log("Grid");
console.log(thisGrid);
console.log("records");
console.log(records);
Shopware.app.Application.addSubApplication({
name: 'Shopware.apps.Order',
action: 'batch',
params: {
mode: 'multi',
records: records
}
});
}
[...]
It always opens the normal view of the order window. (no error in console)
Anybody has a suggestions?
That would be great!
Thanks for your time :)
Greetings
EDIT:
Hey, thank you for your reply so far.
I managed to open the Batch-process-window like this:
me.getView('Shopware.apps.Order.view.batch.Window').create({
orderStatusStore: Ext.create('Shopware.apps.Base.store.OrderStatus').load(),
records: orderRecords,
mode: 'multi'
}).show({});
But now the Problem ist, the Event for the Batch-Process isn't applied on the button on the form...
I am still on try and error.
Many Shopware ExtJS SubApplications can be executed from another app with certain parameters exactly the way you're trying to. Unfortunately I don't see any code in the Order plugin that might lead to the desired result. You can see what actions/params a Shopware SubApplication supports by reading the init function of the main controller -> Shopware.apps.Order.controller.Main
Shopware.apps.Customer.controller.Main from the Customer plugin for example accepts an action like you are using it – it is checking for this:
if (me.subApplication.action && me.subApplication.action.toLowerCase() === 'detail') {
if (me.subApplication.params && me.subApplication.params.customerId) {
//open the customer detail page with the passed customer id
...
In the Order plugin there is similar code, but it just takes an order ID and opens the detail page for the corresponding order. It apparently doesn't feature the batch.Window
You might be able to reuse this class somehow, but that might be a ton of code you need to adapt from the actual Order plugin. If you really need this feature, you can carefully read how the Order plugin is initializing the window and its dependencies and have a try.
I'd rather go for developing a lightweight module in this scenario (It's a frame within a backend window that just uses controllers and template views with PHP/Smarty/HTML)
I have a multiple View setup, a main View and then modules who are opened on button click after certain actions.
Right now the view is reset by me whenever its opened, but what I want to accomplish is, when the view is left, that its gone from the core and its initiated again as if it is opened for the first time.
I am not certain how, and I tried multiple things, none worked as I intended (.destroy(), .removeAllContent(), ... ).
Did I miss a function or is there a way to accomplish that?
Some code:
index.js (how the View is instantiated the first time, its not called a second time)
module.exports.Partner = function(place) {
View = require('./app/js/suche.view');
Bearbeiten = require('./app/js/bearbeiten.view');
var ctrl = View.getController();
ctrl.setPlace(ctrl, place);
ctrl.setEditPlace(ctrl, place);
ctrl.setCreatePlace(ctrl, place);
sap.ui.getCore().byId('suche').placeAt(place);
sap.ui.getCore().byId('suche').byId('searchBtn').attachPress(ctrl.nummerSearch, ctrl);
};
controller (how the View and the index.js is called)
handlePartnerSuche : function(){
this.hideView(); //this is the main view
var p = require('bit-js-business-partner');
var partner_view = p.Partner('content');
p.setCallbackForSchliessen(this, this.callbackForSchliessen);
},
controller (callback function)
callbackForSchliessen: function(){
this.showView();
},
The second View (how its closed)
handleSchliessen : function () {
var p = this.getView('suche');
p.setVisible(false);
this.callbackMethodSchliessen.call(this.callbackCtrlSchliessen);
},
You mentioned you cannot use the Router mechanism due to company restrictions -- am really curious to know what these restriction are then ;-) -- and toggle the visibility properties of the respective views instead.
In that case, I would trigger the OData service in the method where you set the view's visibility to visible, and (re)bind the ODataModel to that view.
From a performance perspective, I would not advise to destroy views from the core
I have created a custom section in umbraco to manage some data in an SQL database.
I can edit items OK but when adding I need to refresh the page to see my new row in the custom tree on the left.
How can I cause a refresh of my custom tree using AngularJS? My tree is called "clients".
I have tried debugging the code and looking at the source to find the event but I can't seem to work out how to do it.
Is there a method I can call on the umbTreeDirective somehow? Or an event to subscribe to?
I am fairly new to AngularJS and am struggling a little.
You're looking for the navigationService.
This line is example of a syncTree call:
navigationService.syncTree({ tree: 'clients', path: content.path, forceReload: false, activate: true });
Here's a contrived, spaghetti promised but full example:
angular.module("umbraco")
.directive('nowplaying', ['navigationService', 'contentResource', 'contentEditingHelper', function (navigationService, contentResource, contentEditingHelper) {
//spaghetti example to create new document
contentResource.getScaffold(parentId, alias)
.then(function (scaffold) {
var myDoc = scaffold;
myDoc.name = name;
//we have minimum to publish
contentResource.publish(myDoc, true, [''])
.then(function (content) {
$scope.newlyCreatedNode = content;
//Sync ('refresh') the tree!
navigationService.syncTree({ tree: 'clients', path: content.path, forceReload: false, activate: true });
});
});
}]);
All of the Belle documentation lives here. -I'm not sure it's actively maintained, i can say for certain that one or two signatures have changed since it was first posted. That aside, it's the best resource i know of to interact with all the umbraco exposed modules and services.
I'm porting a web service into a single-page webapp with Backbone. There is a basic layout consisting on a header, an empty div#content where I'm attaching the views and a footer.
Every route creates the corresponding view and attachtes it to div#content replacing the view that was rendered before with the new one.
I'm using require.js to load the backbone app and it's dependencies.
All Backbone code is pretty small, only one file as I'm only using a router and a view.
This AMD module depends on a util.js file exporting functions that are used in the views.
After a view is created and rendered, It executes the utilities (jquery stuff, ajax, etc) it needs from util.js.
The problem is that when I render a view, it's utilities get called, and when I navigate to another route, and a new view is created, the new view's utilities are called now, but the older view's utilities are still running.
At some point, I have utilities from like five views running altogether, causing conflicts sometimes.
It's clear than my approach is not good enough, as I should have a way to stop/start utilities functions as some kind of services.
I'll paste relevant code that shows my current approach:
require(["utilities"], function(util) {
...
Application.view.template = Backbone.View.extend({
el: "div#content",
initialize: function(){
this.render();
},
render: function(){
var that = this;
// ajax request to html
getTemplate(this.options.template, {
success: function(template) {
var parsedTemplate = _.template( template, that.options.templateOptions || {});
that.$el.html(parsedTemplate);
// execute corresponding utilities
if(that.options.onReady) {
that.options.onReady();
}
},
error: function(template) {
that.$el.html(template);
}
})
}
});
...
Application.router.on('route:requestPayment', function(actions) {
var params = { template: 'request-payment', onReady: util.requestPayment };
var view = new Application.view.template(params);
});
...
});
util.requestPayment consist of a function having all stuff needed to make template work.
I'm confused about how should I handle this issue. I hope I was clear, and any suggestions or help will be appreciated.
EDIT: utilities.js snippet:
...
var textareaCounter = function() {
$('#requestMessage').bind('input propertychange', function() {
var textarea_length = 40 - $(this).val().length;
if(textarea_length === 40 || textarea_length < 0) {
$('#message-counter').addClass('error').removeClass('valid');
$("#submitForm").attr('disabled', 'disabled');
}
else if(textarea_length < 40 && textarea_length > 0) {
$('#message-counter').removeClass('error');
$("#submitForm").removeAttr('disabled');
}
$('#message-counter').text(textarea_length);
});
}
...
var utilities = utilities || {};
...
utilities.requestPayment = function() {
textareaCounter();
initForm();
preventCatching();
requestPaymentCalcFallback();
};
...
return utilities;
...
I would suggest that you should store reference to the currently active view somewhere in your app.
You create a new view here :
var view = new Application.view.template(params);
but you have no access to this variable afterwards. So it exists but you can't stop/delete/get rid of it.
What we normally do is to have a Parent App class which initializes the whole app and manages everything. Your every module in requirejs would be depenedent on it. When a new route is navigating, you ask the Parent App class to change the view. It will delete the old view, create a new one, populate div#content and then store the reference of it.
I think when you delete the old view, all the utilities will stop responding to it.
If you still have the issue with events being called, then you might need to use stopListening event binders before deleting the view reference.
I love KnockoutJS but have been struggling to figure out the best way to build large scale Javascript applications with it.
Right now the way I'm handling the code is by building with a root view model which usually starts at the master page level and then expanding on that. I only ko.applyBindings() on the main view. Here is the example code I have:
var companyNamespace = {};
// Master page. (a.k.a _Layout.cshtml)
(function(masterModule, $, ko, window, document, undefined) {
var private = "test";
masterModule.somePublicMethod = function() {};
masterModule.viewModel = function() {
this.stuff = ko.observable();
};
}(companyNamespace.masterModule = companyNamespace.masterModule || {}, jQuery, ko, window, document));
// Index.cshtml.
(function(subModule, $, ko, window, document, undefined) {
var private = "test";
subModule.somePublicMethod = function() {};
subModule.viewModel = function() {
this.stuff = ko.observable();
};
$(document).ready(function() {
ko.applyBindings(companyNamespace.masterModule);
});
}(companyNamespace.masterModule.subModule = companyNamespace.masterModule.subModule || {}, jQuery, ko, window, document));
I'm just worried since this is a tree structure that if I needed to insert a double master page or something like that, that this would be very cumbersome to re-factor.
Thoughts?
EDIT
I'm aware that you can apply bindings to separate elements to change the scope of the bindings however what if I have nested view models?
I have a rather large knockout.js single page application. (20K+ lines of code currently) that is very easy for anyone to maintain and add additional sections to. I have hundreds of observables and the performance is still great, even on mobile devices like an old iPod touch. It is basically an application that hosts a suite of tools. Here are some insights into the application I use:
1. Only one view model. It keeps things simple IMHO.
The view model handles the basics of any single page application, such as visibility of each page (app), navigation, errors, load and toast dialogs, etc. Example Snippet of View Model: (I separate it out even further js files, but this is to give you an overview of what it looks like)
var vm = {
error:
{
handle: function (error, status)
{
//Handle error for user here
}
},
visibility:
{
set: function (page)
{
//sets visibility for given page
}
},
permissions:
{
permission1: ko.observable(false),
permission2: ko.observable(false)
//if you had page specific permissions, you may consider this global permissions and have a separate permissions section under each app
},
loadDialog:
{
message: ko.observable(''),
show: function (message)
{
//shows a loading dialog to user (set when page starts loading)
},
hide: function()
{
//hides the loading dialog from user (set when page finished loading)
}
},
app1:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
},
app2:
{
visible: ko.observable(false),
load: function ()
{
//load html content, set visibility, app specific stuff here
}
}
}
2. All models go into a separate .js files.
I treat models as classes, so all they really do is store variables and have a few basic formatting functions (I try to keep them simple). Example Model:
//Message Class
function Message {
var self = this;
self.id = ko.observable(data.id);
self.subject = ko.observable(data.subject);
self.body = ko.observable(data.body);
self.from = ko.observable(data.from);
}
3. Keep AJAX database calls in their own js files.
Preferably separated by section or "app". For example, your folder tree may be js/database/ with app1.js and app2.js as js files containing your basic create retrieve, update, and delete functions. Example database call:
vm.getMessagesByUserId = function ()
{
$.ajax({
type: "POST",
url: vm.serviceUrl + "GetMessagesByUserId", //Just a default WCF url
data: {}, //userId is stored on server side, no need to pass in one as that could open up a security vulnerability
contentType: "application/json; charset=utf-8",
dataType: "json",
cache: false,
success: function (data, success, xhr)
{
vm.messaging.sent.messagesLoaded(true);
for (var i = 0; i < data.messages.length; i++)
{
var message = new Message({
id: data.messages[i].id,
subject: data.messages[i].subject,
from: data.messages[i].from,
body: data.messages[i].body
});
vm.messaging.sent.messages.push(message);
}
},
error: function (jqXHR)
{
vm.error.handle(jqXHR.getResponseHeader("error"), jqXHR.status);
}
});
return true;
};
4. Merge and Minify all your model, view model, and database js files into one.
I use the Visual Studio "Web Essentials" extension that allows you to create "bundled" js files. (Select js files, right click on them and go to Web Essentials --> Create Javascript Bundle File) My Bundle file is setup like so:
<?xml version="1.0" encoding="utf-8"?>
<bundle minify="true" runOnBuild="true">
<!--The order of the <file> elements determines the order of them when bundled.-->
<!-- Begin JS Bundling-->
<file>js/header.js</file>
<!-- Models -->
<!-- App1 -->
<file>js/models/app1/class1.js</file>
<file>js/models/app1/class2.js</file>
<!-- App2 -->
<file>js/models/app2/class1.js</file>
<file>js/models/app2/class2.js</file>
<!-- View Models -->
<file>js/viewModel.js</file>
<!-- Database -->
<file>js/database/app1.js</file>
<file>js/database/app2.js</file>
<!-- End JS Bundling -->
<file>js/footer.js</file>
</bundle>
The header.js and footer.js are just a wrapper for the document ready function:
header.js:
//put all views and view models in this
$(document).ready(function()
{
footer.js:
//ends the jquery on document ready function
});
5. Separate your HTML content.
Don't keep one big monstrous html file that is hard to navigate through. You can easily fall into this trap with knockout because of the binding of knockout and the statelessness of the HTTP protocol. However, I use two options for separation depending on whether i view the piece as being accessed by a lot by users or not:
Server-side includes: (just a pointer to another html file. I use this if I feel this piece of the app is used a lot by users, yet I want to keep it separate)
<!-- Begin Messaging -->
<!--#include virtual="Content/messaging.html" -->
<!-- End Messaging -->
You don't want to use server-side includes too much, otherwise the amount of HTML the user will have to load each time they visit the page will become rather large. With that said, this is by far the easiest solution to separate your html, yet keep your knockout binding in place.
Load HTML content async: (I use this if the given piece of the app is used less frequent by users)
I use the jQuery load function to accomplish this:
// #messaging is a div that wraps all the html of the messaging section of the app
$('#messaging').load('Content/messaging.html', function ()
{
ko.applyBindings(vm, $(this)[0]); //grabs any ko bindings from that html page and applies it to our current view model
});
6. Keep the visibility of your pages/apps manageable
Showing and hiding different sections of your knockout.js application can easily go crazy with tons of lines of code that is hard to manage and remember because you are having to set so many different on and off switches. First, I keep each page or app in its own "div" (and in its own html file for separation). Example HTML:
<!-- Begin App 1 -->
<div data-bind="visible: app1.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<div data-bind="visible: app1.section1.visible()">
<!-- A branch off of app1 -->
</div>
<div data-bind="visible: app1.section2.visible()">
<!-- Another branch off of app1 -->
</div>
<!-- End App 1 -->
<!-- Begin App 2 -->
<div data-bind="visible: app2.visible()">
<!-- Main app functionality here (perhaps splash screen, load, or whatever -->
</div>
<!-- End App 2 -->
Second, I would have a visibility function similar to this that sets the visibility for all content on your site: (it also handles my navigation as well in a sub function)
vm.visibility:
{
set: function (page)
{
vm.app1.visible(page === "app1");
vm.app1.section1.visible(page === "app1section1");
vm.app1.section2.visible(page === "app1section2");
vm.app2.visible(page === "app2");
}
};
Then just call the app or page's load function:
<button data-bind="click: app1.load">Load App 1</button>
Which would have this function in it:
vm.visibility.set("app1");
That should cover the basics of a large single page application. There are probably better solutions out there than what I presented, but this isn't a bad way of doing it. Multiple developers can easily work on different sections of the application without conflict with version control and what not.
I like to set up my view models using prototypal inheritance. Like you I have a "master" view model. That view model contains instances of other view models or observable arrays of view models from there you can use the "foreach" and "with" bindings to in your markup. Inside your "foreach" and "with" bindings you can use the $data, $parent, $parents and $root binding contexts to reference your parent view models.
Here are the relevant articles in the KO documentation.
foreach binding
with binding
binding context
If you want I can throw together a fiddle. Let me know.