knockout.js - add/remove items from child list of model - model-view-controller

I have a foreach template displaying a list of jobs on a page. Inside each job I have a list of job notes. I'm having trouble setting up the proper observable to allow for the job notes to be updated in the view.
Here is a link to a jsFiddle: http://jsfiddle.net/b3B8p/
Or, here is the code as it stands. I'm not sure whether to put the observable array of the sublist into the Job object itself or into the View...
<div data-bind="foreach: jobs">
<div>
<div class="jobContainer">
<label data-bind="text: JobTitle"></label>
<label data-bind="text: CompanyName"></label>
<div class="jobDetails" data-bind="visible: expanded">
<label data-bind="text: City"></label>
<label data-bind="text: State"></label>
<div data-bind="foreach: Notes">
<label data-bind="text: Text"></label>
</div>
</div>
<div>
Expand
Add Note
</div>
</div>
function JobNote() {
this.Text = "some text!";
}
function Job() {
this.JobTitle = ko.observable("some job");
this.CompanyName = ko.observable("some company");
this.City = ko.observable("some city");
this.State = ko.observable("some state");
this.Notes = ko.observableArray([
new JobNote(),
new JobNote(),
]);
this.someValue = ko.observable().extend({ defaultIfNull: "some default" });
this.expanded = ko.observable(false);
this.linkLabel = ko.computed(function () {
return this.expanded() ? "collapse" : "expand";
}, this);
};
var viewModel = function () {
this.jobs= ko.observableArray([
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
new Job(),
]);
this.toggle = function (item) {
item.expanded(!item.expanded());
}
this.addNote = function(job) {
// what should go in here?
}
};
ko.applyBindings(new viewModel());​

As Niko said in this scenario I would add the method to the class. Like so.
function Job() {
var self = this;
this.JobTitle = ko.observable("some job");
this.CompanyName = ko.observable("some company");
this.City = ko.observable("some city");
this.State = ko.observable("some state");
this.Notes = ko.observableArray([
new JobNote(),
new JobNote(),
]);
this.someValue = ko.observable().extend({ defaultIfNull: "some default" });
this.expanded = ko.observable(false);
this.linkLabel = ko.computed(function () {
return this.expanded() ? "collapse" : "expand";
}, this);
this.toggle = function () {
self.expanded(!self.expanded());
};
this.addNote = function () {
self.Notes.push(new JobNote());
}
};
This keeps everything nice and encapsulated.
Hope this helps.

Related

Pass the model of a view to controller's action through the MVC binding

