I am trying to created a nested repeater or a nested list view using WinJS 4.0, but I am unable to figure out how to bind the data source of the inner listview/repeater.
Here is a sample of what I am trying to do (note that the control could be Repeater, which I would prefer):
HTML:
<div id="myList" data-win-control="WinJS.UI.ListView">
<span data-win-bind="innerText: title"></span>
<div data-win-control="WinJS.UI.ListView">
<span data-win-bind="innerText: name"></span>
</div>
</div>
JS:
var myList = element.querySelector('#myList).winControl;
var myData = [
{
title: "line 1",
items: [
{name: "item 1.1"},
{name: "item 1.2"}
]
},
{
title: "line 2",
items: [
{name: "item 2.1"},
{name: "item 2.2"}
]
}
];
myList.data = new WinJS.Binding.List(myData);
When I try this, nothing renders for the inner list. I have attempted trying to use this answer Nested Repeaters Using Table Tags and this one WinJS: Nested ListViews but I still seem to have the same problem and was hoping it was a little less complicated (like KnockOut).
I know it is mentioned that WinJS doesn't support nested ListViews, but that seems to be a few years ago and I am hoping that is still not the issue.
Update
I was able to get the nested repeater to work correctly, thanks to Kraig's answer. Here is what my code looks like:
HTML:
<div id="myTemplate" data-win-control="WinJS.Binding.Template">
<div
<span>Bucket:</span><span data-win-bind="innerText: name"></span>
<span>Amount:</span><input type="text" data-win-bind="value: amount" />
<button class="removeBucket">X</button>
<div id="bucketItems" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#myTemplate')}"
data-win-bind="winControl.data: lineItems">
</div>
</div>
</div>
<div id="budgetBuckets" data-win-control="WinJS.UI.Repeater"
data-win-options="{data: Data.buckets,template: select('#myTemplate')}">
</div>
JS: (after the "use strict" statement)
WinJS.Namespace.define("Data", {
buckets: new WinJS.Binding.List([
{
name: "A",
amount: 5,
lineItems: new WinJS.Binding.List( [
{ name: 'test item1', amount: 50 },
{ name: 'test item2', amount: 25 }
]
)
}
])
})
*Note that this answers part of my question, however, I would really like to do this all after a repo call and set the repeater data source programmatically. I am going to keep working towards that and if I get it I will post that as the accepted answer.
The HTML Repeater control sample for Windows 8.1 has an example in scenario 6 with a nested Repeater, and in this case the Repeater is created through a Template control. That's a good place to start. (I discuss this sample in Chapter 7 of Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition, starting on page 372, or 374 for the nested part.)
Should still work with WinJS 4, though I haven't tried it.
Ok, so I have to give much credit to Kraig because he got me on the correct path to getting this worked out and the referenced book Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition is amazing.
The original issue was a combination of not using templates correctly (using curly braces in the data-win-bind attribute), not structuring my HTML correctly and not setting the child lists as WinJS.Binding.List data source. Below is the final working code structure to created a nested repeater when binding the data from code only:
HTML:
This is the template for the child lists. It looks similar, but I plan on add more things so I wanted it separate instead of recursive as referenced in the book. Note that the inner div after the template control declaration was important for me.
<div id="bucketItemTemplate" data-win-control="WinJS.Binding.Template">
<div>
<span>Description:</span>
<span data-win-bind="innerText: description"></span>
<span>Amount:</span>
<input type="text" data-win-bind="value: amount" />
<button class="removeBucketItem">X</button>
</div>
</div>
This is the main repeater template for the lists. Note that the inner div after the template control declaration was important for me. Another key point was using the "winControl.data" property against the property name of the child lists.
<div id="bucketTemplate" data-win-control="WinJS.Binding.Template">
<div>
<span>Bucket:</span>
<span data-win-bind="innerText: bucket"></span>
<span>Amount:</span>
<input type="text" data-win-bind="value: amount" />
<button class="removeBucket">X</button>
<div id="bucketItems" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#bucketItemTemplate')}"
data-win-bind="winControl.data: lineItems">
</div>
</div>
</div>
This is the main control element for the nested repeater and it is pretty basic.
<div id="budgetBuckets" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#bucketTemplate')}">
</div>
JavaScript:
The JavaScript came down to a few simple steps:
Getting the winControl
var bucketsControl = element.querySelector('#budgetBuckets').winControl;
Looping through the elements and making the child lists into Binding Lists - the data here is made up but could have easily came from the repo:
var bucketsData = selectedBudget.buckets;
for (var i = 0; i < bucketsData.length; i++) {
bucketsData[i].lineItems =
new WinJS.Binding.List([{ description: i, amount: i * 10 }]);
}
Then finally converting the entire data into a Binding list and setting it to the "data" property of the winControl.
bucketsControl.data = new WinJS.Binding.List(bucketsData);
*Note that this is the entire JavaScript file, for clarity.
(function () {
"use strict";
var nav = WinJS.Navigation;
WinJS.UI.Pages.define("/pages/budget/budget.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
// TODO: Initialize the page here.
var bindableBuckets;
require(['repository'], function (repo) {
//we can setup our save button here
var appBar = document.getElementById('appBarBudget').winControl;
appBar.getCommandById('cmdSave').addEventListener('click', function () {
//do save work
}, false);
repo.getBudgets(nav.state.budgetSelectedIndex).done(function (selectedBudget) {
var budgetContainer = element.querySelector('#budgetContainer');
WinJS.Binding.processAll(budgetContainer, selectedBudget);
var bucketsControl = element.querySelector('#budgetBuckets').winControl;
var bucketsData = selectedBudget.buckets;
for (var i = 0; i < bucketsData.length; i++)
{
bucketsData[i].lineItems = new WinJS.Binding.List([{ description: i, amount: i * 10 }]);
}
bucketsControl.data = new WinJS.Binding.List(bucketsData);
});
});
WinJS.UI.processAll();
}
});
})();
Related
I'm using this code with the intent to create different tags, i.e. item.tag below:
<div v-for="item in items" :key="item.id">
<{{item.tag}}>
{{item.data}}
</{{item.tag}}>
</div>
With items defined as follows:
items: generateItems(2, i => ({
id: 'item' + i,
tag: 'hr',
data: ''
}))
But the HTML inside the div after the code runs has the < and > escaped, even though they aren't inside {{ }}, so it looks like this:
<hr> </hr>
But if I define the type explicitly:
<div v-for="item in items" :key="item.id">
<hr>
{{item.data}}
</hr>
</div>
The < and > are not escaped, and the horizontal rules display no problem.
I intend to use other tags besides hr so would like to be able to use item.tag some way.
Can anyone explain what is going on, and is there a workaround for this?
One way to do this is to use the <component :is="tag"> For example:
var demo = new Vue({
el: '#demo',
data() {
return {
tag: 'button',
othertag: 'hr'
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-html id="demo">
<component :is="tag">hello</component>
<component :is="othertag"></component>
</div>
Right now, I'm trying to make a website that shows recent news posts which is supplied my NodeJS API.
I've tried the following:
HTML
<div id="news" class="media" v-for="item in posts">
<div>
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
JavaScript
const news = new Vue({
el: '#news',
data: {
posts: [
{title: 'My First News post', msg: 'This is your fist news!'},
{title: 'Cakes are great food', msg: 'Yummy Yummy Yummy'},
{title: 'How to learnVueJS', msg: 'Start Learning!'},
]
}
})
Apparently, the above didn't work because Vue can't render multiple root elements.
I've looked up the VueJS's official manual and couldn't come up with a solution.
After googling a while, I've understood that it was impossible to render multiple root element, however, I yet to have been able to come up with a solution.
The simplest way I've found of adding multiple root elements is to add a single <div> wrapper element and make it disappear with some CSS magic for the purposes of rendering.
For this we can use the "display: contents" CSS property. The effect is that it makes the container disappear, making the child elements children of the element the next level up in the DOM.
Therefore, in your Vue component template you can have something like this:
<template>
<div style="display: contents"> <!-- my wrapper div is rendered invisible -->
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
</div>
</template>
I can now use my component without the browser messing up formatting because the wrapping <div> root element will be ignored by the browser for display purposes:
<table>
<my-component></my-component> <!-- the wrapping div will be ignored -->
</table>
Note however, that although this should work in most browsers, you may want to check here to make sure it can handle your target browser.
You can have multiple root elements (or components) using render functions
A simple example is having a component which renders multiple <li> elements:
<template>
<li>Item</li>
<li>Item2</li>
... etc
</template>
However the above will throw an error. To solve this error the above template can be converted to:
export default {
functional: true,
render(createElement) {
return [
createElement('li', 'Item'),
createElement('li', 'Item2'),
]
}
}
But again as you probably noticed this can get very tedious if for example you want to display 50 li items. So, eventually, to dynamically display elements you can do:
export default {
functional: true,
props: ['listItems'], //this is an array of `<li>` names (e.g. ['Item', 'Item2'])
render(createElement, { props }) {
return props.listItems.map(name => {
return createElement('li', name)
})
}
}
INFO in those examples i have used the property functional: true but it is not required of course to use "render functions". Please consider learning more about functional componentshere
Define a custom directive:
Vue.directive('fragments', {
inserted: function(el) {
const children = Array.from(el.children)
const parent = el.parentElement
children.forEach((item) => { parent.appendChild(item) })
parent.removeChild(el)
}
});
then you can use it in root element of a component
<div v-fragments>
<tr v-for="post in posts">...</tr>
</div>
The root element will not be rendered in DOM, which is especially effective when rendering table.
Vue requires that there be a single root node. However, try changing your html to this:
<div id="news" >
<div class="media" v-for="item in posts">
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
This change allows for a single root node id="news" and yet still allows for rendering the lists of recent posts.
In Vue 3, this is supported as you were trying:
In 3.x, components now can have multiple root nodes! However, this does require developers to explicitly define where attributes should be distributed.
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Multiple root elements are not supported by Vue (which caused by your v-for directive, beacause it may render more than 1 elements). And is also very simple to solve, just wrap your HTML into another Element will do.
For example:
<div id="app">
<!-- your HTML code -->
</div>
and the js:
var app = new Vue({
el: '#app', // it must be a single root!
// ...
})
I'm using slick carousel, and once a div is active I want to open the corresponding description.
Problem I'm having is with this code:
if ($('div').hasClass('active')) {
var title = $(this).attr('title');
$('ul li').removeClass('open');
$(title).addClass('open');
}
What I'm trying to achieve:
Once a div gets class 'active', I want to take its title value, and use it as a id link to list element I want to display(add class to).
Here is a FIDDLE.
Use event handling, not class monitoring.
The slick carousel API has events for this, I believe you want to use the afterChange event to act on the active element after it has been made visible.
Check out the docs and examples, especially the section titled "Events" on Slick page: http://kenwheeler.github.io/slick/
And I think you don't want to use title attribute for this because that is for tooltips. I recommend data-* attributes instead. And element IDs should generally start with a letter and not a number (was required in HTML4 and makes life easier when mapping IDs to JavaScript variables; though if you are using HTML5 I think this requirement is no longer in effect).
HTML
<div id="carousel">
<div data-content-id="content1">
Selector 1 </div>
<div data-content-id="content2">
Selector 2 </div>
<div data-content-id="content3">
Selector 3 </div>
</div>
<ul class="content">
<li id="content1">Content 1</li>
<li id="content2">Content 2</li>
<li id="content3">Content 3</li>
</ul>
JavaScript
$('#carousel').on('afterChange', function(event, slick, currentSlide) {
// get the associated content id
var contentId = $(slick.$slides.get(currentSlide)).data("content-id");
if(contentId && contentId.length)
{
var $content = $("#" + contentId);
$(".content>li").removeClass("open"); // hide other content
$content.addClass("open"); // show target content, or whatever...
}
});
I have found a solution:
$('.slider').on('afterChange', function(event, slick, currentSlide, nextSlide){
var contentId= $(slick.$slides.get(currentSlide)).data('content');
if(contentId)
{
$(".content li").removeClass('open');
$('#' + contentId).addClass('open');
}
});
Working fiddle
I have a basic Videogular video player setup to play videos from Firebase Storage. In the HTML view this works:
<div ng-controller="MyController as controller" class="videogular-container">
<videogular vg-theme="controller.config.theme.url">
<vg-media vg-src="controller.config.sources" vg-native-controls="true"></vg-media>
</videogular>
</div>
In the controller this works:
var ref = firebase.database().ref(); // Create Firebase reference
var obj = $firebaseObject(ref.child($routeParams.id)); // get the record with the key passed in from the URL
var controller = this; // controller refers to the controller object
obj.$loaded( // wait until the async data loads from the remote Firebase
function(data) {
// video player
controller.config = { // provides an object to the controller
preload: "auto",
sources: [
// My Firebase video
{src: $sce.trustAsResourceUrl($scope.wordObject.videos[0].videoURL), type: "video/" + $scope.wordObject.videos[0].videoMediaFormat},
// The Videogular test videos
{src: $sce.trustAsResourceUrl("http://static.videogular.com/assets/videos/videogular.mp4"), type: "video/mp4"},
{src: $sce.trustAsResourceUrl("http://static.videogular.com/assets/videos/videogular.webm"), type: "video/webm"},
{src: $sce.trustAsResourceUrl("http://static.videogular.com/assets/videos/videogular.ogg"), type: "video/ogg"}
],
theme: {
url: "http://www.videogular.com/styles/themes/default/latest/videogular.css"
}
};
},
function(error) {
console.log("Error: ", error)
});
Everything works, to play one video. Now I want to dynamically access arrays of videos by theme. E.g., the user clicks to see all my cat videos or clicks another button to see all my dog videos. I have the Firebase Storage URLs on the $scope and ng-repeat prints out the URLs in the view:
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 text-center">
<h3>{{currentTheme}}</h3>
<div>
<div ng-repeat="video in currentVideos">
{{video.videoURL}}
</div>
</div>
</div>
</div>
That works great too. So to spin out a series of video players with all my cat videos I just have to make an ng-repeat with a new video player for each video, with the vg-src coming from the $scope:
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 text-center">
<h3>{{currentTheme}}</h3>
<div>
<div ng-repeat="video in currentVideos">
<div ng-controller="MyController as controller" class="videogular-container">
<videogular vg-theme="controller.config.theme.url">
<vg-media vg-src="{{video.videoURL}}" vg-native-controls="true"></vg-media>
</videogular>
</div>
</div>
</div>
</div>
</div>
That doesn't work. The error is Error: [$parse:syntax], meaning there's an Angular syntax error. The syntax error goes away when I change the vg-src back to vg-src="controller.config.sources":
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12 text-center">
<h3>{{currentWord}}</h3>
<div>
<div ng-repeat="video in currentVideos">
<div ng-controller="EnglishController as controller" class="videogular-container">
<videogular vg-theme="controller.config.theme.url">
<vg-media vg-src="controller.config.sources" vg-native-controls="true"></vg-media>
</videogular>
</div>
</div>
</div>
</div>
</div>
That works. The problem is that vg-src="controller.config.sources" works but vg-src="{{video.videoURL}}" doesn't work. Why can't Videogular source videos from the $scope?
I tried to put my video sources from the $scope onto controller.config in the controller but this never worked. Should I try to do this again tomorrow? (It's late and I'm getting confused trying to figure out why I can't put my video sources from the $scope onto controller.config in the controller.)
I wrote the question before I went to bed and woke up with (what I hope is) the answer. {{video.videoURL}} inserts the URLs of the videos. controller.config.sources inserts an object with a lot of stuff. I'll try making an array of configured objects and see what happens!
...
Yep, that worked! I wrote a tutorial for a Videogular minimum install, using the $scope instead of controller.config. I don't understand why the official How To Start tutorial uses controller.config instead of the $scope.
...
I can get the one video to play from my array of cat videos when the user clicks "Cat Videos" but I can't get ng-repeat to spin out all the videos in the array.
In the controller when the user clicks the "Cat Videos" button the handler accesses the array of cat videos on Firebase Storage, iterates through the array with forEach, for each video in the array it creates a variable for the videoSource and another variable for the video file format (videoSourceType), then makes a videoObject with an array of sources and a theme, then pushes the videoObject into the array $scope.videoObjects.
$scope.videoObjects = [];
$scope.showVideosOfTheme = function() {
theme.videos.forEach(function(video) { // iterate through the array of videos
var i = 0;
var videoSource = $scope.currentVideos[i].videoURL; // set the video source
var videoSourceType = $scope.currentVideos[i].videoMediaFormat; // set the video format
var videoObject = { // make a video object
preload: "auto",
sources: [
{src: $sce.trustAsResourceUrl(videoSource), type: "video/" + videoSourceType},
],
theme: {
url: "http://www.videogular.com/styles/themes/default/latest/videogular.css"
}
};
$scope.videoObjects.push(videoObject);
i++;
});
};
In the HTML view ng-repeat iterates through the array $scope.videoObjects and fdor each video object spins out a new Videogular video player using the theme and the sources. This doesn't work and the error message is Error: [$parse:syntax], in other words, an Angular syntax error.
<div ng-repeat="video in videoObjects" class="videogular-container">
<videogular vg-theme="{{video.theme.url}}">
<vg-media vg-src="{{video.sources}}" vg-native-controls="true"></vg-media>
</videogular>
</div>
I'll keep working on it!
I am very new to Orchard.
I have created a new theme, based on the Minty theme. The only real change is the layout, where I have adapted the html from an existing asp.net masterpage to match the orchard style razor layout.cshtml. I have experience with MVC and razor, so no problem on that side... unless I have missed something vital.
The problem is the login page. Clicking the sign in link takes me to the correct url without errors, but not login form gets rendered. I have checked that this is the case by Inspecting Element in google chrome.
I am aware that setting up widgets, etc, I can make content appear. However, I can't find how the login form gets inserted when the login url gets requested. I presume it uses the Orchard.Users module, but not sure how. Does it need a specific zone? I can't see why, but see how else.
As a result, I can't solve my problem...
Any pointers?
Any books or other learning media?
The code for my layout.cshtml is:
#functions {
// To support the layout classifaction below. Implementing as a razor function because we can, could otherwise be a Func<string[], string, string> in the code block following.
string CalcuClassify(string[] zoneNames, string classNamePrefix) {
var zoneCounter = 0;
var zoneNumsFilled = string.Join("", zoneNames.Select(zoneName => { ++zoneCounter; return Model[zoneName] != null ? zoneCounter.ToString() : "";}).ToArray());
return HasText(zoneNumsFilled) ? classNamePrefix + zoneNumsFilled : "";
}
}
#{
/* Global includes for the theme
***************************************************************/
SetMeta("X-UA-Compatible", "IE=edge,chrome=1");
Style.Include("http://fonts.googleapis.com/css?family=Handlee");
Style.Include("http://html5shiv.googlecode.com/svn/trunk/html5.js");
Style.Include("site.css");
Script.Require("jQuery").AtHead();
Script.Require("jQueryUI_Core").AtHead();
Script.Require("jQueryUI_Tabs").AtHead();
Script.Include("http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.0.4/modernizr.min.js").AtHead();
Style.Include("TagDefaults.css");
Style.Include("LayoutStructure.css");
Style.Include("LayoutStyling.css");
Style.Include("TopMenu.css");
Style.Include("LeftBlock.css");
Style.Include("RightBlock.css");
Style.Include("MenuAdapter.css");
Style.Include("Content.css");
Style.Include("FloatedBoxes.css");
Style.Include("Helen.css");
/* Some useful shortcuts or settings
***************************************************************/
Func<dynamic, dynamic> Zone = x => Display(x); // Zone as an alias for Display to help make it obvious when we're displaying zones
/* Layout classification based on filled zones
***************************************************************/
//Add classes to the wrapper div to toggle aside widget zones on and off
var asideClass = CalcuClassify(new [] {"Sidebar"}, "aside-"); // for aside-1, aside-2 or aside-12 if any of the aside zones are filled
if (HasText(asideClass)) {
Model.Classes.Add(asideClass);
}
//Add classes to the wrapper div to toggle tripel widget zones on and off
var tripelClass = CalcuClassify(new [] {"TripelFirst", "TripelSecond", "TripelThird"}, "tripel-"); // for tripel-1, triple-2, etc. if any of the tripel zones are filled
if (HasText(tripelClass)) {
Model.Classes.Add(tripelClass);
}
//Add classes to the wrapper div to toggle quad widget zones on and off
var footerQuadClass = CalcuClassify(new [] {"FooterQuadFirst", "FooterQuadSecond", "FooterQuadThird", "FooterQuadFourth"}, "split-"); // for quad-1, quad-2, etc. if any of the quad zones are filled
if (HasText(footerQuadClass)) {
Model.Classes.Add(footerQuadClass);
}
var slideshowClass = CalcuClassify(new[] {"HomeSlideshow"}, "slideshow-");
if (HasText(slideshowClass)) {
Model.Classes.Add(slideshowClass);
}
/* Inserting some ad hoc shapes
***************************************************************/
//WorkContext.Layout.Header.Add(New.Branding(), "5"); // Site name and link to the home page
//WorkContext.Layout.Footer.Add(New.BadgeOfHonor(), "5"); // Powered by Orchard
WorkContext.Layout.Footer.Add(New.User(), "10"); // Login and dashboard links
/* Last bit of code to prep the layout wrapper
***************************************************************/
Model.Id = "layout-wrapper";
var tag = Tag(Model, "div"); // using Tag so the layout div gets the classes, id and other attributes added to the Model
}
#tag.StartElement
<a name="top"></a>
<div id="SiteHeader">
</div>
<div id="PageContainer">
<div style="position: absolute; Left:-80px; top:-88px;z-index:1000;">
<img id="bird" title="Pheasant" src="/Themes/TheFarmsBlogs/Styles/Images/PositionedImages/pheasant.gif" />
</div>
<div class="SiteMenu"><p>Hello Menu</p></div>
<div id="Specialized">
<div id="PageName">
<!--
PageName NOT in use!
-->
</div>
#if (Model.RightColumn != null) {
<div id="RightCol">
#Zone(Model.RightColumn)
</div>
}
<!-- Page divided into two main columns, of which the left column is subdivided as necessary -->
<div id="LeftCol">
<div id="PageBanner">
<div id="PageBannerLeft">
#if (Model.MainImage != null) {
<div id="PageBannerImage">
#Zone(Model.MainImage)
</div>
}
#if(Model.TheStrip != null) {
<div id="TheStrip">
#Zone(Model.TheStrip)
</div>
}
</div>
</div>
<div id="SpecializedContent">
#if(#Model.content != null)
{
#Zone(Model.content)
}
</div>
</div>
<div id="SpecializedFooter">
</div>
</div>
<div id="PageFooter">
#if (Model.FooterPage != null){
#Zone(Model.FooterPage)
}
</div>
</div>
<div id="SiteFooter">
#Display(Model.Footer)
The Farms Ltd - © 2007
</div>
#tag.EndElement
PS: the branding and badge of honour are commented out as I am only enabling bit by bit to eliminate the source of errors. It will be in the live site.
ADDENDUM:
See Bertrand Le Roy's answer below. The Orchard.Users module requires a Content zone with a Capital C. That instantly cured the problem.
I added this as Bertrand's response was tentative, and I wanted to reinforce that the problem was the name of the zone.
In Orchard.Users, look for Controllers/AccountController.cs. In there, there is a LogOn action. It creates a LogOn shape that it then puts in a shape result. This then gets resolved as the Views/LogOn.cshtml template (which you can override in your theme by just dropping a file with the same name in there, for example a copy of the original that you can tweak). The LogOn template will be rendered within the theme's layout, in the Content zone. Does this answer your question?
I think the mistake you made was to name your Content zone content (notice the casing).