I want to render a multi-column tree (i.e. tree table or tree grid) in Angular. My data is a tree of arbitrary depth, and looks something like this:
[
{name: "node 1",
type: "folder",
children: {[
{name:"node 1-1", type:file},
{name:"node 1-2", type:file},
{name:"node 1-3", type:file},
]}
},
...
...
]
I've seen examples that use recursive templates to render this in a ul/li tree, but I need multiple columns, so I think I need a table. So far the best HTML I have come up with to render the above data structure would look something like:
<tr>
<td>
node 1
</td>
<td>
folder
</td>
</tr>
<tr>
<td>
<span class="treetable-padding"/>
node 1-1
</td>
<td>
file
</td>
</tr>
...
...
So, in other words it's just a flat table where the tree depth is represented using a number of span elements, with that number being equal to that level in the hierarchy.
I am not great with html/AngularJS/js in general so I don't know if this is the best way to render my data, but it certainly looks the way I want.
My question is, what's the best way to do this using AngularJS? I know I can build a monolithic chunk of html like this in JavaScript and then wrap that in a custom directive, but I'm looking for a solution that's more in the spirit of Angular, so that I can take advantage of data binding etc. I ultimately want the cells to be editable so I don't want to do anything that will make data binding messy.
I thought of writing some code to flatten my tree into a list of objects that explicitly contain a "depth" attribute. Then maybe I could use ng-repeat. But I thought maybe there is a cleaner way using one or more custom directives
Thanks!
I have something that works now. It works in the way I described in my comment on my question above. Just to recap, I'm using 2 custom directives, both of which are based on ng-repeat. The first one builds a flat table structure with literal values in the class attribute to indicate depth. The second directive uses that depth information to repeat the padding element the appropriate number of times. Both repeaters use transclusion to minimize restrictions/dependencies on the html. The only caveat is that the second repeater needs the depth info to be somewhere in it's ancestry within the DOM.
Here's what the HTML template looks like for producing a tree-table of the structure I described in my question:
<table class="treetable" style="width: 40%;">
<tr class="treetable-node" my-repeat="item in things"
my-repeat-children="children">
<td>
<span my-repeat-padding class="treetable-node-indent"></span>
{{ item.name }}
</td>
<td>
{{ item.type }}
</td>
</tr>
</table>
I based my custom repeater on this example. What makes mine different is that you can specify a "children" property and the directive logic will check for that property on each item, and descend recursively into it if it's there - although it attaches all new elements to the same parent, giving me the flat structure.
It's probably pretty inefficient, and could use some optimization - like the code in the link I provided, it rebuilds everything when there's a change.
myApp.directive("myRepeat", function($parse) {
return {
restrict: "A",
replace: true,
transclude: "element",
link: function (scope, element, attrs, controller, transclude) {
match = attrs.myRepeat.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/);
itemName = match[1];
collectionName = match[2];
childCollectionName = attrs["myRepeatChildren"];
parentElement = element.parent();
elements = [];
scope.$watchCollection(collectionName, function(collection) {
var i, block, childScope;
if (elements.length > 0) {
for(i=0; i<elements.length; i++) {
elements[i].el.remove();
elements[i].scope.$destroy();
};
elements = [];
}
var buildHtml = function(parent, itemList, depth=0) {
for (var i=0; i<itemList.length; i++) {
childScope = scope.$new();
childScope[itemName] = itemList[i];
transclude(childScope, function(clone) {
parentElement.append(clone);
block = {};
block.el = clone;
block.scope = childScope;
block.el.addClass("depth-" + depth);
elements.push(block);
});
/*Recurse if this item has children,
adding the sub-elements to the same
parent so we end up with a flat list*/
if(childCollectionName in itemList[i]) {
buildHtml(parentElement,
itemList[i][childCollectionName],
depth+1);
}
}
}
for (i=0; i<collection.length; i++) {
buildHtml(parentElement, collection);
}
});
}
};
});
The second one is a bit hacky. It's also a transcluding repeater. It searches upwards in the DOM for the depth class attribute, parses out the depth value, and prepends itself that many times into the parent. So it's totally dependent on that depth class set by the first directive. There are probably better ways of doing it. I also didn't bother setting up a watch or anything like that since this is purely cosmetic.
myApp.directive("myRepeatPadding", function($parse) {
return {
restrict: "A",
replace: true,
transclude: "element",
terminal: true,
link: function (scope, element, attrs, controller, transclude) {
var getDepth = function(element) {
classes = element.attr("class");
if (classes) {
match = classes.match(/depth-([\d+])/);
if(match.length > 0) {
return match[1];
} else {
return getDepth(element.parent());
}
} else {
return getDepth(element.parent());
}
}
depth = getDepth(element);
for (var i=0; i<depth; i++) {
transclude(scope, function(clone) {
element.parent().prepend(clone);
block = {};
block.el = clone;
block.scope = scope;
elements.push(block);
});
}
}
};
});
It's far from perfect and I'll have to spend some time improving it. I'm not even sure if the HTML I'm producing is the best way to get the appearance I want - but it does render the way I want while keeping the html template looking reasonably elegant.
Related
I'm practicing creating an API by scraping using cheerio. I'm scraping from this fairly convoluted site:
http://www.vegasinsider.com/nfl/odds/las-vegas/
I'm trying to target the text after these <br> tags within the anchor tag in this <td> element:
<td class="viCellBg1 cellTextNorm cellBorderL1 center_text nowrap"
width="56">
<a class="cellTextNorm" href="/nfl/odds/las-vegas/line-movement/packers-#-
bears.cfm/date/9-05-19/time/2020#BT" target="_blank">
<br>46u-10<br>-3½ -10
</a>
</td>
The code below is what i'm using to target the data I want. The problem I'm having is I don't know how to get that text after the <br> tags. I've tried .find('br') and couldn't get it to work. Here is the code:
app.get("/nfl", function(req, res) {
var results = [];
axios.get("http://www.vegasinsider.com/nfl/odds/las-vegas/").then(function(response) {
var $ = cheerio.load(response.data);
$('span.cellTextHot').each(function(i,element) {
// console.log($(element).text());
var newObj = {
time:$(element).text()
}
$(element).parent().children().each(function(i,thing){
if(i===2){
newObj.awayTeam = $(thing).text();
}
else if (i===4){
newObj.homeTeam = $(thing).text();
}
});
newObj.odds= $(element).parent().next().next().text().trim();
$('.frodds-data-tbl').find('td').next().next().children().each(function(o, oddsThing){
if(o===0){
newObj.oddsThing = $(oddsThing).html();
}
});
res.json(results);
});
});
You can see I am able to output all the text in this box to the newObj.odds value. I was trying to use something like the next line where I'm targeting that td element and loop through and break out each row into its own newObj property, newObj.oddsLine1 and newObj.oddsLine2 for example.
Hope that makes sense. Any help is greatly appreciated.
You can't select text nodes with cheerio, you need to use js dom properties / functions:
$('td a br')[0].nextSibling.nodeValue
Note $(css)[0] will give you the first element as a js object (rather than a cheerio object)
I've read several posts on this but nothing that would really answer my question in an up-to-date way (i.e. UI.insert is depreciated).
So what's the best way of inserting/rendering a Template into a Surface reactively, meaning, not only one data object (renderWithData), but whatever is defined in the Template.helpers should also be updated reactively. Code tried so far:
var div = document.createElement('div');
//UI.insert(UI.render(function() { return Template.hello; }), div);
surface = new famous.core.Surface({
//content: Blaze.toHTML(function() { return Template.hello; }),
//content: div,
content: '<div class="hell"></div>',
size: [window.innerWidth, window.innerHeight],
properties: {...}
});
Blaze.render(function() { return Template.hello}, $('.hell')[0]);
These are 3 different approaches I've tried. First UI.insert inserts it, but handlebars-style Helpers are not recognized. Second, toHTML, no reactivity, even when I put everything into a Tracker.autorun(). Third, Blaze.render(...), doesn't work at all.
Is there an easy solution to this?
one possible answer is to use Famodev
var ReactiveTemplate = famodev.ReactiveTemplate;
var reactive = new ReactiveTemplate({
template: Template.mytemplate,
data: Collection.find().fetch(),
properties: {
}
});
Maybe it will be useful in some way http://github.com/dcsan/moflow
I have a link like this:
<h1>
Title 001 - Stuff
</h1>
I want to style only "Title 001". It's possible to create a css rule to do this?
I don't remember how I did in the past, I think it was something like this:
h1 a[text="Title 001"]
But this doesn't work
And... then I want to know if it possible to do that with "Title XXX" where XXX is a dynamic number.
You can't select by content, but you can use attributes (as you nearly did already).
<h1>
Title 001 - Stuff
</h1>
a[data-content="Title 0001 - Stuff"] {
color: red;
}
Duplicate Content in Attribute
If you would like to avoid using JavaScript, you could duplicate the content (gasp) in an actual attribute, and select based on that attribute:
Title 001 - Stuff
And then select anything that starts with "Title":
a[data-content^="Title"] {
color: red;
}
Manually Test textContent
Alternatively, you'd have to take an approach with JavaScript:
var links = document.querySelectorAll( "a" );
var pattern = /^Title\s\d{3}/;
for ( var i = 0; i < links.length; i++ ) {
if ( pattern.test( links[ i ].textContent ) ) {
links[ i ].classList.add( "distinguish" );
}
}
This is simply one example of how you could add a .distinguish class to all matching elements.
Filtering with jQuery
If you are using jQuery (or a similar utility) you could accomplish this without so much verbosity:
$("a").filter(function () {
return /^Title/.test( $(this).text() );
}).addClass("distinguish");
Isolating "Title :digits:"
If you only want to isolate, and style, the Title XXX portion and you don't have access to the source templates, you could do this too with JavaScript:
$("a").html(function ( index, html ) {
return html.replace(/(Title \d+)/, "<span>$1</span>");
});
The above assumes you are using jQuery, but if you're not you can accomplish the same thing with the following:
var anchors = document.getElementsByTagName("a")
, length = anchors.length
, el;
while ( length-- ) {
el = anchors[ length ];
el.innerHTML = el.innerHTML.replace(/(Title \d+)/, "<span>$1</span>");
}
With css
Title 001 - Stuff
a[data-content^="Title"] {
color: red;
}.
But
Here is what you can do with jQuery in much smarter way,
$('a').filter(function (i, element) {
return element.text == "Title 001 - Stuff";
}).css('color','green');
Working fiddle
I'm afraid this is impossible in CSS.
You're using an attribute selector:
a[text="Title 001"]
and your <a> hasn't got an attribute called text.
You would have to use Javascript to handle such situation. There was once an idea to have a :contains() pseudo-selector but this has never been implemented.
I am using Knockout.js with jQuery tmpl plugin. The template I have defined has a few select lists. I need to expand the width of the select items (on IE 8) when a select list is clicked (to accomodate the longest element in the list). I was thinking of toggling the css class when a user clicks on the select list to achieve this but am not sure how to go about it. Here is what I have so far:
//CSS classes
<style>
.bigclass
{
width: 200px;
}
.normalclass
{
width: auto;
}
</style>
// Call makeBig javascript method on mouseover.
<script id='criteriaRowTemplate' type='text/html'>
<tr>
<td style="width: 23%"><select style="width: 100%" data-bind='event: { mouseover: makeBig, mouseout: makeNormal}, options: criteriaData, optionsText: "Text", optionsCaption: "--Select--", value: SearchCriterion' /></td>
</tr>
</script>
var CriteriaLine = function() {
this.SearchCriterion = ko.observable();
//Control comes to this method. Not sure though if the element captured is correct.
makeBig = function(element) {
$(element).addClass("bigclass")
};
makeNormal = function(element) {
$(element).addClass("normalclass")
};
};
So my questions are:
How do we pass the select list to the makeBig javascript function? I believe I need to change the following syntax in my code:
data-bind='event: { mouseover: makeBig, mouseout: makeNormal
How do we add the class to the passed select list. I have added the code but it's not working, maybe because element doesn't have a value.
Alternatively, is there any other approach to ensure that the user sees the full text of the dropdown in IE 8?
Here is a custom binding to add a CSS class to the element on hovering:
http://jsfiddle.net/BL9zt/
Note that it subscribes to the IE specific mouseenter and mouseleave event, so you also have to reference jQuery which simulates those events in the other browsers.
Another, knockout-independent approach is described here:
http://www.getharvest.com/blog/wp-content/uploads/2009/12/select_box_demo.html
I'd like to attach images to specific words but cannot find the right CSS selector to do so.
I have a portion of my site which displays data as it's pulled from a database, so adding classes or id's to certain words is not an option for me. I need the css to simply display a background image wherever that word (or in this case, name) is found on the page.
For example, in the following (which is pulled from a database):
<td class="data1"><font face="Verdana, Arial, Helvetica, sans-serif" size="1">Patrick</font></td>
I would like to add a background image where the name Patrick is found.
I tried variations of,
td[.table1 *='Parick'] {
background-image:url(../images/accept.png);
but that didn't get me anywhere. And since it's not in a <span> or <div> or even a link, I can't figure it out. If you have any ideas or a jQuery workaround, please let me know. Thanks!
If you can guarantee the names only appear as the only text nodes in elements, you can use a simple jQuery selector...
$(':contains("Patrick")').addClass('name');
jsFiddle.
If there may be surrounding whitespace and/or the search should be case insensitive, try...
$('*').filter(function() {
return $.trim($(this).text()).toLowerCase() == 'patrick';
}).addClass('name');
jsFiddle.
If you need to find the name anywhere in any text node and then you need to wrap it with an element, try...
$('*').contents().filter(function() {
return this.nodeType == 3;
}).each(function() {
var node = this;
this.data.replace(/\bPatrick\b/i, function(all, offset) {
var chunk = node.splitText(offset);
chunk.data = chunk.data.substr(all.length);
var span = $('<span />', {
'class': 'name',
text: all
});
$(node).after(span);
});
});
jsFiddle.
I would recommend using the third example.