I have a file upload controller working as a backend to an EditorTemplate that holds a Kendo Core MVC Upload control.
The plan is to make this EditorControl work standalone, but I do have to pass a record Id (a Guid) to it to maintain reference.
Controller:
public class UploadController : BaseControllerWithAuth<UploadController>
{
private readonly IWebHostEnvironment hostEnvironment;
public UploadController(IWebHostEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccess, IUserService userService) : base(httpContextAccess, userService) => hostEnvironment = hostingEnvironment;
public async Task<ActionResult> ChunkSave([Bind(Prefix = "IdCode.letterFiles")] IEnumerable<IFormFile>? letterFiles, string? metaData, Guid? idCode)
{
try
{
if (metaData == null)
return await Save(letterFiles);
var chunkData = JsonSerializer.Deserialize<ChunkMetaDataModel>(metaData)!;
if (letterFiles != null)
{
foreach (var file in letterFiles) AppendToFile(Path.Combine(hostEnvironment.WebRootPath, Constants.FileUploadPath, chunkData!.FileName), file, idCode?.ToString());
}
var fileBlob = new FileResultModel
{
uploaded = chunkData!.TotalChunks - 1 <= chunkData.ChunkIndex,
fileUid = chunkData.UploadUid
};
return Json(fileBlob);
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(UploadController)} => {nameof(ChunkSave)}: Error: {ex.Message}");
throw;
}
}
public ActionResult Remove(string[]? fileNames)
{
try
{
if (fileNames == null) return Content("");
foreach (var fullName in fileNames)
{
var fileName = Path.GetFileName(fullName);
var physicalPath = Path.Combine(hostEnvironment.WebRootPath, Constants.FileUploadPath, fileName);
if (System.IO.File.Exists(physicalPath))
{
System.IO.File.Delete(physicalPath);
}
}
return Content("");
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(UploadController)} => {nameof(Remove)}: Error: {ex.Message}");
throw;
}
}
public void AppendToFile(string fullPath, IFormFile content, string? idCode)
{
try
{
var basePath = Path.Combine(hostEnvironment.WebRootPath, Constants.FileUploadPath);
if (!Directory.Exists(basePath)) Directory.CreateDirectory(basePath);
var letterPath = Path.Combine(basePath, idCode!);
if (!Directory.Exists(letterPath)) Directory.CreateDirectory(letterPath);
using var stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
content.CopyTo(stream);
}
catch (IOException ex)
{
Logger.LogError(ex, $"{nameof(UploadController)} => {nameof(AppendToFile)}: Error: {ex.Message}");
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(UploadController)} => {nameof(AppendToFile)}: Error: {ex.Message}");
throw;
}
}
public async Task<ActionResult> Save([Bind(Prefix = "IdCode.letterFiles")] IEnumerable<IFormFile>? letterFiles)
{
try
{
if (letterFiles == null) return Content("");
foreach (var file in letterFiles)
{
var fileContent = ContentDispositionHeaderValue.Parse(file.ContentDisposition);
var fileName = Path.GetFileName(fileContent.FileName!.Trim('"'));
var physicalPath = Path.Combine(hostEnvironment.WebRootPath, Constants.FileUploadPath, fileName);
await using var fileStream = new FileStream(physicalPath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
return Content("");
}
catch (Exception ex)
{
Logger.LogError(ex, $"{nameof(UploadController)} => {nameof(Save)}: Error: {ex.Message}");
throw;
}
}
}
EditorTemplate:
#model Guid?
#{
string[] extensions = { ".jpg", ".png", ".pdf", ".ppt", ".pptx", ".doc", ".docx", ".xls", ".xlsx" };
}
#Html.HiddenFor(m => m)
<kendo-upload name="letterFiles" multiple="true">
<async save-url="#Url.Action("ChunkSave","Upload")"
remove-url="#Url.Action("Remove","Upload")"
auto-upload="true"
chunk-size="11000" />
<validation allowed-extensions="#extensions" max-file-size="36700160" />
</kendo-upload>
Note the [Bind(Prefix = "IdCode.letterFiles")] in the Controller's Actions. I had to figure that out because of the Upload control being in an EditorTemplate. Also, on the parent View, this is how the EditorTemplate is placed:
#using (Html.BeginForm("", "Letter", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(m => m.IdCode)
<div class="panel mt-20px" data-sortable-id="ui-widget-16">
<div class="panel-heading bg-da-blue text-white">
<h4 class="panel-title">RL Info</h4>
</div>
<div class="panel-body">
#Html.EditorFor(m => m, "Letter")
</div>
</div>
<div class="panel mt-20px" data-sortable-id="ui-widget-16">
<div class="panel-heading bg-da-blue text-white">
<h4 class="panel-title">Attachments</h4>
</div>
<div class="panel-body">
#Html.EditorFor(m => m.IdCode, "LetterAttachmentsManage")
</div>
</div>
<div class="row mt-3">
<div class="col-md-1">
<button type="submit" class="btn btn-primary w-100 me-5px" formaction="CreateSave" title="#(Model.IsUpdateCase ? "Update letter" : "Save letter")">#(Model.IsUpdateCase ? "Update" : "Save")</button>
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-default w-100" formaction="CreateSubmit" title="#(Model.IsUpdateCase ? "Update letter & submit" : "Save letter & submit")">Submit</button>
</div>
</div>
}
I am in a situation where I trigger the View passing the Guid. Once the view is rendered as partial, I can upload a file(s) through the Kendo control, and it posts to the ChunkSave method on the controller. I need to pass the value of the View's model too.
I thought that by simple adding a same-name and type parameter: Guid? idCode to the ChunkSave action on the controller would be enough but apparently not though.
I modified my Upload controls as:
<kendo-upload name="letterFiles" multiple="true"
on-upload="letterFilesOnUpload" on-remove="letterFilesOnRemove" on-complete="letterFilesOnComplete"
on-success="letterFilesOnSuccess" on-error="letterFilesOnError" on-cancel="letterFilesOnCancel">
<async save-url="#Url.Action(nameof(UploadController.ChunkSave), ControllerExtensions.Nameof<UploadController>())"
remove-url="#Url.Action(nameof(UploadController.Remove), ControllerExtensions.Nameof<UploadController>())"
auto-upload="true" chunk-size="2097152" concurrent="true" />
<validation allowed-extensions="#extensions" max-file-size="36700160"/>
</kendo-upload>
And here is how handled the events while passing the custom data, where needed:
<script language="javascript" type="text/javascript">
function letterFilesOnUpload(e) {
$('#IsBusy').val('True');
e.data = {
letterGuid: $('#LetterGuid').val()
};
}
function letterFilesOnRemove(e) {
$('#IsBusy').val('True');
e.data = {
letterGuid: $('#LetterGuid').val()
};
}
</script>
And on the controller side, I slightly modified the signature of the methods by adding the Guid? letterGuid:
public class UploadController : BaseControllerWithAuth<UploadController>
{
...
public async Task<ActionResult> ChunkSave(IEnumerable<IFormFile>? letterFiles, string? metaData, Guid? letterGuid)
{
...
}
public ActionResult Remove(string[]? fileNames, Guid? letterGuid)
{
...
}
}

