React components as instances like in OO programming - javascript

I created React component using React.creatClass ()
module.exports = React.createClass({ // input-field-units.jsx is the file name
displayName: 'input-field-units',
render: function () {
return (
<div >
<form className="form-inline" role="form">
<div className="implement-width-select">
<input id={inputid} type="number" className="form-control" onChange={this.onChangeTest}></input>
<div className="form-group">
<select id="implement-width-unit" className="form-control" defaultValue="m" onChange={this.onChangeTest} >
<option value="m" >m</option>
<option value="mm">mm</option>
<option value="ft">ft</option>
</select>
</div>
</div>
</form>
</div>
);
},
componentWillMount: function(){
inputid = this.props.inputid;
console.log("component: " + inputid);
},
onChangeTest: function(){
$(document).ready(function () {
var _unit = document.getElementById("implement-width-unit").value;
var _widthValue = document.getElementById(inputid).value;
//processing of code here..
});
I intend to call this component like objects in C# where values of properties are not shared if this is called several times. here inputid is set from this.props.inputid in componentWillMount()
Im using this component on several places in my application (distributed code is in a single file). In my .jsx file I am doing this
var InputFieldUnitsComponent = require('../Components/input-field-units.jsx');
var ImplementWidthID = "Implement-Width-ID", againWidthID = "again-width-id";
module.exports = React.createClass({
displayName: 'PathPlannerSidebarHeader',
render: function () {
return (
<div>
<h2 className="sidebar-header-subtitle">Implement Width</h2>
<InputFieldUnitsComponent
inputid= {ImplementWidthID} // 1st call
/>
<h2 className="sidebar-header-subtitle">again Width</h2>
<InputFieldUnitsComponent
inputid= {againWidthID}
/>
</div>
);
//....
})
so that everytime I have a new this.props.inputid to set id of
but the problem is this.props.inputid maintains same value change and hold the last value. eg in this case inputid will have "again-width-id" even when I want to use for 1st time I called the component.
In short I like OO behavior where the properties of objects are not shared with each other.
Please ask if this doesn't make sense I will explain

You essentially made inputid a global variable by not declaring it with var (or const or let).
You could say this.inputid in componentDidMount, but that doesn't make a lot of sense: why have the same value as both this.inputid and this.props.inputid
It's simpler to just use this.props.inputid consistently. If you want to simplify render(), define it as a local variable in there.
I suggest to install eslint and enable it in your editor to find this kind of errors.
You also need to update the function onChangeTest. It's not correct to try something like:
onChangeTest: function() {
$(document).ready(function () {
var _widthValue = document.getElementById(this.inputid).value;
});
}
onChangeTest is a method of your react class, but the anonymous function that you're passing to ready() isn't, and it can't refer to your react component via this... unless you bind it!
onChangeTest: function() {
$(document).ready(function () {
var _widthValue = document.getElementById(this.inputid).value;
}.bind(this));
}
or with the ES6 syntax:
onChangeTest: function() {
$(document).ready(() => {
var _widthValue = document.getElementById(this.inputid).value;
});
}
Obligatory reading: How does the "this" keyword work?

Related

Can you use functions from an imported JavaScript library such as Change Case directly in a Vue component's template?

I understand how to import and use Change Case within the <script></script> element of a Vue component, which is just the standard Javascript import covered in the Change Case Github page. However, I would like to use the Change Case functions directly in the template if possible.
Currently, it is my understanding that for dynamic content in the template, in this case generated by v-for running through an array, I must render the return value of a intermediary method from the component's methods section which applies the Change Case function. A method is required for each case type (e.g. camelCase, snakeCase, etc.) I want to render, in this instance one (capitalCase). For example:
// ...
<div
v-for="location in locations"
:key="location.name"
>
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCaseLocationName(location.name)"
v-model="locationsInput"
/>
<label :for="`select-${location.name}`">
{{ capitalCaseLocationName(location.name) }}
</label>
</div>
// ...
methods: {
capitalCaseLocationName(name) {
return capitalCase(name)
}
},
// ...
It would be preferable to somehow import Change Case into the template logic so I could write it like this (no intermediary methods needed):
// ...
<div
v-for="location in locations"
:key="location.name"
>
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCase(location.name)"
v-model="locationsInput"
/>
<label :for="`select-${location.name}`">
{{ capitalCase(location.name) }}
</label>
</div>
// ...
Any chance of that being possible?
As long as you register the imported function as a method you should be able to use it directly in the template.
According to the code, you use Options API, so something like this should do the trick:
import {capitalCase} from "change-case";
...
methods: {
capitalCase,
myOtherMethod () => {...}
}
...
And in the <template>:
<input
type="checkbox"
:id="`select-${location.name}`"
:value="capitalCase(location.name)"
v-model="locationsInput"
/>
The functions need to be defined and passed to the template, that is why even console.log won't work from a template.
You already have an answer with an example, but here's another thing you could do that might make things easier.
You can create a helper like this:
template-helpers.js
export function capitalCase(str) {
return str.split(" ").map(wrd => wrd[0].toUpperCase() + wrd.slice(1)).join(" ")
}
export default {
capitalCase
}
this would make it so that you could use it in a composition/setup like this
import templateHelpers from "../utils/template-helpers.js";
setup(){
return{
...templateHelpers
}
}
in an options API component you could just include it like this
import templateHelpers from "../utils/template-helpers.js";
// ...
methods: {
...templateHelpers,
// other methods
}
// ...
Example
by exporting functions in export default you can destructure them by using methods: { ...templateHelpers
the downside is that it would all the methods every time, but it would make for a more convenient solution. Alternatively, you can pick and chose, since the functions are also exported
import {capitalCase} from "../utils/template-helpers.js";
// ...
methods: {
capitalCase,
// other methods
}
// ...
Vue does have a way to add global definitions, but it's discouraged. This would be done by assigning it to config.globalProperties
https://vuejs.org/api/application.html#app-config-globalproperties
app.config.globalProperties.capitalCase = (str) => {
return str.split(" ").map(wrd => wrd[0].toUpperCase() + wrd.slice(1)).join(" ")

Async loading a template in a Knockout component

I'm pretty experienced with Knockout but this is my first time using components so I'm really hoping I'm missing something obvious! I'll try and simplify my use case a little to explain my issue.
I have a HTML and JS file called Index. Index.html has the data-bind for the component and Index.js has the ko.components.register call.
Index.html
<div data-bind="component: { name: CurrentComponent }"></div>
Index.js
var vm = require("SectionViewModel");
var CurrentComponent = ko.observable("section");
ko.components.register("section", {
viewModel: vm.SectionViewModel,
template: "<h3>Loading...</h3>"
});
ko.applyBindings();
I then have another HTML and JS file - Section.html and SectionViewModel.js. As you can see above, SectionViewModel is what I specify as the view model for the component.
Section.html
<div>
<span data-bind="text: Section().Name"></span>
</div>
SectionViewModel.js
var SectionViewModel = (function() {
function SectionViewModel() {
this.Section = ko.observable();
$.get("http://apiurl").done(function (data) {
this.Section(new SectionModel(data.Model)); // my data used by the view model
ko.components.get("dashboard", function() {
component.template[0] = data.View; // my html from the api
});
});
}
return SectionViewModel;
});
exports.SectionViewModel = SectionViewModel;
As part of the constructor in SectionViewModel, I make a call to my API to get all the data needed to populate my view model. This API call also returns the HTML I need to use in my template (which is basically being read from Section.html).
Obviously this constructor isn't called until I've called applyBindings, so when I get into the success handler for my API call, the template on my component is already set to my default text.
What I need to know is, is it possible for me to update this template? I've tried the following in my success handler as shown above:
ko.components.get("section", function(component) {
component.template[0] = dataFromApi.Html;
});
This does indeed replace my default text with the html returned from my API (as seen in debug tools), but this update isn't reflected in the browser.
So, basically after all that, all I'm really asking is, is there a way to update the content of your components template after binding?
I know an option to solve the above you might think of is to require the template, but I've really simplified the above and in it's full implementation, I'm not able to do this, hence why the HTML is returned by the API.
Any help greatly appreciated! I do have a working solution currently, but I really don't like the way I've had to structure the JS code to get it working so a solution to the above would be the ideal.
Thanks.
You can use a template binding inside your componente.
The normal use of the template bindign is like this:
<div data-bind="template: { name: tmplName, data: tmplData }"></div>
You can make both tmplData and tmplName observables, so you can update the bound data, and change the template. The tmplName is the id of an element whose content will be used as template. If you use this syntax you need an element with the required id, so, in your succes handler you can use something like jQuery to create a new element with the appropriate id, and then update the tmplname, so that the template content gets updated.
*THIS WILL NOT WORK:
Another option is to use the template binding in a different way:
<div data-bind="template: { nodes: tmplNodes, data: tmplData }"></div>
In this case you can supply directly the nodes to the template. I.e. make a tmplNodes observable, which is initialized with your <h3>Loading...</h3> element. And then change it to hold the nodes received from the server.
because nodesdoesn't support observables:
nodes — directly pass an array of DOM nodes to use as a template. This should be a non-observable array and note that the elements will be removed from their current parent if they have one. This option is ignored if you have also passed a nonempty value for name.
So you need to use the first option: create a new element, add it to the document DOM with a known id, and use that id as the template name. DEMO:
// Simulate service that return HTML
var dynTemplNumber = 0;
var getHtml = function() {
var deferred = $.Deferred();
var html =
'<div class="c"> \
<h3>Dynamic template ' + dynTemplNumber++ + '</h3> \
Name: <span data-bind="text: name"/> \
</div>';
setTimeout(deferred.resolve, 2000, html);
return deferred.promise();
};
var Vm = function() {
self = this;
self.tmplIdx = 0;
self.tmplName = ko.observable('tmplA');
self.tmplData = ko.observable({ name: 'Helmut', surname: 'Kaufmann'});
self.tmplNames = ko.observableArray(['tmplA','tmplB']);
self.loading = ko.observable(false);
self.createNewTemplate = function() {
// simulate AJAX call to service
self.loading(true);
getHtml().then(function(html) {
var tmplName = 'tmpl' + tmplIdx++;
var $new = $('<div>');
$new.attr('id',tmplName);
$new.html(html);
$('#tmplContainer').append($new);
self.tmplNames.push(tmplName);
self.loading(false);
self.tmplName(tmplName);
});
};
return self;
};
ko.applyBindings(Vm(), byName);
div.container { border: solid 1px black; margin: 20px 0;}
div {padding: 5px; }
.a { background-color: #FEE;}
.b { background-color: #EFE;}
.c { background-color: #EEF;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="byName" class="container">
Select template by name:
<select data-bind="{options: tmplNames, value: tmplName}"></select>
<input type="button" value="Add template"
data-bind="click: createNewTemplate"/>
<span data-bind="visible: loading">Loading new template...</span>
<div data-bind="template: {name: tmplName, data: tmplData}"></div>
</div>
<div id="tmplContainer" style="display:none">
<div id="tmplA">
<div class="a">
<h3>Template A</h3>
<span data-bind="text: name"></span> <span data-bind="text: surname"></span>
</div>
</div>
<div id="tmplB">
<div class="b">
<h3>Template B</h3>
Name: <span data-bind="text: name"/>
</div>
</div>
</div>
component.template[0] = $(data)[0]
I know this is old, but I found it trying to do the same, and the approcah helped me come up with this in my case, the template seems to be an element, not just raw html

Using serverside HTML-templates with ReactJS

I'm struggling with Reactjs and component rendering.
Basically I've regular html-templates at the server and I'm trying to use them as a JSX-components with the React. Otherwise it works just fine, but I'm not able to fire the events for example: this.handleSubmit.
How to render the loaded template as a React-element?
//Template /index.html
<form onSubmit={this.handleSubmit}>
<input type="text">
<input type="submit"
</form>
//Template loader
var App = React.createClass({
componentDidMount: function() {
this.updateContent();
},
updateContent: function(){
/**
* Loads the template from the server and sets
* a new state
*/
var url = this.props.source.slice(1);
$.get(url, function(result) {
var html = result;
if (this.isMounted()) {
this.setState({
content: html
});
}
}.bind(this));
},
handleSubmit: function(){
console.log('Submit fired');
}
render: function() {
var converter = new HTMLtoJSX({createClass: false});
var jsx = '/** #jsx React.DOM */ ' + converter.convert(this.state.content);
return (
<div>
{JSXTransformer.exec(jsx)}
</div>
);
});
React.render(
<App source="#/index.html" />,
mountPoint
);
JSX isn't templates for a markup language, it's a syntax extension to the JavaScript programming language. The distinction is important here.
You need to convert JSX to JS (usually done when building your project). If we modify your code to be valid JSX it looks like this:
<form onSubmit={this.handleSubmit}>
<input type="text" />
<input type="submit" />
</form>
And when run through the jsx tools the output is the following JavaScript expression.
React.createElement("form", {onSubmit: this.handleSubmit},
React.createElement("input", {type: "text"}),
React.createElement("input", {type: "submit"})
)
You need to execute this code in render, with the correct this context. You could do this by wrapping the above in a function before serving it to the client:
function renderThingy(){
React.createElement("form", {onSubmit: this.handleSubmit},
React.createElement("input", {type: "text"}),
React.createElement("input", {type: "submit"})
)
}
And calling that in render:
render: function() {
return (
<div>
{renderThingy.call(this)}
</div>
);
}
This is of course confusing, and it's not apparent if handleSubmit is used by anything. Then of course, there's the issue of loading code asynchronously... I'd rather not delve into that here.
This also severely limits what you can do in your 'template', and various other problems will pop up.
tl;dr don't do this
If you want to use JSX on the server: cool, you just need a JS runtime, and a component. React.renderToString or React.renderToStaticMarkup will take care of actually giving you valid html.
The template should be precompiled using React.renderToString(), then the html returned from the function has all the extra DOM attributes such as data-react-id needed to make a reconciliation with the client.
This is because the onSubmit={this.handleSubmit} in your template doesn't have the react data associated, react doesnt care/know about it.
React can render clean html too without the bloat using React.renderToStaticMarkup() function;
Take a look here http://facebook.github.io/react/docs/top-level-api.html#react.rendertostring

How to update data in Meteor using Reactivevar

I have a page with a form. In this form user can add multiple rows with key and values. There is a restriction that the customFields is created on the fly, not from any subscribed collection.
...html
<template name="main">
{{#each customFields}}
<div>
<input type="text" value="{{key}}"/>
<input type="text" style="width: 300px;" value="{{value}}"/>
</div>
{{/each}}
</template
.... router.js
Router.route 'products.add',
path: '/products/add/:_id'
data:
customFields:[]
....products.js
#using customFieldSet as Reactive Var from meteor package
Template.product.created = ->
#customFieldSet = new ReactiveVar([])
Template.product.rendered = ->
self = this
Tracker.autorun ->
arr = self.customFieldSet.get()
self.data.customFields = arr
Template.product.events(
'click .productForm__addField': (e)->
t = Template.instance()
m = t.customFieldSet.get()
console.log t
m.push(
key: ''
value: ''
)
t.customFieldSet.set m
....
The last event will be trigger when I click the button. And it add another row with key and value empty to the page.
Please advise me why I actually see the reactive variable customFieldSet updated, but there is nothing changed dynamically in html.
P/s: I guess customFields is not updated via Iron router.
Basically, you're doing the thing right. However, you shouldn't be assigning the new reactive data to your template's data context, but rather access it directly from your helpers:
Template.product.helpers({
customFileds: function () {
return Template.instance().customFiledsSet.get();
},
});
Now you can use {{customFields}} in your template code and it should work reactively. Just remember that {{this.customFileds}} or {{./customFileds}} will not work in this case.

Dynamically adding items into view and posting back to controller (ASP.NET MVC 4)

I have a ASP.NET MVC 4 app with model, that contains and colection (IEnumerable<T> or IList<T>), i.e.:
class MyModel
{
public int Foo { get; set; }
public IList<Item> Bar { get; set; }
}
class Item
{
public string Baz { get; set; }
}
And I render the data in view with classic #for..., #Html.EditorFor... ad so on. Now there's a need to add on client side to add dynamically new items and then post it back to server.
I'm looking for an easy solution to handle the adding (in JavaScript), aka not manually creating all the inputs etc. Probably to get it somehow from editor template view. And to add it the way that when the form is submitted back to server the model binder will be able to properly create the IList<T> collection, aka some smart handling of inputs' names. I read a bunch of articles, but nothing that was easy and worked reliably (without magic strings like collection variable names, AJAX callbacks to server, ...).
So far this looks promising, but I'd like to rather rely on rendering (items known in advance) on server.
I'm not sure what do you mean 'collection variable names' and probably my solution is kind of magic you noticed.
My solution is based on copying existing editor for element and altering input names via Javascript.
First of all, we need to mark up our editor. This is a code of form outputs editor for collection
#for (var i = 0; i < Model.Count; i++)
{
<div class="contact-card">
#Html.LabelFor(c => Model[i].FirstName, "First Name")
#Html.TextBoxFor(c => Model[i].FirstName)
<br />
#Html.LabelFor(c => Model[i].LastName, "Last Name")
#Html.TextBoxFor(c => Model[i].LastName)
<br />
#Html.LabelFor(c => Model[i].Email, "Email")
#Html.TextBoxFor(c => Model[i].Email)
<br />
#Html.LabelFor(c => Model[i].Phone, "Phone")
#Html.TextBoxFor(c => Model[i].Phone)
<hr />
</div>
}
Our editor is placed into div with class contact-card. On rendering, ASP.NET MVC gives names like [0].FirstName, [0].LastName ... [22].FirstName, [22].LastName to inputs used as property editors. On submitting Model Binder converts this to collection of entities based both on indexes and property names.
Next we create javascript function that copies last editor and increases index in brackets by 1. On submitting it adds additional element to collection:
var lastContent = $("#contact-form .contact-card").last().clone();
$("#contact-form .contact-card").last().after(lastContent);
$("#contact-form .contact-card")
.last()
.find("input")
.each(function () {
var currentName = $(this).attr("name");
var regex = /\[([0-9])\]/;
var newName = currentName.replace(regex, '[' + (parseInt(currentName.match(regex)[1]) + 1) + ']');
$(this).val('');
$(this).attr('name', newName);
});
VOILA!! On submitting we will get one more element!
At the end I did similar stuff what STO was suggesting, but with the custom (non-linear) indices for collections suggested by Phil Haack.
This uses manual naming of elements (so I'm not binding directly to the model) and I can use custom instances (for empty element templates). I've also created some helper methods to generate me the code for the instance, so it's easier to generate code for actual instances from the model or empty ones.
I did this with help of Backbone (for file uploader) where i insert template whenever user click #addButton
View:
#using Telerik.Web.Mvc.UI
#{
ViewBag.Title = "FileUpload";
Layout = "~/Areas/Administration/Views/Shared/_AdminLayout.cshtml";
}
<div id="fileViewContainer" class="span12">
<h2>File upload</h2>
#foreach(var fol in (List<string>)ViewBag.Folders){
<span style="cursor: pointer;" class="uploadPath">#fol</span><br/>
}
#using (Html.BeginForm("FileUpload", "CentralAdmin", new { id = "FileUpload" }, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<label for="file1">Path:</label>
<input type="text" style="width:400px;" name="destinacionPath" id="destinacionPath"/><br />
<div id="fileUploadContainer">
<input type="button" class="addButton" id="addUpload" value="Add file"/>
<input type="button" class="removeButton" id="removeUpload" value="Remove file"/>
</div>
<input type="submit" value="Upload" />
}
</div>
<script type="text/template" id="uploadTMP">
<p class="uploadp"><label for="file1">Filename:</label>
<input type="file" name="files" id="files"/></p>
</script>
#{
Html.Telerik().ScriptRegistrar().Scripts(c => c.Add("FileUploadInit.js"));
}
FileUploadInit.js
$(document).ready(function () {
var appInit = new AppInit;
Backbone.history.start();
});
window.FileUploadView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, 'render', 'addUpload', 'removeUpload', 'selectPath');
this.render();
},
render: function () {
var tmp = _.template($("#uploadTMP").html(), {});
$('#fileUploadContainer').prepend(tmp);
return this;
},
events: {
'click .addButton': 'addUpload',
'click .removeButton': 'removeUpload',
'click .uploadPath': 'selectPath'
},
addUpload: function (event) {
this.render();
},
removeUpload: function (event) {
$($('.uploadp')[0]).remove();
},
selectPath: function (event) {
$('#destinacionPath').val($(event.target).html());
}
});
var AppInit = Backbone.Router.extend({
routes: {
"": "defaultRoute"
},
defaultRoute: function (actions) {
var fileView = new FileUploadView({ el: $("#fileViewContainer") });
}
});
In Controller you keep your code
I Hope this will help.

Categories