markedjs from React component - stopped capturing the headers - javascript

Strange problem with marked executed within my React component.
Has been working, but cannot see why it's stopped.
This is my React component
** #jsx React.DOM */
var React = require('react');
var marked = require('marked');
var Panel = require('react-bootstrap').Panel;
var Highlight = require('highlight.js');
var Markdown = React.createClass({
componentWillMount:function( ){
marked.setOptions({
highlight: function (code) {
return Highlight.highlightAuto(code).value;
},
sanitize: true
});
},
render: function () {
var rawMarkup;
if (_.isString(this.props.content)) {
rawMarkup = marked(this.props.content);
}
else if (_.isArray(this.props.content)) {
rawMarkup = marked(this.props.content.join("\n"));
}
return (
<div>
<span dangerouslySetInnerHTML={{__html: rawMarkup}} />
</div>
);
}
});
exports.Markdown = Markdown;
Tracing it through, the problem appear to lie here:
(this is extract of marked js)
// heading
if (cap = this.rules.heading.exec(src)) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'heading',
depth: cap[1].length,
text: cap[2]
});
continue;
}
Where this.rules.heading == /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/
and src == "#heading"
Nothing is found.
I have updated npm packages, but don't see how this would have stopped working?
Any ideas?

Related

How can I recreate Squarespace's Product Quick View in standard Product View?