Implement Aria 1.1

I am trying to implement the example 1 provided by w3c.org. The URL is https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html. It keeps giving me aria undefined error on line (var ex1Combobox = new aria.ListboxCombobox). Here is the code:
<!DOCTYPE html>
<html>
<body>
<label for="ex1-input"
id="ex1-label"
class="combobox-label">
Choice 1 Fruit or Vegetable
</label>
<div class="combobox-wrapper">
<div role="combobox"
aria-expanded="false"
aria-owns="ex1-listbox"
aria-haspopup="listbox"
id="ex1-combobox">
<input type="text"
aria-autocomplete="list"
aria-controls="ex1-listbox"
id="ex1-input">
</div>
<ul aria-labelledby="ex1-label"
role="listbox"
id="ex1-listbox"
class="listbox hidden">
</ul>
</div>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* ARIA Combobox Examples
*/
var FRUITS_AND_VEGGIES = [
'Apple',
'Artichoke',
'Asparagus',
'Banana',
'Beets',
'Bell pepper',
'Broccoli',
'Brussels sprout',
'Cabbage',
'Carrot',
'Cauliflower',
'Celery',
'Chard',
'Chicory',
'Corn',
'Cucumber',
'Daikon',
'Date',
'Edamame',
'Eggplant',
'Elderberry',
'Fennel',
'Fig',
'Garlic',
'Grape',
'Honeydew melon',
'Iceberg lettuce',
'Jerusalem artichoke',
'Kale',
'Kiwi',
'Leek',
'Lemon',
'Mango',
'Mangosteen',
'Melon',
'Mushroom',
'Nectarine',
'Okra',
'Olive',
'Onion',
'Orange',
'Parship',
'Pea',
'Pear',
'Pineapple',
'Potato',
'Pumpkin',
'Quince',
'Radish',
'Rhubarb',
'Shallot',
'Spinach',
'Squash',
'Strawberry',
'Sweet potato',
'Tomato',
'Turnip',
'Ugli fruit',
'Victoria plum',
'Watercress',
'Watermelon',
'Yam',
'Zucchini'
];
function searchVeggies (searchString) {
var results = [];
for (var i = 0; i < FRUITS_AND_VEGGIES.length; i++) {
var veggie = FRUITS_AND_VEGGIES[i].toLowerCase();
if (veggie.indexOf(searchString.toLowerCase()) === 0) {
results.push(FRUITS_AND_VEGGIES[i]);
}
}
return results;
}
/**
* #function onload
* #desc Initialize the combobox examples once the page has loaded
*/
window.addEventListener('load', function () {
var ex1Combobox = new aria.ListboxCombobox(
document.getElementById('ex1-combobox'),
document.getElementById('ex1-input'),
document.getElementById('ex1-listbox'),
searchVeggies,
false
);
var ex2Combobox = new aria.ListboxCombobox(
document.getElementById('ex2-combobox'),
document.getElementById('ex2-input'),
document.getElementById('ex2-listbox'),
searchVeggies,
true
);
var ex3Combobox = new aria.ListboxCombobox(
document.getElementById('ex3-combobox'),
document.getElementById('ex3-input'),
document.getElementById('ex3-listbox'),
searchVeggies,
true,
function () {
// on show
document.getElementById('ex3-combobox-arrow')
.setAttribute('aria-label', 'Hide vegetable options');
},
function () {
// on hide
document.getElementById('ex3-combobox-arrow')
.setAttribute('aria-label', 'Show vegetable options');
}
);
document.getElementById('ex3-combobox-arrow').addEventListener(
'click',
function () {
if (ex3Combobox.shown) {
document.getElementById('ex3-input').focus();
ex3Combobox.hideListbox();
}
else {
document.getElementById('ex3-input').focus();
ex3Combobox.updateResults(true);
}
}
);
});
</script>
</body>
</html>
Any help would be appreciated.
I realize that this is an old post, but I have determined why the undefined error is occurring:
There are two js files associated with this example:
ListBox-Combox.js &
ListBox-Combo-example.js
The 'ListBox-Combo-example.js' file has event listeners for all three examples on the page
https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html.
Since you only copied the code for the first example, when the javascript attempts to find the combobox 'ex2-Combobox' it cannot find it so javascript throws an error.
You can comment out these lines in the 'ListBox-Combo-example.js' file:
);
var ex2Combobox = new aria.ListboxCombobox(
document.getElementById('ex2-combobox'),
document.getElementById('ex2-input'),
document.getElementById('ex2-listbox'),
searchVeggies,
true
);
var ex3Combobox = new aria.ListboxCombobox(
document.getElementById('ex3-combobox'),
document.getElementById('ex3-input'),
document.getElementById('ex3-listbox'),
searchVeggies,
true,
and replace with a comma. That should solve the problem.

How update a view when observable subcription changes on angular 6?

I have a service which is publishing objects on an observable, but when I update an array using a subscription, the new object does not display on the list (html):
export class PublicationService {
// ... more code ...
getAllPapersOnState(state: string): Observable<Paper> {
return Observable.create(observer => {
this.WF_SC.deployed().then(instance => {
// State changed watcher to update on-live the observable
const event = instance.AssetStateChanged({});
event.on('data', (data) => {
console.log('StateChanged catched!');
this.getPaper((data['args']['assetAddress'])).then((paper) => observer.next(paper));
});
// TODO Filter by asset type
return instance.findAssetsByState.call(state);
}).then(addresses => {
addresses.forEach((address) => this.getPaper(address).then((paper) => observer.next(paper)));
});
});
}
}
export class HomeComponent implements OnInit, OnDestroy {
papersSubmitted$:Observable<DataCard>;
papersOnReview$:Observable<DataCard>;
papersPublished$:Observable<DataCard>;
constructor(private publicationService: PublicationService) { }
ngOnInit() {
this.papersSubmitted$ = this.publicationService.getAllPapersOnState("Submitted").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', 'Review'))
);
this.papersOnReview$ = this.publicationService.getAllPapersOnState("OnReview").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', 'Accept'))
);
this.papersPublished$ = this.publicationService.getAllPapersOnState("Published").pipe(
map(paper => HomeComponent.paperToCard(paper,'Read', ''))
);
}
// ... more code ...
}
<app-cardlist
[title]="'Published'"
[description]="'Last science papers published. Yay!'"
[collection$]="papersPublished$"
(clickActionCard)="clickActionCardHandlePublished($event)"></app-cardlist>
<app-cardlist
[title]="'On Review'"
[description]="'Last papers on review. On publish way!'"
[collection$]="papersOnReview$"
(clickActionCard)="clickActionCardHandleOnReview($event)"></app-cardlist>
<app-cardlist
[title]="'Submitted'"
[description]="'Last papers submitted for reviewing. Be the first one to check them!'"
[collection$]="papersSubmitted$"
(clickActionCard)="clickActionCardHandleSubmitted($event)"></app-cardlist>
export class CardlistComponent implements OnInit {
#Input() title;
#Input() description;
#Input() collection$: Observable<DataCard>;
items:DataCard[] = [];
subscription:Subscription;
#Output() clickActionCard: EventEmitter<any> = new EventEmitter();
constructor() { }
ngOnInit() {
this.subscription = this.collection$.subscribe((data_card:DataCard) => {
console.log(data_card);
this.items.push(data_card);
console.log(this.items);
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
<div *ngIf="(items)" class="card-deck">
<div *ngFor="let item of items" class="card" style="width: 18rem;">
<div class="card-body">
<div class="card-body">
<h5 class="card-title">{{item['title']}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{item['subtitle']}}</h6>
<p class="card-text">{{item['description']}}</p>
{{item['action_1_name']}}
{{item['action_2_name']}}
</div>
</div>
</div>
</div>
The new object published on observable $collection should be displayed on the CardList but nothing is refreshed although console.log(data_card) show the new object.
Repository code: HomeComponent, CardListComponent

Angular2 - Display image

I created a Angular2 app that allows the user to upload images. I want to implement a preview option. However, when i try to imperilment it the image doesn't show up. How do i achieve this feature?
UploadComponent.ts
import * as ng from '#angular/core';
//import { UPLOAD_DIRECTIVES } from 'ng2-uploader';
import {UploadService} from '../services/upload.service';
#ng.Component({
selector: 'my-upload',
providers:[UploadService],
template: require('./upload.html')
})
export class UploadComponent {
progress:any;
logo:any;
filesToUpload: Array<File>;
constructor(public us:UploadService){
this.filesToUpload = [];
}
upload() {
this.us.makeFileRequest("http://localhost:5000/api/SampleData/Upload", this.filesToUpload)
.then((result) => {
console.log(result);
}, (error) => {
console.error(error);
});
}
onFileChange(fileInput: any){
this.logo = fileInput.target.files[0];
}
}
Upload.html
<h2>Upload</h2>
<input type="file" (change)="onFileChange($event)" placeholder="Upload image..." />
<button type="button" (click)="upload()">Upload</button>
<img [src]="logo" alt="Preivew">
The way you try it, you don't get the image URL with fileInput.target.files[0], but an object.
To get an image URL, you can use FileReader (documentation here)
onFileChange(fileInput: any){
this.logo = fileInput.target.files[0];
let reader = new FileReader();
reader.onload = (e: any) => {
this.logo = e.target.result;
}
reader.readAsDataURL(fileInput.target.files[0]);
}
filesToUpload: Array<File> = [];
url: any;
image: any;
//file change event
filechange(fileInput: any) {
this.filesToUpload = <Array<File>>fileInput.target.files;
this.image = fileInput.target.files[0]['name'];
this.readurl_file(event);
}
//read url of the file
readurl_file(event) {
if (event.target.files && event.target.files[0]) {
const reader = new FileReader();
reader.onload = (eve: any) => {
this.url = eve.target.result;
};
reader.readAsDataURL(event.target.files[0]);
}
}
<div class="form-group">
<label for="image">Image</label>
<input type="file" class="form-control" (change)="filechange($event)" placeholder="Upload file..." >
</div>
<div class="container">
<img [src]="url">
</div>
Using FileReader is not a good practice. If an image is too large it can crash your browser because onload function load whole image in RAM.
Better approach is to use:
url = URL.createObjectURL($event.target.files[0]);
Then, show it out with DomSanitizer:
this.sanitizer.bypassSecurityTrustUrl(url)
So in ts:
constructor(private sanitizer: DomSanitizer) {}
onFileChange(fileInput: any){
this.url = URL.createObjectURL($event.target.files[0]);
}
get previewUrl(): SafeUrl {
return this.sanitizer.bypassSecurityTrustUrl(this.url);
}
And in html:
<img [src]="previewUrl"/>

Rendering an object stored in state

I'm trying to render out my calendar's event summaries, using a .map function. I've stored my calendar events object in state, but can't find a way to .map out the different event summaries. Any suggestions?
export default class Container extends React.Component{
calendarID="xxx"
apiKey="zzz";
state = { events: [] };
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({events: response.body});
console.log("success");
console.log(this.state.events);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let lista = this.state.events;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((event, index) => {
const summary = event.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
}
EDIT:
Thanks for your answers! This is the data that the ajax call returns when I console log this.state.items:
Object {items: Array[1]}
items: Array[1]
0: Object
id: "cmkgsrcohfebl5isa79034h8a4"
start: Object
summary: "Stuff going down"
If I skip the ajax call and create my own state, the mapping works:
state = { items: [
{ items: { summary: "testing"} },
{ items: { summary: "12"} },
{ items: { summary: "3"} }
]};
To get this working, however, I change my render-function to:
render(){
let lista = this.state.items;
let arr = Object.keys(lista).map(key => lista[key])
return (
<div class = "container">
{arr.map((item, index) => {
const summary = item.items.summary;
return (<div key={index}>{summary}</div>);
})}
</div>
);
}
So maybe it has something to do with the object that this.state.items returns from the ajax call?
Edit2: #Andrea Korinski, you were right! I changed my render function to this, and now it works:
render(){
let list = this.state.items;
const arr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{arr}
</div>
);
}
}
The whole component:
export default class Container extends React.Component{
calendarID="xxx";
apiKey="zzz";
state = {items: []};
setEvents = (a) => {
this.setState(a);
}
componentDidMount() {
ajax.get(`https://www.googleapis.com/calendar/v3/calendars/${this.calendarID}/events?fields=items(summary,id,location,start)&key=${this.apiKey}`)
.end((error, response) => {
if(!error && response ) {
this.setEvents({items: response.body});
console.log("success");
console.log(this.state.items);
} else {
console.log("Errors: ", error);
}
});
}
render(){
let list = this.state.items;
const irr = (list.items || []).map((item, index) => {
const summary = item.summary;
return (<div key={index}>{summary}</div>);
});
return (
<div class = "container">
{irr}
</div>
);
}
}

Resources