test case not detecting update in ViewContainerRef - angular6

My html uses an ng-template. The template is to create thumbnails.
<ng-template #thumbnailTemplate let-option="option">
<div id="{{option.divId}}"> <!-- top level div of thumbnail. This will have ids thumbnail-1, thumbnail-2 etc.-->
<img id="{{option.imgId}}" src="{{option.imgSrc}}"> <!-- this will have width, height=80-->
<!-- the X button is created using CSS. This will have ids close-button-1, close-button-2. They'll also containn reference to the parent div id (thumbnail-1, thumbnail-2 ) -->
</div>
</ng-template>
The thumbnails gets created when a file is selected from an input element. FileReader sends load event and my event handler is called which should create a thumbnail by adding a view in the container
handleReaderLoaded(event:FileReaderProgressEvent) {
console.log("got load event of file reader ",event);
let thumbnailTemplateViewRef:EmbeddedViewRef<any>;
let imageString = event.target.result;//this will be like data:image/png;base64,ZGQ=ZGF0YTppbWFnZS9wbmc7YmFzZTY0LFpHUT0=
console.log("result from file load: ",imageString);
console.log("consecutive generator is ",this.consecutiveIdGenerator);
//create new ids for div, img and a in the template
++this.consecutiveIdGenerator;
let divId = "thumbnail-"+(this.consecutiveIdGenerator);
console.log("div id "+divId);
let imgId = "img-"+(this.consecutiveIdGenerator);
console.log("img id "+imgId);
let closeId = "close-button-"+(this.consecutiveIdGenerator);
console.log("close Id is "+closeId);
console.log("thumbnail container length was "+this.thumbnailContainerRef.length);
//TODOM - define context as a class so that it can be used in new question and question details
thumbnailTemplateViewRef = this.thumbnailContainerRef.createEmbeddedView(this.thumbnailTemplateRef,{option:{divId:divId,
imgId:imgId,
closeId:closeId,
imgSrc:imageString}});
//store the reference of the view in context of the template. This will be used later to retrive the index of the view when deleting the thumbnail
thumbnailTemplateViewRef.context.option.viewRefId = thumbnailTemplateViewRef;
console.log("thumbnail container length is "+this.thumbnailContainerRef.length);
}
Now I want to test handleReaderLoaded and check that it updates the thumbnailContainerRef by adding thumbnailTemplateViewRef in it.
The spec I have written is
fit('should upload image if user selects an image', () => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let file1 = new File(["foo1"], "foo1.txt");
let reader = newPracticeQuestionComponent.handleFileSelect([file1]);//the callback for FileReader load method is assigned in this function. The callback is handleReaderLoaded
fixture.detectChanges();
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(1);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1);
done(); //wait
console.log('done here');
});
My test case is failing because expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1); is coming out as 0.
What am I doing wrong?

seems, I didn't understand the purpose of done correctly. I thought if I'll use done, the script will automatically wait but it doesn't (as is clear from the following trace)
reading file --> this is in handleFileSelect
context.js:1972 done here -->ths is in handleFileSelect
context.js:1972 got load event of file reader ProgressEvent {isTrusted: true, lengthComputable: true, loaded: 4, total: 4, type: "load", …} --> this is in the callback handleReaderLoaded. So the spec finished before the callback was called.
I done acts as a checkpoint in Jasmine. When Jasmine sees that a spec uses done, it knows that it cannot proceed to the next step (say run next spec) unless the code leg containing done has been called.
I re-wrote the spec to and created the checkpoint using done as follows
it('should upload image if user selects an image', (done) => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(0);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let imageThumbnailDiv = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg = fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose = fixture.debugElement.query(By.css('#close-button-1'));
//there should not be any HTML code which contains thumbnail
expect(imageThumbnailDiv).toBeFalsy();
expect(imageThumbnailImg).toBeFalsy();
expect(imageThumbnailClose).toBeFalsy();
let file1 = new File(["foo1"], "foo1.txt");
let reader = newPracticeQuestionComponent.handleFileSelect([file1]);
//file upload is async. so give time for `load` event of FileReader to be triggered and handled
setTimeout(function() {
console.log("in timeout");
fixture.detectChanges();//without this, the view will not be updated with model
expect(newPracticeQuestionComponent.currentImageAttachmentCount).toBe(1);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1);
//the html for thumbnail should be created now
let imageThumbnailDiv2 = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg2= fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose2 = fixture.debugElement.query(By.css('#close-button-1'));
expect(imageThumbnailDiv2).toBeTruthy();
expect(imageThumbnailImg2).toBeTruthy();
expect(imageThumbnailClose2).toBeTruthy();
done();//without done, jasmine will finish this test spec without checking the assertions in the timeout
}, 2000);
//if done is not use, jasmine will just finish the current spec without checking any assertions
});

