I'm trying to use Ember to dynamically create child views in a ContainerView.
The problem is those child views need data bindings to a value from an attribute given to the container view.
Here is a bit of code showing roughly what I'm doing:
ReferenceCat.EditorView = Ember.ContainerView.extend({
init: function(){
this._super();
if(this.get('model.type') != undefined){
this.modelTypeChanges();
}
},
modelTypeChanges : function(){
// 1st step: Remove all children of my view
this.removeAllChildren();
var model = this.get('model');
// 2nd step is to run through all of the type information
// generating the views that we need to actually
// fill in this reference
var tI = model.get('typeInfo');
var self = this;
tI.forEach(function(field){
// Now we have a field
switch(field.type){
case "string":
// add new child view here with data binding to data.<field.name>
break;
}
});
}
});
And this class is referenced like this:
{{view ReferenceCat.EditorView
modelBinding=model}}
In your modelTypeChanges function instead of using a switch statement to create the different types of childViews you need to override the ContainerView's createChildView function (http://emberjs.com/api/classes/Ember.ContainerView.html#method_createChildView). The createChildView function itself will return the instantiated childView and in that overidded function itself you can place your switch statement. So it will look something like this...
createChildView: function(view, attrs) {
switch(attr.type) {
case "string":
view = yourview //your view class here, it is overriding the default
break;
}
.....
...
//Once you place all your case statements above
//make sure to call the parents createChildView function.
//This line is actually creating the view you want with the
//properties that are defined in the object attrs
return this._super(view, attrs);
}
So make sure when you call your overridden createChildView function to pass it the object you want bounded in the childView as a property of the object you pass as its second parameter...
var self = this,
tempviewarray = [];
tI.forEach(function(field){
var view = self.createChildView(null, field);
tempviewarray.push(view);
});
// add all the instantiated views to the ContainerView as children views
this.pushObjects(tempviewarray);
Related
I want to double click on any row to getting respective row values. It is an .Razor page.
First of all, I would create a component that will inherit from QuickGrid, so you can manage it easier in the future.
// CustomGrid.razor.cs
[CascadingTypeParameter( nameof(TGridItem) )]
public partial class CustomGrid<TGridItem> : QuickGrid<TGridItem>, IAsyncDisposable
{
[Inject]
private IJSRuntime JS { get; set; } // inject service in order to use JS interop
// sometimes it is mandatory to override base class parameters by your own
}
// CustomGrid.razor
#using Microsoft.AspNetCore.Components.QuickGrid // move it into the _Imports.razor
#typeparam TGridItem // QuickGrid is a generic-typed component
#inherits QuickGrid<TGridItem> // show inheritance
<div #ref="#_gridRef"> // HTML reference of current element
<QuickGrid TGridItem="TGridItem"
Items="#Items"
ItemsProvider="#ItemsProvider"
ChildContent="#ChildContent"
Class="#Class"
// more parameters... >
</QuickGrid>
</div>
Since there is no built-in functionality for adding your custom logic into QuickGrid, you will need to use some JavaScript interopability. Read about it more in docs here and here.
We need to declare some local variables in our CustomGrid.razor.cs:
private string? _rowValue; // specifies row value of currently double clicked row
private ElementReference _gridRef; // HTML element reference object that will be passed to JS function
private IJSObjectReference? _module; // JS module, a file that contains our JS functions
private DotNetObjectReference<CustomGrid<TGridItem>>? _objRef; // .NET object reference that will be passed to JS function in order to use its C# methods
And override some of the component lifecycle methods:
protected override void OnInitialized()
{
_objRef = DotNetObjectReference.Create( this ); // creates .NET object reference of current component instance
}
protected override async Task OnAfterRenderAsync( bool firstRender )
{
if( firstRender )
{
_module = await JS.InvokeAsync<IJSObjectReference>( "import", "./js/customGrid.js" ); // creates a reference of our JS module
if( _module is not null )
{
await _module.InvokeVoidAsync( "initialize", _gridRef, _objRef ); // calls our JS function and passes some arguments
}
}
}
Now, you need to create a JS module and a functions that will add desired logic for you on the first render of the CustomGrid component, like this:
// wwwroot/js/customGrid.js
export function initialize(customGrid, dotNetObj) {
if (customGrid) { // check if custom grid element exists
var rowValue;
const rows = customGrid.querySelectorAll('tbody > tr'); // get all rows except the header row
for (let i = 0; i < rows.length; i++) {
rows[i].addEventListener('dblclick', (e) => { // add event listener to current row in the loop
rowValue = e.path[1].innerText; // get innerText of current row in the loop
console.log(rowValue)
updateCurrentRowValue(rowValue, dotNetObj); // function that will return the current row value and refresh the UI
});
}
}
}
function updateCurrentRowValue(rowValue, dotNetObj) {
dotNetObj.invokeMethodAsync("UpdateCurrentRowValue", rowValue); // C# method
}
We're almost done here! If you would try to perform double click on the row, you would see an error in the console stating that CustomGrid does not contain a public method called UpdateCurrentRowValue. Let's add it like this:
[JSInvokable]
public void UpdateCurrentRowValue( string rowValue )
{
_rowValue = rowValue; // assign received rowValue from the JS function to our local _rowValue variable
StateHasChanged(); // force UI refresh
}
Now, all you need to do is to display your _rowValue:
// CustomGrid.razor
<div #ref="#_gridRef">
<QuickGrid TGridItem="TGridItem" . . . /> // collapsed for brevity
<p>Current Row Value: #_rowValue</p>
</div>
You will also need to Dispose your newly created objects of _module and _objRef using IAsyncDisposable.DisposeAsync method:
// CustomGrid.razor.cs
async ValueTask IAsyncDisposable.DisposeAsync()
{
if( _module is not null )
{
await _module.DisposeAsync();
}
_objRef?.Dispose();
}
Usage:
<CustomGrid Items="#people">
<PropertyColumn Property="#(p => p.PersonId)" Sortable="true" />
<PropertyColumn Property="#(p => p.Name)" Sortable="true" />
<PropertyColumn Property="#(p => p.BirthDate)" Format="yyyy-MM-dd" Sortable="true" />
</CustomGrid>
That should work. If you will need any help -- don't hesitate to ask!
Remarks:
This is a basic implementation of your request. It doesn't support scenarios when there are more than 1 grid on the page. It might work, but will be buggy, I guess. For that, you will need to add some more code in JS and CustomGrid code-behind. I didn't add it because it would be too much code in one answer (quite a lot of code came out here anyway).
UPD-1:
Removed custom [Parameter]s to override QuickGrid's ones and added a comment.
I ran into an interesting issue when defining a global variable (as null in this case), and having a function change that global variable to a callback that was passed to that function---then trying to invoke the global variable (which is now the function) from a click event listener. However if that global variable wasn't defined (var globalCallback; as opposed to var globalCallback = null;) then everything is okay. I was under the assumption that the updated variable reference is always accessible by event listeners regardless of the variable's initial value---this doesn't seem to be the case.
See code below:
TypeError
document.addEventListener("DOMContentLoaded", function(){
//...
theSettingFunction(function(){
//...
});
var globalCallback = null; //creates TypeError when invoked after assigned to function
//var globalCallback = function(){}; //tried this too to test
function theSettingFunction(callback)
{
//...
globalCallback = callback;
//...
}
/* This event listener doesn't need removing it's a core UI element
This event gets triggered only after theSettingFunction() has been invoked first */
document.querySelector('#myButtonDiv').addEventListener('click', function(){
//...
globalCallback(); //invoking sees globalCallback as null still = TypeError
});
//...
});
Everything Okay
document.addEventListener("DOMContentLoaded", function(){
//...
theSettingFunction(function(){
//...
});
var globalCallback;
function theSettingFunction(callback)
{
//...
globalCallback = callback;
//...
}
/* This event listener doesn't need removing it's a core UI element
This event gets triggered only after theSettingFunction() has been invoked first */
document.querySelector('#myButtonDiv').addEventListener('click', function(){
//...
globalCallback(); //invoking... hey I see you---you're a function! Invoked.
});
//...
});
Is this because of the way the JS engine optimizes? Why else are event listeners not getting the updated references to global variables when they're defined?
No, this is not an engine optimization. It's an effect of JavaScript's funny hoisting rules, and the order in which the various things appear in your code.
The JavaScript language specifies that certain declarations are "hoisted" to the beginning of the program. I guess the idea was to make programming easier by not forcing programmers to pay attention to the order of things -- except, as it turns out, automatic reordering only gets you so far...
For example, you can call a function before defining it. Let's look at an example:
func(); // No error, because "func" is defined later.
variable = 1; // No error, because "variable" is declared later.
function func() {}
var variable;
console.log(variable); // Will print "1".
Under the hood, this is implemented by the engine reordering things as follows:
function func() {}
var variable;
func();
variable = 1;
console.log(variable); // Will print "1".
(Again, this is not an optimization, it's a requirement of the JavaScript language.)
An additional detail is that a combined declaration+initialization of a variable is split into two steps, and only the declaration is hoisted. So this:
console.log(variable); // Prints "undefined".
var variable = 1;
after internal re-ordering becomes this:
var variable;
console.log(variable); // Prints "undefined".
variable = 1;
Now, if we simplify your code a bit, we can see that this is exactly what happens. By inlining theSettingFunction, and assuming that the body of the onclick-handler is invoked directly, we get:
// This is what `theSettingFunction` does:
globalCallback = function() { /*...*/ }
var globalCallback = null;
// This is what the click-handler does:
globalCallback();
which the engine is required to reorder to:
var globalCallback; // hoisted declaration
globalCallback = function() { /*...*/ }
globalCallback = null;
globalCallback(); // TypeError, of course!
So you can easily fix the problem by moving the declaration+initialization of globalCallback before its first assignment, i.e. the call site of theSettingFunction:
document.addEventListener("DOMContentLoaded", function(){
var globalCallback = null; // Declare and initialize...
//...
theSettingFunction(function(){ // ...before assigning.
//...
});
function theSettingFunction(callback)
{
//...
globalCallback = callback;
//...
}
document.querySelector('#myButtonDiv').addEventListener('click', function(){
//...
globalCallback();
});
//...
});
Arguably, this will also make your code easier to read, because it's easier for humans to understand in which order things are happening when they don't have to take any invisible reordering into account. For this reason, personally, I would also move the definition of theSettingFunction before its first call.
Hi guys looking for some basic advice.
I have four models: BoardViewModel, List, Card, Member
var Member = function (id, name, avatar) {
var self = this;
self.id = id;
self.name = name;
self.avatar = avatar;
self.isChecked = ko.observable(false);
};
I am instantiating members property inside BoardViewModel. But I want to use a copy of this model inside each Card model to instantiate a list of assigned members.
Each card stores comma separated list of member references like
",1,2,4,5"
I am writing a loop to BoardViewModel.members and mark members as checked if id references match bore I assign it as Card.members.
The last piece of the puzzle I am missing is reference to the BoardViwModel.members.
I have a lovely example fiddler that would somewhat help to build a picture of what I am talking about.
Just bear in mind that once I have this working properly I want to replace view() binding
foreach: $root.members
with
foreach: members
If at all possible I would like to avoid passing BoardViewModel.members as parameter into List and then into Card.
Update 1
As suggested by #Jeroen here's a simplified version of my fiddler.
The top view() model which encompases a concept of lists:
var BoardViewModel = function (lists, members) {
var self = this;
// in reality members are fetched via ajax call to the server
// and instantiate as a ko.observableArray()
self.groupMembers = ko.observableArray(members);
self.lists = ko.observableArray(lists);
...
}
In reality this has a signature like this:
var boardViewModel = function (initialData)
moving on.
The child List model which encompases a concept of cards:
var List = function (id, name, cards, sortOrder, groupMembers) {
var self = this;
self.cards = ko.observableArray(cards);
...
}
in reality:
var list = function (item, groupMembers)
nothing special there really.
The child Card model which encompases the concept of card items (but lets not go there yet):
var Card = function (id, title, description, contentItemId, members, groupMembers) {
var self = this;
self.members = ko.observableArray(parseMembers(members));
// now remember each card has a members property
// which stores comma separated list ",1,4"
function (members) {
var memberList = groupMembers;
var memberRefList = members.split(',');
ko.utils.arrayForEach(memberList, function(member){
ko.utils.arrayForEach(memberRefList, function(memberId){
if(member.id === meberId) {
member.isChecked(true);
}
});
});
return memberList;
}
...
}
in reality:
var card = function (item, groupMembers)
nothing too fancy there either.
I currently have something like this working on my dev environment.
Problem:
Those with keen eyes probably noticed the way I was passing groupMembers all the way up. I am not particularly hyped about the idea.
Anyone know a better way of implementing this? i.e. why can't I just do something like
var memberList = self.parent.parent.groupMembers;
for instance.
As per me, the better way to do is to have the child viewmodels inside the parent view-model. like this where you can access the parent data members as well as methods directly.
ViewModel
var BoardViewModel = function(){
var self = this;
this.members = ko.observableArray();
this.lists = ko.observableArray();
// Child View Models
this.Member = function(member){
this.id = member.id;
this.name = member.name;
this.avatar = member.avatar;
this.isChecked = ko.observable(false);
}
this.List = function(list){
// same for this
};
this.Card = function(card){
// same for this
};
// a Method to bind the data with the observables and arrays
// Assuming data is a json object having Members, List objects
this.applyData = function(data){
self.members(jQuery.map(data.Members, function(item){
return new self.Member(item);
}));
self.lists(jQuery.map(data.Lists, function(item){
return new self.List(item);
}));
}
}
onDom ready
// json object holding your data
var data = {
"Members" : [
],
"Lists" : [
],
"Cards" : [
]
};
var vm = new BoardViewModel();
$(function(){
ko.applyBindings(vm, document.getElementById('myModule'));
vm.applyData(data);
});
I am using Knockout 3.2 and the new component system. I am trying to have components that include sub-components.
Home Page (component - with HomePageViewModel)
NewsFeed1 (component with HomePageViewModel.NewsFeedViewModel1)
NewsFeed2 (component with HomePageViewModel.NewsFeedViewModel2)
HomePageViewModel
var viewModel = (function () {
function viewModel() {
this.message = ko.observable("Welcome to DKT!");
this.newsFeedViewModel = new gr.viewModel();
this.newsFeedViewModel2 = new gr.viewModel();
this.newsFeedViewModel.message("Message 1");
this.newsFeedViewModel2.message("Message 2");
}
return viewModel;
})();
NewsFeedViewModel
var viewModel = (function () {
function viewModel() {
this.message = ko.observable("This is the profile!");
}
return viewModel;
})();
As you can see the HomePageViewModel contains both the NewsFeedViewModel. I now want to be able to use these as the DataContext/BindingContext of my two components but this does not seem to work.
Home.html
<news-feed data-bind="newsFeedViewModel"></news-feed>
<news-feed data-bind="newsFeedViewModel2"></news-feed>
Both these components do not use the ViewModels from the HomePageViewModel but uses a new NewsFeedViewModel. How can I make the datacontext of both these components bind to the viewModels stored in the top component (home)?
Generally, you would want to supply your component with any data via params. For example, with your structure, you could create the component like:
ko.components.register("news-feed", {
viewModel: function (params) {
this.vm = params.vm;
},
template: "<h2>News Feed</h2><div data-bind=\"text: vm.message\"></div>"
});
Then, you would define the elements like:
<news-feed params="vm: newsFeedViewModel"></news-feed>
<news-feed params="vm: newsFeedViewModel2"></news-feed>
You could choose to pass the message in directly for each and/or choose whatever names make sense for your params (rather than vm).
Sample: http://jsfiddle.net/rniemeyer/fssXE/
I want the list of all style properties/values applied to a UIComponent's selector via css but I can't seem to find a list of them anywhere.
For example, I have a BorderContainer and the CSS gives it
backgroundColor: #869ca7;
backgroundAlpha: .5;
and from an ActionScript function I would like to retrieve the list {backgroundColor:#869ca7, backgroundAlpha:.5}. But in an abstract way that works for all UIComponents (i.e. I can't just call getStyle("backgroundColor");
I've tried two ways and I feel very close but can't actually retrieve the list.
It feels like I should be able to get the list of properties from the UIComponents by using the styleDeclaration property on the UIComponent, but it doesn't seem to show the list of style properties it has.
It also seems like I should be able to get the values by calling "uiComponent.getStyle(_)" but that requires that I already know the property names.
Thank you for any insight you can help me with.
For reference, the CSSStyleDeclaration class:
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/styles/CSSStyleDeclaration.html
So my initial research shows that there is no direct function call or list for retrieving an array of style properties.
I think the only way is to check the cascading arrays for the property name.
Code for getStyle for reference:
public function getStyle(styleProp:String):*
{
var o:*;
var v:*;
// First look in the overrides, in case setStyle()
// has been called on this CSSStyleDeclaration.
if (overrides)
{
// If the property exists in our overrides, but
// has 'undefined' as its value, it has been
// cleared from this stylesheet so return
// undefined.
if (styleProp in overrides &&
overrides[styleProp] === undefined)
return undefined;
v = overrides[styleProp];
if (v !== undefined) // must use !==
return v;
}
// Next look in the style object that this CSSStyleDeclaration's
// factory function produces; it contains styles that
// were specified in an instance tag of an MXML component
// (if this CSSStyleDeclaration is attached to a UIComponent).
if (factory != null)
{
factory.prototype = {};
o = new factory();
v = o[styleProp];
if (v !== undefined) // must use !==
return v;
}
// Next look in the style object that this CSSStyleDeclaration's
// defaultFactory function produces; it contains styles that
// were specified on the root tag of an MXML component.
if (defaultFactory != null)
{
defaultFactory.prototype = {};
o = new defaultFactory();
v = o[styleProp];
if (v !== undefined) // must use !==
return v;
}
// Return undefined if the style isn't specified
// in any of these three places.
return undefined;
}