Overview
So I'm trying to take functionality from one part of Squarespace's Galapagos commerce template and add it to another but it's proving to be more difficult than I thought.
I need the image-swapping capability of the "Quick View" (example - mouse over any image and click Quick View ) to replace the column of full sized zoomable images in the "Product View" (example - you see this once you click on a product).
So I found the code for each section:
Product View
This code simply goes through each image in the array and spits it out with the id jsProductItemImages which allows it to be hovered and zoomed.
<div class="productitem-images" id="jsProductItemImages">
{.repeated section items}
{.image?}
<div class="productitem-image-zoom-wrapper sqs-image-zoom-area"><img data-load="false" class="productitem-image loading" {#|image-meta} /></div>
{.end}
{.video?}
{#|video}
{.end}
{.end}
</div>
Quick View
I'm not 100% on the logic here, but essentially it's grabbing the first image and making it a hover/zoomable primary image then listing the entire array of images beneath it as thumbnails. I read that the # symbol is the equivalent of saying this in javascript, but I don't get why it's being used to spit out only the first image in the array.
<figure class="ProductItem-gallery">
{.section items}
<div class="ProductItem-gallery-slides">
{.repeated section #}
{.image?}
<div class="ProductItem-gallery-slides-item" data-slide-index="{#index}"><img class="ProductItem-gallery-slides-item-image" data-load="false" {#|image-meta} /></div>
{.end}
{.video?}
{#|video}
{.end}
{.end}
</div>
{.end}
<div class="ProductItem-gallery-thumbnails">
{.if items.1}{.repeated section items}<div class="ProductItem-gallery-thumbnails-item"><img class="ProductItem-gallery-thumbnails-item-image" data-load="false" {#|image-meta} /></div>{.end}{.end}
</div>
</figure>
Associated JS
First off, it should be noted that I went through and console logged every function to see what was giving the Quick View it's functionality - to no avail. Which is subsequently why I'm here. So it's easy to see where the zoom function is originating: the Product View in the Galapagos.ProductItem function on line 103 $imageContainer = Y.one('#jsProductItemImages');
But I don't see anything out of the ordinary pop up when I look at the Quick View. I've got to be missing something!
var Galapagos = {};
Y.use('node', function(Y) {
Galapagos.Site = (function(){
console.log("Galapagos.Site");
var $productPage;
function init() {
console.log("Galapagos.Site init()");
$productPage = Y.one('.collection-type-products');
if( $productPage && $productPage.hasClass('view-list') ) Galapagos.ProductList.init();
if( $productPage && $productPage.hasClass('view-item') ) Galapagos.ProductItem.init();
addDesktopTouchscreenClass();
addMediaQueryBreakpointClass();
bindEventListeners();
}
function addDesktopTouchscreenClass() {
console.log("Galapagos.Site addDesktopTouchscreenClass()");
if (Y.one('html').hasClass('touch')) {
var mousemoveDetection = Y.on('mousemove', function(){
Y.one('body').addClass('galapagos-desktop-touchscreen');
mousemoveDetection.detach();
});
}
}
function addMediaQueryBreakpointClass() {
console.log("Galapagos.Site addMediaQueryBreakpointClass()");
if( document.documentElement.clientWidth <= 724 ) {
if (Y.one('.catnav-container')) Y.one('.nav-container').prepend(Y.one('.catnav-list'));
Y.one('html').addClass('tablet-breakpoint-mixin');
} else {
if (Y.one('.catnav-container')) Y.one('.catnav-container').prepend(Y.one('.catnav-list'));
Y.one('html').removeClass('tablet-breakpoint-mixin');
}
}
function bindEventListeners() {
console.log("Galapagos.Site bindEventListeners()");
Y.on('resize', addMediaQueryBreakpointClass);
}
function getDocWidth() {
console.log("Galapagos.Site getDocWidth()");
return Y.one(document).get('docWidth');
}
function getDocHeight() {
console.log("Galapagos.Site getDocHeight()");
return Y.one(document).get('docHeight');
}
return {
init:init,
getDocWidth: getDocWidth,
getDocHeight: getDocHeight
}
}());
Galapagos.TweakListener = (function(){
console.log("Galapagos.TweakListener");
function listen(tweakName, callBack) {
if (Y.Global) {
Y.Global.on('tweak:change', Y.bind(function(f){
if ((f.getName() == tweakName) && (typeof callBack === 'function')) {
callBack(f.getValue());
}
}));
}
}
return {
listen:listen
}
}());
Galapagos.ProductItem = (function(){
console.log("Galapagos.ProductItem");
var cat;
var $imageContainer;
var $images;
var imageZoomInstances = [];
function init() {
console.log("Galapagos.ProductItem init()");
cat = Y.QueryString.parse(location.search.substring(1)).category;
$imageContainer = Y.one('#jsProductItemImages');
$images = $imageContainer.all('img[data-src]');
if ( cat ) setCatCrumb();
loadProductDetailImages();
bindEventListeners();
bindTweakListeners();
buildProductDetailImagesLightbox();
}
function bindEventListeners() {
console.log("Galapagos.ProductItem bindEventListeners()");
Y.on('resize', function(){
loadProductDetailImages();
});
}
function setCatCrumb() {
console.log("Galapagos.ProductItem setCatCrumb()");
var $catCrumb = Y.one('#jsCategoryCrumb');
var $catCrumbLink = $catCrumb.one('a');
var catCrumbHref = $catCrumbLink.getAttribute('href');
//var $mobileCatCrumbLink = Y.one('#jsMobileCategoryCrumb');
$catCrumbLink.set('text', cat).setAttribute('href', catCrumbHref + '?category=' + encodeURIComponent(cat));
//$mobileCatCrumbLink.setAttribute('href', catCrumbHref + '?category=' + encodeURIComponent(cat));
$catCrumb.removeClass('galapagos-display-none');
}
function loadProductDetailImages() {
console.log("Galapagos.ProductItem loadProductDetailImages()");
var imageZoomEnabled = Y.one('.tweak-product-item-image-zoom-enabled');
$images.each(function(image) {
ImageLoader.load(image.removeAttribute('data-load'), { load:true });
if (imageZoomEnabled) {
image.on('load', function() {
instantiateImageZoom(image);
});
}
});
}
function instantiateImageZoom(image) {
console.log("Galapagos.ProductItem instantiateImageZoom()");
imageZoomInstances.push(new Y.Squarespace.ImageZoom({
host: image.get('parentNode'),
behavior: 'hover',
zoom: parseFloat(Y.Squarespace.Template.getTweakValue('tweak-product-item-image-zoom-factor'))
}));
}
function destroyImageZoomInstances() {
console.log("Galapagos.ProductItem destroyImageZoomInstances()");
if (!imageZoomInstances || imageZoomInstances.length < 1) {
return;
}
Y.Array.each(imageZoomInstances, function(zoomInstance){
zoomInstance.destroy(true);
});
}
function buildProductDetailImagesLightbox() {
console.log("Galapagos.ProductItem buildProductDetailImagesLightbox()");
if ($images.size() >= 1 ) {
var lightboxSet = [];
$images.each(function(image) {
lightboxSet.push({
content: image
});
});
// Only show controls for size > 1
var hasControls = $images.size() > 1;
$imageContainer.delegate('click', function(e) {
var lightbox = new Y.Squarespace.Lightbox2({
controls: {
previous: hasControls,
next: hasControls
},
set: lightboxSet,
currentSetIndex: $images.indexOf(e.target)
});
lightbox.render();
}, 'img', this);
}
}
function bindTweakListeners() {
console.log("Galapagos.ProductItem bindTweakListeners()");
if (Y.Global) {
Y.Global.on('tweak:close', function() {
if (Y.one('.collection-type-products.view-item')) {
destroyImageZoomInstances();
if (Y.one('.tweak-product-item-image-zoom-enabled')) {
$images.each(function(image){
instantiateImageZoom(image);
});
}
}
}, this);
}
}
return {
init:init
}
}());
Galapagos.ProductList = (function(){
console.log("Galapagos.ProductList");
var $catNav,
$productGrid,
$productGridOrphans,
$productGridImages,
$orphanProducts,
productCount,
maxGridUnit,
orphanProductCount,
isGridBuilt;
function init() {
console.log("Galapagos.ProductList init()");
$catNav = Y.one('#jsCatNav');
$productGrid = Y.one('#jsProductGrid');
$productGridOrphans = Y.one('#jsProductGridOrphans');
if (!Y.UA.mobile && Y.one('.show-alt-image-on-hover:not(.product-info-style-overlay)')) {
$productGridImages = $productGrid.all('img[data-src]');
} else {
$productGridImages = $productGrid.all('img.productlist-image--main[data-src]');
}
productCount = $productGrid.all('.productlist-item').size();
maxGridUnit = 8;
orphanProductCount;
isGridBuilt = false;
bindEventListeners();
bindTweakListeners();
if($catNav) setActiveCategory();
if(Y.one('body').hasClass('product-grid-style-organic')) {
buildGrid();
} else {
$productGrid.removeClass('loading').removeClass('loading-height');
loadGridImages($productGridImages);
}
}
function bindEventListeners() {
console.log("Galapagos.ProductList bindEventListeners()");
Y.on('resize', function(){
loadGridImages($productGridImages);
});
}
function buildGrid() {
console.log("Galapagos.ProductList buildGrid()");
for (var i = maxGridUnit; i > 0; i--) {
orphanProductCount = productCount % i;
if(productCount <= maxGridUnit || i > 4) {
if(0 === orphanProductCount) {
$productGrid.addClass('item-grid-' + i);
isGridBuilt = true;
break;
}
} else {
if(0 === productCount % 9) { // if productCount is a multiple of 9, use the 9-grid. we use 9-grid only for multiples of 9 because 8-grid looks more interesting.
$productGrid.addClass('item-grid-' + 9);
} else { // otherwise, use the 8-grid and put the remainder into the orphan div
$productGrid.addClass('item-grid-' + maxGridUnit);
$orphanProducts = Y.all('.productlist-item').slice((productCount % maxGridUnit) * -1);
$productGridOrphans
.append($orphanProducts)
.addClass('item-grid-' + productCount % maxGridUnit);
}
isGridBuilt = true;
break;
}
}
if(isGridBuilt) {
$productGrid.removeClass('loading').removeClass('loading-height');
loadGridImages();
}
}
function setActiveCategory() {
console.log("Galapagos.ProductList setActiveCategory()");
var catNavItemCount = $catNav.all('.catnav-item').size();
for (var i = catNavItemCount - 1; i > 0; i--) {
var $item = $catNav.all('.catnav-item').item(i);
var $link = $item.one('.catnav-link');
var category = Y.QueryString.parse(location.search.substring(1)).category;
var href = Y.QueryString.parse($link.getAttribute('href').substring(2)).category;
if(category && href && category === href) {
$item.addClass('active-link');
}
else if(!category) {
$catNav.one('#jsCatNavRoot').addClass('active-link');
}
}
}
function loadGridImages() {
console.log("Galapagos.ProductList loadGridImages()");
$productGridImages.each(function(image) {
ImageLoader.load(image.removeAttribute('data-load'), { load: true });
image.on('load', function(){
if (image.hasClass('productlist-image--main.has-alt-image')) {
image.siblings('.productlist-image--alt').removeClass('galapagos-hidden');
}
});
});
}
function bindTweakListeners() {
console.log("Galapagos.ProductList bindTweakListeners()");
if (Y.Global) {
Y.Global.on(['tweak:beforeopen', 'tweak:close', 'tweak:reset'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
}, 1000);
});
Y.Global.on(['tweak:beforeopen'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
$productGrid.one('.productlist-item').addClass('is-hovered');
}, 1000);
});
Y.Global.on(['tweak:close'], function() {
setTimeout(function(){
Galapagos.ProductList.init();
$productGrid.one('.productlist-item').removeClass('is-hovered');
}, 1000);
});
}
Galapagos.TweakListener.listen('product-grid-style', function(value) {
if('Organic' === value) {
buildGrid();
} else {
$productGrid.append($orphanProducts);
loadGridImages();
}
});
Galapagos.TweakListener.listen('product-info-style', function(value) {
if('Overlay' === value) {
$productGrid.one('.productlist-item').addClass('is-hovered');
} else {
$productGrid.one('.productlist-item').removeClass('is-hovered');
}
});
Galapagos.TweakListener.listen('productImageAspectRatio', function(value) {
loadGridImages();
});
Galapagos.TweakListener.listen('productImageSpacing', function(value) {
loadGridImages();
});
}
return {
init:init
}
}());
Y.on('domready', function() {
Galapagos.Site.init();
});
});
My Attempts
My first few attempts have been dropping the jsProductItemImages div from the Product view and dumping in the entire figure block from the Quick View then updating the associated css. While it pulls in the images (I can see them in the inspector and they take up space on the page) it shows up as being blank.
I also tried only using the thumbnails section from the Quick View and limiting the Product View to only show the first image by using {.section items.0} but then any thumbnail I clicked wouldn't swap out without writing the script for it (obviously) but I didn't want to write something like that when I know it exists in the code already!
Any help would be greatly appreciated!
UPDATE:
After replacing the product view markup with the quick view markup I ran into these errors
Uncaught TypeError: Cannot read property 'all' of null site.js:104
init # site.js:104
init # site.js:17
(anonymous function) # site.js:432
_notify # common-2a739bf…-min.js:1479
notify # common-2a739bf…-min.js:1479
_notify # common-2a739bf…-min.js:1475
_procSubs # common-2a739bf…-min.js:1476
fireSimple # common-2a739bf…-min.js:1476
_fire # common-2a739bf…-min.js:1476
fire # common-2a739bf…-min.js:1489
_load # common-2a739bf…-min.js:1463
f # common-2a739bf…-min.js:1457
Unsure why it's hitting an error with .all because it should be addressing the same array of images in both situations?
There's a few questions buried in the this post but let me answer the Quick View question specifically since that's what you're looking to "fix".
Squarespace uses a modular system of JavaScript/CSS add-ons called "rollups". If you pull the source code you'll see a window object that contains the current configuration of any given page. When visiting a Products page, the system triggers the use of their quick view JS and accommodating CSS file. This is where you'll want to be looking. The JS you're digging into is not relevant to Quick View (I don't believe).
Quick View Rollup JS: http://static.squarespace.com/universal/scripts-compressed/product-quick-view-6a1e5642b473ebbb5944-min.js
Quick View Rollup CSS: http://static.squarespace.com/universal/styles-compressed/product-quick-view-eb4b900ac0155bed2f175aa82e2a7c17-min.css
These rollups are triggered off of JavaScript hooks in the template files. What you'll need to do is experiment with using the Galapagos product template word and word so it has the same classes and data-attributes, and see if that works. It would take far too long to cover all of the details of what you need to do without actually working on the project. I would start here first and see if you can setup your product template to triggers the Quick View JS as is, without customization.

How to get element from component

I Have some script, that takes element, but i cant find how to get it from react component. At least, i can get element after rendering, but now i have another problem - cant make event on button, coz i need SignatureObj
var SignaturePad = require('signature_pad');
var SignatureWidget = React.createClass({
_handleClear: function(e, SignatureObj) {
SignatureObj.clear();
},
_getSignature: function(canvas) {
return new SignaturePad(canvas);
},
componentDidMount: function() {
var wrapper = this.getDOMNode(),
canvas = wrapper.querySelector('canvas'),
SignatureObj = this._getSignature(canvas);
},
render: function() {
var canvas = React.createElement('canvas');
return (
<div>
{canvas}
<button>Clear</button>
</div>
);
}
});
Finally, i need to get something like this
render: function() {
var canvas = React.createElement('canvas'),
>> canvasEl = canvas.getElement(),<< magic
SignatureObj = this._getSignature(canvasEl);
return (
<div>
{canvas}
<button onClick={this._handleClear.bind(this, SignatureObj)}>Clear</button>
</div>
);
}
first some side notes, getDOMNode is deprecated, u should use findDOMNode, further u don't need to use create element within the render, because if you use the jsx syntax which u do it will be converted to the syntax u are using (createlement ..etc .. under the hood)
for your problem about selecting the element:
React.findDOMNode(this.refs.findMe)
<-- makes u find the node or as u call it :D (Magic)
render: function() {
return (
<div>
<canvas ref='findMe'></canvas> <------- magic
<button>Clear</button>
</div>
);
}
Hope this helps!
some good reads:
refs:
https://facebook.github.io/react/docs/more-about-refs.html
jsx:
https://facebook.github.io/react/docs/jsx-in-depth.html
i used this way to add event
componentDidMount: function() {
var pad = this.refs.pad.getDOMNode(),
saveButton = this.refs.save.getDOMNode(),
clearButton = this.refs.clear.getDOMNode(),
SignatureObj = this._getSignature(pad);
clearButton.onclick = this._handleClear.bind(this, SignatureObj);
saveButton.onclick = this._handleSave.bind(this, SignatureObj);
},

React.js: Managing State and Component Rerender

I've hit a wall as I start my adventure with React.js. I've got the UI of the following time tracking app working on several levels:
http://jsfiddle.net/technotarek/4n8n17tr/
What's working as hoped:
Filtering based on user input
Project clocks can be started and stopped independently
What's not working:
If you start one or more clocks and then try to filter, any clock that's not in the filter result set gets reset once it is re-displayed. (Just click the start on all clocks, then search for a project, then clear your search input.)
I assume this is happening because a setState is run onChange of the filter input, which is re-rendering everything and using the clock getInitialState values.
So, what's the correct way to preserve the 'state' of these clocks and the buttons when the filter re-renders the components? Should I not be storing the clock or the button 'states' as genuine React states? Do I need a function to explicitly save the clock values before the re-render?
I'm not asking for anyone to fix my code. Rather, I'm hoping for a pointer in where my understanding of React is failing.
To satisfy SO's code requirement, below is the component that contains each row in the time tracker. The clocks are started via toggleClock. IncrementClock writes the state that is getting cleared out by the search filter. Please see the complete code in the fiddle link above.
var LogRow = React.createClass({
getInitialState: function() {
return {
status: false,
seconds: 0
};
},
toggleButton: function(status) {
this.setState({
status: !this.state.status
});
this.toggleClock();
},
toggleClock: function() {
var interval = '';
if(this.state.status){
// if clock is running, pause it.
clearInterval(this.interval);
} else {
// otherwise, start it
this.interval = setInterval(this.incrementClock, 1000);
}
},
incrementClock: function() {
this.setState({ seconds: this.state.seconds+1 });
},
render: function() {
var clock = <LogClock seconds={this.state.seconds} />
return (
<div>
<div className="row" key={this.props.id}>
<div className="col-xs-7"><h4>{this.props.project.title}</h4></div>
<div className="col-xs-2 text-right">{clock}</div>
<div className="col-xs-3 text-right"><TriggerButton status={this.state.status} toggleButton={this.toggleButton} /></div>
</div>
<hr />
</div>
);
}
})
When you filter, you're removing LogRow components from the rendered output - when this happens, React unmounts the component and disposes of its state. When you subsequently change the filter and a row is once again displayed, you're getting an entirely new LogRow component, so getInitialState() is called again.
(You also have a leak here because you're not clearing the interval when these components unmount using the componentWillUnmount() lifecycle hook - those intervals are still running away the background)
To solve this, you could move the timer state and the methods which control and increment it up out of the LogRow component, so its job is just to display and control the current state but not to own it.
You're currently using the LogRow component to tie the state and behaviour of a project timer together. You could either move this state and behaviour management up to a parent component which will manage it the same way, or out into another object, e.g.:
function Project(props) {
this.id = props.id
this.title = props.title
this.ticking = false
this.seconds = 0
this._interval = null
}
Project.prototype.notifyChange = function() {
if (this.onChange) {
this.onChange()
}
}
Project.prototype.tick = function() {
this.seconds++
this.notifyChange()
}
Project.prototype.toggleClock = function() {
this.ticking = !this.ticking
if (this.ticking) {
this.startClock()
}
else {
this.stopClock()
}
this.notifyChange()
}
Project.prototype.startClock = function() {
if (this._interval == null) {
this._interval = setInterval(this.tick.bind(this), 1000)
}
}
Project.prototype.stopClock = function() {
if (this._interval != null) {
clearInterval(this._interval)
this._interval = null
}
}
Since the clearIntervals being used are an external source of change, you'd need to subscribe to them somehow, so I've implemented the ability to register a single onChange callback, which the LogRow component is doing when it mounts in the snippet below.
The working code snippet below does the most simple and direct thing possible to achieve this and as a result the solution has some discouraged practices (modifying props) and caveats (you can only have one "listener" on a Project) but it works. (This is generally my experience with React - it works first, then you make it "right" afterwards).
Next steps could be:
PROJECTS is effectively a singleton Store - you could make it an object which allows registration of listeners for changes to project state. You could then add an Action object to encapsulate triggering changes to project state so LogRow never touches its project prop directly, only reads from it and calls sideways to an Action to change it. (This is just indirection, but helps with thinking about data flow). See the Less Simple Communication example in the react-trainig repo for a worked example of this.
You could make LogRow completely dumb by listening for all project changes at a higher level and re-rendering everything on change. Passing individual project props to LowRow would then allow you to implement shouldComponentUpdate() so only rows which need to display a change actually re-render.
<meta charset="UTF-8">
<script src="http://fb.me/react-with-addons-0.12.2.js"></script>
<script src="http://fb.me/JSXTransformer-0.12.2.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet">
<div class="container">
<div class="row">
<div id="worklog" class="col-md-12">
</div>
</div>
</div>
<script type="text/jsx;harmony=true">void function() { "use strict";
/* Convert seconds input to hh:mm:ss */
Number.prototype.toHHMMSS = function () {
var sec_num = parseInt(this, 10);
var hours = Math.floor(sec_num / 3600);
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
var seconds = sec_num - (hours * 3600) - (minutes * 60);
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
if (seconds < 10) {seconds = "0"+seconds;}
var time = hours+':'+minutes+':'+seconds;
return time;
}
function Project(props) {
this.id = props.id
this.title = props.title
this.ticking = false
this.seconds = 0
this._interval = null
}
Project.prototype.notifyChange = function() {
if (typeof this.onChange == 'function') {
this.onChange()
}
}
Project.prototype.tick = function() {
this.seconds++
this.notifyChange()
}
Project.prototype.toggleClock = function() {
this.ticking = !this.ticking
if (this.ticking) {
this.startClock()
}
else {
this.stopClock()
}
this.notifyChange()
}
Project.prototype.startClock = function() {
if (this._interval == null) {
this._interval = setInterval(this.tick.bind(this), 1000)
}
}
Project.prototype.stopClock = function() {
if (this._interval != null) {
clearInterval(this._interval)
this._interval = null
}
}
var PROJECTS = [
new Project({id: "1", title: "Project ABC"}),
new Project({id: "2", title: "Project XYZ"}),
new Project({id: "3", title: "Project ACME"}),
new Project({id: "4", title: "Project BB"}),
new Project({id: "5", title: "Admin"})
];
var Worklog = React.createClass({
getInitialState: function() {
return {
filterText: '',
};
},
componentWillUnmount: function() {
this.props.projects.forEach(function(project) {
project.stopClock()
})
},
handleSearch: function(filterText) {
this.setState({
filterText: filterText,
});
},
render: function() {
var propsSearchBar = {
filterText: this.state.filterText,
onSearch: this.handleSearch
};
var propsLogTable = {
filterText: this.state.filterText,
projects: this.props.projects
}
return (
<div>
<h2>Worklog</h2>
<SearchBar {...propsSearchBar} />
<LogTable {...propsLogTable} />
</div>
);
}
});
var SearchBar = React.createClass({
handleSearch: function() {
this.props.onSearch(
this.refs.filterTextInput.getDOMNode().value
);
},
render: function() {
return (
<div className="form-group">
<input type="text" className="form-control" placeholder="Search for a project..." value={this.props.filterText} onChange={this.handleSearch} ref="filterTextInput" />
</div>
);
}
})
var LogTable = React.createClass({
render: function() {
var rows = [];
this.props.projects.forEach(function(project) {
if (project.title.toLowerCase().indexOf(this.props.filterText.toLowerCase()) === -1) {
return;
}
rows.push(<LogRow key={project.id} project={project} />);
}, this);
return (
<div>{rows}</div>
);
}
})
var LogRow = React.createClass({
componentDidMount: function() {
this.props.project.onChange = this.forceUpdate.bind(this)
},
componentWillUnmount: function() {
this.props.project.onChange = null
},
onToggle: function() {
this.props.project.toggleClock()
},
render: function() {
return <div>
<div className="row" key={this.props.id}>
<div className="col-xs-7">
<h4>{this.props.project.title}</h4>
</div>
<div className="col-xs-2 text-right">
<LogClock seconds={this.props.project.seconds}/>
</div>
<div className="col-xs-3 text-right">
<TriggerButton status={this.props.project.ticking} toggleButton={this.onToggle}/>
</div>
</div>
<hr />
</div>
}
})
var LogClock = React.createClass({
render: function() {
return (
<div>{this.props.seconds.toHHMMSS()}</div>
);
}
});
var TriggerButton = React.createClass({
render: function() {
var button;
button = this.props.status != false
? <button className="btn btn-warning" key={this.props.id} onClick={this.props.toggleButton}><i className="fa fa-pause"></i></button>
: <button className="btn btn-success" key={this.props.id} onClick={this.props.toggleButton}><i className="fa fa-play"></i></button>
return (
<div>
{button}
</div>
);
}
})
React.render(<Worklog projects={PROJECTS} />, document.getElementById("worklog"));
}()</script>

React: Tagging active component in an array with a CSS class

I am loading a series of sentences into an array Sentences in React. On my front end I have one "active" sentence and after user form input the next sentence in the loaded array should become "active" with a new CSS class attached to it.
How would I go about doing this? I understand SentenceList needs to keep track of the active_sentence and the Sentence needs to tell SentenceList has been updated, and when it does it should set the class "active" to the next sentence sequentially. But I'm not sure how to implement it.
SentenceList:
var SentenceList = React.createClass({
render: function() {
var sentences = [];
active_sentence = 0;
//loop through sentences and push each sentence into array
this.props.sentences.forEach(function(sentence) {
// increment an index variable here and put an if index === active_sentence statement?
//grabs #sentences from Rails
sentences.push(<Sentence key={sentence.id} details={sentence} />)
});
return (
<div>{sentences}</div>
)
}
});
Sentence:
var Sentence = React.createClass({
getInitialState: function() {
return {
//
}
},
addBlip: function(e) {
var blipBody = this.refs.newBlip.getDOMNode().value;
var sentenceId = this.props.details.id;
var thisSentenceComponent = this;
$.ajax({
url: '/sentences/' + sentenceId + '/blips',
type: 'POST',
dataType: 'json',
data: {blip: {body: blipBody}}
});
e.preventDefault();
},
render: function() {
//get user input and submit blip and make next sentence "active"
var phrase = this.props.details.body;
var phrase_display = phrase.split("*");
return (
<div className="blipForm">
{phrase_display[0]}
{this.props.details.index}
<form onSubmit={this.addBlip}>
<input
type="text"
ref="newBlip"
/>
</form>
{phrase_display[1]}
</div>
)
}
});
You can create a variable in state (activeKey in this example) to keep track of which sentence key is considered active, then a prop can be passed to Sentence telling it whether it is active. The function setActiveKey below can be used to update the active sentence when you load a new sentence :
var SentenceList = React.createClass({
getInitialState: function() {
return {
activeKey: false
};
},
setActiveKey(newActiveKey){
this.setState({activeKey: newActiveKey});
},
render: function() {
var sentences = [];
this.props.sentences.forEach(function(sentence) {
sentences.push(<Sentence isActive={this.state.activeKey === sentence.id} key={sentence.id} details={sentence} />)
}.bind(this));
return (
<div>{sentences}</div>
)}
});
Then within the render function of Sentence, you can the prop isActive, and if the value is true, you can render it with the active style:
render: function() {
var phrase = this.props.details.body
var phrase_display = phrase.split("*");
return (
<div className="blipForm" style={this.props.isActive ? styles.active : styles.inactive}>
{phrase_display[0]}
{this.props.details.index}
<form onSubmit={this.addBlip}>
<input type="text"
ref="newBlip" />
</form>
{phrase_display[1]}
</div>
)
}
And you can control your styles in a variable (styles here):
var styles={
active:{
//set active styles here
},
inactive:{
//set inactive styles here
}
};

Dynamically generating components in React-Bootstrap

I'm trying to dynamically generate alert components in React-Bootstrap at runtime by instantiating the components from Javascript classes. I'm doing this because there are lots of alerts to show and because Javascript classes are more succinct to create.
My attempts to do this are not working. I'm not sure whether the problem generally applies to React or just to React-Bootstrap. However, the error occurs in react.js, which throws the following:
TypeError: undefined is not a function
The throw occurs in the alert.getComponent() call in the following JSX file:
/** #jsx React.DOM */
var Alert = ReactBootstrap.Alert;
var AlertDismissible = React.createClass({
getInitialState: function() {
return {
isVisible: true
};
},
render: function() {
if(!this.state.isVisible)
return null;
var message = this.props.message;
if(this.props.code !== null)
message = message +'(Code '+ this.props.code +')';
return (
<Alert bsStyle={this.props.level} onDismiss={this.dismissAlert}>
<p>{message}</p>
</Alert>
);
},
dismissAlert: function() {
this.setState({isVisible: false});
}
});
function AlertNotice(level, message, code) {
this.level = level;
this.message = message;
this.code = code || null;
}
AlertNotice.prototype.getComponent = function() {
// What should go here? Using React.createClass() doesn't help
return (
<AlertDismissible level={this.level} message={this.message}
code={this.code} />
);
};
function SuccessAlert(message) {
AlertNotice.call(this, 'success', message);
}
SuccessAlert.prototype = Object.create(AlertNotice);
SuccessAlert.prototype.constructor = SuccessAlert;
/* ...more kinds of alerts... */
function ErrorAlert(message, code) {
AlertNotice.call(this, 'danger', message, code);
}
ErrorAlert.prototype = Object.create(AlertNotice);
ErrorAlert.prototype.constructor = ErrorAlert;
var SomethingWithAlerts = React.createClass({
render: function() {
var alerts = [
new ErrorAlert("Goof #1", 123),
new ErrorAlert("Goof #2", 321)
].map(function(alert) {
// react.js throws "TypeError: undefined is not a function"
return alert.getComponent();
});
return (
<div>{alerts}</div>
);
}
});
var TestComponent = (
<div>
<SomethingWithAlerts />
</div>
);
React.renderComponent(
TestComponent,
document.getElementById('content')
);
The Alert component comes from the React-Bootstrap library. The div components seem extraneous but I found them necessary to satisfy the react framework. In reality, I'll be storing the AlertNotice instances in react state and then generating react nodes from them.
What is the proper way to go about this?
Here's a hint. If I replace return alert.getComponent(); with the following hardcoded alert, the AlertDismissible components render without error (in duplicate) but I get a warning:
return (
<AlertDismissible level="danger" message="Goof" code="777" />
);
The following is the warning message I get with the above replacement, including a link that explains I should set key= to a unique for each alert:
Each child in an array should have a unique "key" prop. Check the render method
of SpecimenSetManager. See http://fb.me/react-warning-keys for more information.
However, if I simply replace the code inside of AlertNotice.prototype.getComponent with the above hardcoded alert, I get the same TypeError message as before.
For completeness, here is my HTML source. This is react and react-boostrap v0.11.1
<html>
<head>
<script src="lib/react.js"></script>
<script src="lib/react-bootstrap.js"></script>
<script src="lib/JSXTransformer.js"></script>
<link rel="stylesheet" href="css/bootstrap-theme.min.css">
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<div id="content"></div>
<script src="components.js" type="text/jsx"></script>
</body>
</html>
I solved the problem. The solution was to create a special react component that represents a set of alerts. Apparently it is only possible to reference automatic or object variables in component parameters from within the React.createClass() definition. Perhaps this is a syntactic constraint of JSX rather than a logical constraint of react.
I don't understand why this solution works. I would like to understand so that I don't have to deal with similar problems again in the future. If you can explain the general principle I'm violating and the general principle that should be followed instead -- something more insightful than what I've stated here -- then I'll mark your response as the "answer" to this question. I'd like to know how much flexibility I really have.
Here's the code that works, including a new AlertSet component:
/** #jsx React.DOM */
function AlertNotice(level, message, code) {
this.level = level;
this.message = message;
this.code = code || null;
}
function SuccessAlert(message) {
AlertNotice.call(this, 'success', message);
}
SuccessAlert.prototype = Object.create(AlertNotice);
SuccessAlert.prototype.constructor = SuccessAlert;
/* ...more kinds of alerts... */
function ErrorAlert(message, code) {
AlertNotice.call(this, 'danger', message, code);
}
ErrorAlert.prototype = Object.create(AlertNotice);
ErrorAlert.prototype.constructor = ErrorAlert;
var Alert = ReactBootstrap.Alert;
var AlertDismissible = React.createClass({
getInitialState: function() {
return {
isVisible: true
};
},
render: function() {
if(!this.state.isVisible)
return null;
var message = this.props.message;
if(this.props.code !== null)
message = message +'(Code '+ this.props.code +')';
return (
<Alert bsStyle={this.props.level} onDismiss={this.dismissAlert}>
<p>{message}</p>
</Alert>
);
},
dismissAlert: function() {
this.setState({isVisible: false});
}
});
var AlertSet = React.createClass({
render: function() {
var alerts = this.props.alerts.map(function(alert, i) {
return (
<AlertDismissible key={"alert-"+i} level={alert.level}
message={alert.message} code={alert.code} />
);
});
// component must be a single node, so wrap in a div
return (
<div>{alerts}</div>
);
}
});
var SomethingWithAlerts = React.createClass({
render: function() {
var alerts = [
new ErrorAlert("Goof #1", 123),
new ErrorAlert("Goof #2", 321)
];
return (
<AlertSet alerts={alerts} />
);
}
});
// TestComponent returns a single node, so doesn't need a div
var TestComponent = (
<SomethingWithAlerts />
);
React.renderComponent(
TestComponent,
document.getElementById('content')
);

Categories