Related

How to represent a state machine with HTML elements?

On a web page I wish to display an element which depends on the state of some JavaScript. State like in a state machine. Currently the possible states are these (but I may add more):
input: display some input elements for the user to set. The user can click a button to start some JavaScript processing and move to the working state.
working: display a progress bar informing the user that the script is running. The user can cancel the computation (moving back to the input state) or the computation can end (moving to either the result or error state).
result: display the computation result. The user can go back to input with a button.
error: display the error. The user can go back to input with a button.
The JavaScript part is ready and working, but I'm unsure how to do this in HTML + CSS.
Current solution and its issue
Currently I've been doing it with classes: I set a class to a common ancestor element with the same name of the state and I display the right elements based on it. Something like this:
const parent=document.querySelector("#parent");
let timer=null;
function input(){
parent.classList.remove("working","result","error");
parent.classList.add("input");
}
function run(){
parent.classList.remove("input");
parent.classList.add("working");
timer=setTimeout(result,1500)
}
function stop(){
clearTimeout(timer);
input();
}
function result(){
parent.classList.remove("working");
if(Math.random()>0.5){parent.classList.add("result");}
else{parent.classList.add("error");}
}
input();
#input{display:none;}
#working{display:none;}
#result{display:none;}
#error{display:none;}
#parent.input #input{display:block;}
#parent.working #working{display:block;}
#parent.result #result{display:block;}
#parent.error #error{display:block;}
<div id="parent">
<div id="input">INPUT. RUN</div>
<div id="working">WORKING. STOP</div>
<div id="result">RESULT. RESTART</div>
<div id="error">ERROR. RESTART</div>
</div>
This solution works but it feels unstable: in theory it would be possible for the parent element to have no classes (in which case nothing is displayed) or multiple ones (in which case you'd see multiple states at once). This shouldn't happen, but the only thing preventing it is the correctness of my script.
Question
Are there better ways to implement this idea of states, so that the HTML elements can't end up in inconsistent states?
Let’s consider the role which HTML plays in a state machine on the web. A machine has moving parts, it is dynamic, so the core of any machine on the web must be implemented in Javascript. HTML is useful only to provide the interface between the user and the machine. It’s a subtle distinction but it fundamentally changes the way you write it.
Have you ever used React? React provides the framework to create entire web applications as “state machines”. React’s mantra is “UI is a function of state”. In a React app, you have a single variable which contains the current state, rendering code which builds the UI based on the state, and core code (mostly event handlers) which updates the state.
Even if you don’t want to build in React, you can use the same general idea:
keep the current state in a Javascript variable (typically you’d use an object, but in this case we only need a string)
write a rendering function which reads the state and then builds the appropriate HTML to represent that state
in the event handlers for your links, do any operations which are required, update the state and call the rendering function
let state = null
let timer = null
// core code
const input = () => {
state = 'input'
render()
}
const run = () => {
state = 'working'
render()
timer = setTimeout(result,1500)
}
const stop = () => {
clearTimeout(timer)
state = 'input'
render()
}
const result = () => {
if(Math.random()>0.5)
state = 'result'
else
state = 'error'
render()
}
// rendering code
const render = () => {
let x = state
switch(state) {
case 'input':
x += ' run'
break
case 'working':
x += ' stop'
break
case 'result':
x += ' restart'
break
case 'error':
x += ' restart'
break
}
document.getElementById('container').innerHTML = x
}
// initialisation code
state = 'input'
render()
<div id="container"></div>

Can i load data referenced by a Web Component dynamically, with caching?

I'm currently learning Web Components and I wonder if it is possible to have a Component load its own data dynamically, similar to how <img> does from its src attribute, i.e. something like this:
<my-fancy-thingy src='/stuff.json'></my-fancy-thingy>
Obviously this functionality would be useful if stuff.json could be rather large, so it should also be possible to make use of the browser's caching mechanism so the referenced file doesn't get reloaded every time we request the page, unless changed.
Can this be done?
Sure, take inspiration from <load-file> See Dev.to Post
/*
defining the <load-file> Web Component,
yes! the documenation is longer than the code
License: https://unlicense.org/
*/
customElements.define("load-file", class extends HTMLElement {
// declare default connectedCallback as sync so await can be used
async connectedCallback(
// attach a shadowRoot if none exists (prevents displaying error when moving Nodes)
// declare as parameter to save 4 Bytes: 'let '
shadowRoot = this.shadowRoot || this.attachShadow({mode:"open"})
) {
// load SVG file from src="" async, parse to text, add to shadowRoot.innerHTML
shadowRoot.innerHTML = await (await fetch(this.getAttribute("src"))).text()
// append optional <tag [shadowRoot]> Elements from inside <load-svg> after parsed <svg>
shadowRoot.append(...this.querySelectorAll("[shadowRoot]"))
// if "replaceWith" attribute
// then replace <load-svg> with loaded content <load-svg>
// childNodes instead of children to include #textNodes also
this.hasAttribute("replaceWith") && this.replaceWith(...shadowRoot.childNodes)
}
})
Change .text() to .json() and it parses JSON files
Caching can be done by storing the String in localStorage (but a 5MB limit total, I think):
https://en.wikipedia.org/wiki/Web_storage
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
You need to come up with "data has changed" strategy; as the Client has no clue when data actually was changed. Maybe an extra semaphore file/endpoint that provides info if the (large) JSON file was changed.
This works like a charm
export class MonElement extends HTMLElement {
constructor(){
super();
this.attachShadow({mode:'open'});
(...)
this.shadowRoot.appendChild(atemplate);
}
connectedCallback(){...}
static get observedAttributes(){
return ['src'];
}
attributeChangedCallback(nameattr,oldval,newval)
{
if (nameattr==='src') {
this[nameattr]=newval;
here do the fetch for the src value which is newval then update what you got in the innerdom
}
(...)

WinJS variable/object scope, settings, and events?

I am not sure what the proper heading / title for this question should be. I am new to WinJS and am coming from a .NET webform and winclient background.
Here is my scenario. I have a navigation WinJS application. My structure is:
default.html
(navigation controller)
(settings flyout)
pages/Home.html
pages/Page2.html
So at the top of the default.js file, it sets the following variables:
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
It seems like I cannot use these variables anywhere inside my settings flyout or any of my pages:ready functions. They are only scoped to the default.js?
In the same regard, are there resources on the interwebs (links) that show how to properly share variables, events, and data between each of my "pages"?
The scenario that I immediately need to overcome is settings. In my settings flyout, I read and allow the user to optionally set the following application setting:
var applicationData = Windows.Storage.ApplicationData.current;
var localSettings = applicationData.localSettings;
localSettings.values["appLocation"] = {string set by the user};
I want to respond to that event in either my default.js file or even one of my navigation pages but I don't know where to "listen". My gut is to listen for the afterhide event but how do I scope that back to the page where I want to listen from?
Bryan. codefoster here. If you move the lines you mentioned...
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
...up and out of the immediate function, they'll be in global scope and you'll have access to them everywhere. That's one of the first things I do in my apps. You'll hear warnings about using global scope, but what people are trying to avoid is the pattern of dropping everything in global scope. As long as you control what you put in there, you're fine.
So put them before the beginning of the immediate function on default.js...
//stuff here is scoped globally
var app = WinJS.Application;
var activation = Windows.ApplicationModel.Activation;
var nav = WinJS.Navigation;
(function () {
//stuff here is scoped to this file only
})();
If you are saving some data and only need it in memory, you can just hang it off the app variable instead of saving it into local storage. That will make it available to the whole app.
//on Page2.js
app.myCustomVariable = "some value";
//on Page3.js
if(app.myCustomVariable == "some value") ...
Regarding your immediate need:
like mentioned in the other answer, you can use datachanged event.
Regards sharing variables:
If there are variables that you would like to keep global to the application, they can be placed outside the anonymous function like mentioned in the Jeremy answer. Typically, that is done in default.js. Need to ensure that scripts using the global variables are placed after the script defining the global variable - in default.html. Typically - such variable will point to singleton class. For example: I use it in one of my apps to store authclient/serviceclient for the backend service for the app. That way - the view models of the multiple pages need not create instance of the object or reference it under WinJS namespace.
WinJS has also concept of Namespace which lets you organize your functions and classes. Example:
WinJS.Namespace.define('Utils.Http',
{
stringifyParameters: function stringifyParameters(parameters)
{
var result = '';
for (var parameterName in parameters)
{
result += encodeURIComponent(parameterName) + '=' + encodeURIComponent(parameters[parameterName]) + '&';
}
if (result.length > 0)
{
result = result.substr(0, result.length - 1);
}
return result;
},
}
When navigating to a page using WinJS.Navigation.navigate, second argument initialState is available as options parameter to the ready event handler for the page. This would be recommended way to pass arguments to the page unless this it is application data or session state. Application data/session state needs to be handled separately and needs a separate discussion on its own. Application navigation history is persisted by the winjs library; it ensures that if the app is launched again after suspension - options will be passed again to the page when navigated. It is good to keep the properties in options object as simple primitive types.
Regards events:
Typically, apps consume events from winjs library. That can be done by registering the event handler using addEventListener or setting event properties like onclick etc. on the element. Event handlers are typically registered in the ready event handler for the page.
If you are writing your own custom control or sometimes in your view model, you may have to expose custom events. Winjs.UI.DOMEventMixin, WinJS.Utilities.createEventProperties can be mixed with your class using WinJS.Class.mix. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Utilities.createEventProperties('customEvent'),
WinJS.UI.DOMEventMixin);
Most often used is binding to make your view model - observable. Refer the respective samples and api documentation for details. Example:
WinJS.Class.mix(MyViewModel,
WinJS.Binding.mixin,
WinJS.Binding.expandProperties({ items: '' }));
Here is what I ended up doing which is kinda of a combination of all the answers given:
Created a ViewModel.Settings.js file:
(function () {
"use strict";
WinJS.Namespace.define("ViewModel", {
Setting: WinJS.Binding.as({
Name: '',
Value: ''
}),
SettingsList: new WinJS.Binding.List(),
});
})();
Added that file to my default.html (navigation container page)
<script src="/js/VMs/ViewModel.Settings.js"></script>
Add the following to set the defaults and start 'listening' for changes
//add some fake settings (defaults on app load)
ViewModel.SettingsList.push({
Name: "favorite-color",
Value: "red"
});
// listen for events
var vm = ViewModel.SettingsList;
vm.oniteminserted = function (e) {
console.log("item added");
}
vm.onitemmutated = function (e) {
console.log("item mutated");
}
vm.onitemchanged = function (e) {
console.log("item changed");
}
vm.onitemremoved = function (e) {
console.log("item removed");
}
Then, within my application (pages) or my settings page, I can cause the settings events to be fired:
// thie fires the oniteminserted
ViewModel.SettingsList.push({
Name: "favorite-sport",
Value: "Baseball"
});
// this fires the itemmutated event
ViewModel.SettingsList.getAt(0).Value = "yellow";
ViewModel.SettingsList.notifyMutated(0);
// this fires the itemchanged event
ViewModel.SettingsList.setAt(0, {
Name: "favorite-color",
Value: "blue"
});
// this fires the itemremoved event
ViewModel.SettingsList.pop(); // removes the last item
When you change data that needs to be updated in real time, call applicationData.signalDataChanged(). Then in the places that care about getting change notifications, listen to the datachanged on the applicationData object. This is also the event that is raised when roaming settings are synchronized between computers.
I've found that many times, an instant notification (raised event) is unnecessary, though. I just query the setting again when the value is needed (in ready for example).

Getting Current Data from KendoUI TreeView

I'm using a kendo UI tree with a remote data source from a JSON file.
I have a button on the tree page which gets the current data of the tree,sends it through a POST to a server and the server saves the current data to the JSON file so as the next time I reload the page,the changes I made will be kept.That's what I want to happen.
So I know the current data of the tree is in:
$("#treeview").data("kendoTreeView").dataSource.data()
Which means the data changes real time in there for example when someone drag and drops a node of the tree.
My problem starts when this data doesn't seem to change when I drag and drop nodes inside the tree,and only changes when I drag and drop a node on the root level of the tree and even then it doesn't do it correcly as the node should be moved in there as well but instead the node gets copied,leaving the past node in the tree as well...
For Example I have this tree:
If I make a drag and drop change like this:
And I send the data,save it and reload the change isn't made at all!
PS:Even when I view the current data after the change before sending it,I see that there is no change on the data at all even though I did a change visualy with a drag and drop.So it doesn't have to do with the sending,saving and the server.
On the other hand,if I make a change like this:
I can see in the current data that the moved node is added in the end of the data indeed but it is not deleted from it's initial position within the data!So if i send the current data to the server,save it and then refresh I get the result:
The code for viewing and sending the data is:
function sendData() {
var req = createRequest();
var putUrl = "rest/hello/treeData";
req.open("post", putUrl, true);
req.setRequestHeader("Content-type","application/json");
var dsdata = $("#treeview").data("kendoTreeView").dataSource.data();
alert(JSON.stringify(dsdata));
req.send(JSON.stringify(dsdata));
req.onreadystatechange = function() {
if (req.readyState != 4) {
return;
}
if (req.status != 200) {
alert("Error: " + req.status);
return;
}
alert("Sent Data Status: " + req.responseText);
}
}
Is this a Bug or am I doing something wrong?Has anyone been able to see the current data changing correctly on every drag and drop?
First and most important you have to use the latest version of KendoUI (Kendo UI Beta v2012.3.1024) still in beta but is where they have solved many problems.
Then, when you create the kendoTreeView you have to say something like:
tree = $("#treeview").kendoTreeView({
dataSource :kendo.observableHierarchy(data),
dragAndDrop:true
}).data("kendoTreeView");
Here the important is not using directly data array but wrapping it with kendo.observableHierarchy.
Then you will have the data updated with the result of drag & drops.
For me in addition to OnaBai answer I had to use the sync function on the save method. I am using Type Script.
this.treeData = new kendo.data.HierarchicalDataSource({
data: kendo.observableHierarchy([]),//Thanks OnaBai
schema: {
model: {
id: "MenuItemId",
children: "MenuItemChildren",
hasChildren: (e) => {
//this removes arrow next to items that do not have children.
return e.MenuItemChildren && e.MenuItemChildren.length > 0;
}
}
}
});
public save() {
this.treeData.sync().done(() => {
console.log("sync data");
var myType = this.treeData.view();
this.$http.post("/api/TreeViewPicker", myType)
.then((response) => {
});
});
}

Soundcloud HTML5 Player: Events.FINISH only fired once

I'm using the SC HTML5 player, when one sound finishes, I load in another source, however the FINISH event only seems to fire for the first song, my code is as follows
//Set the source
document.getElementById("sc-widget").src = scPath;
//get the widget reference
var widgetIframe = document.getElementById('sc-widget'),
widget = SC.Widget(widgetIframe);
//set the finish event
widget.bind(SC.Widget.Events.FINISH, endSC);
function endSC() {
var scPath = "http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F1848538&show_artwork=true&auto_play=true";
document.getElementById("sc-widget").src = scPath;
var widgetIframe = document.getElementById('sc-widget'),
widget = SC.Widget(widgetIframe);
widget.bind(SC.Widget.Events.FINISH, endSC);
}
I've tried setting the endSC target to another function but that doesn't work, what am I missing? Thanks!
I had the same problem. SC.Widget method is working fine when I call it for the first time, but if I try to call it for the second time the console will fire "Uncaught TypeError: Cannot read property 'parentWindow' of null" error in http://w.soundcloud.com/player/api.js script. And that is where api.js script stops with actions (.Widget, .bind, etc.)
I found the solution. It's very weird, but it is a solution.
SoundCloud remote script is minified. Load it in your browser, C/P it in some online js beautifier and save it locally. Edit line 103 as follows:
return a.contentWindow;// || a.contentDocument.parentWindow
So I removed that .parentWindow call.
Save the file and call it in your page's head section. And that's it! Now FINISH event fires on every loaded widget.
I hope this will help.
Looks like this question is over 10 years old, but it just came up for me now.
I recreated the iframe div from scratch. Otherwise, the SC.Widget.Events.FINISH will only fire when the original embed player finishes.
You must reset the DOM element events by completely recreating the iframe element, like so:
//EXAMPLE SC SONG IDs
let songIds = [216109050, 779324239, 130928732]
let incrementingIndex = 0
function playSongsInIframe() {
let iframeParent = document.querySelector('#sound-player')
let iframeElement = document.querySelector('#sound-player iframe')
iframeElement.remove()
//CODE TO ADD NEW SOUND IDs
//yourSoundId = songIds[incrementingIndex]
let newIframe = document.createElement('iframe')
newIframe.id = "sound-" + yourSoundId
newIframe.width = "100%"
newIframe.height = "166"
newIframe.scrolling="no"
newIframe.frameborder="no"
newIframe.allow = "autoplay"
newIframe.src = "https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/" + yourSoundId + "&auto_play=true"
iframeParent.appendChild(newIframe)
let widget = SC.Widget("sound-" + yourSoundId);
widget.bind(SC.Widget.Events.READY, () => {
console.log('Ready...');
widget.play()
});
widget.bind(SC.Widget.Events.FINISH, () => {
console.log('Song ended...');
incrementingIndex++
playSongsInIframe()
});
}
One last consideration - this process must be started from a user event, like a click. You can add this function to the onclick attribute of an HTML button element:
<button onclick="playSongsInIframe()">Start Radio</button>