On a rails 6 installation, I have the following:
Controller:
# app/controllers/foo_controller.rb
def bar
#items = [["firstname", "{{ FIRSTNAME }}"], ["lastname", "{{ LASTNAME }}"], ["company", "{{ COMPANY }}"]]
end
View:
# app/views/foo/bar.html.erb
<p>Quia <span data-field="firstname">{{ FIRSTNAME }}</span> quibusd <span data-field="firstname">{{ FIRSTNAME }}</span> am sint culpa velit necessi <span data-field="lastname">{{ LASTNAME }}</span> tatibus s impedit recusandae modi dolorem <span data-field="company">{{ COMPANY }}</span> aut illo ducimus unde quo u <span data-field="firstname">{{ FIRSTNAME }}</span> tempore voluptas.</p>
<% #items.each do |variable, placeholder| %>
<div data-controller="hello">
<input
type="text"
data-hello-target="name"
data-action="hello#greet"
data-field="<%= variable %>"
value="<%= placeholder %>">
</div>
<% end %>
and the relevant stimulus code (vanilla JS):
//app/javascript/controllers/hello_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "name" ]
greet() {
var elements = document.body.querySelectorAll('[data-field="' + this.nameTarget.dataset.field + '"]');
for (var i = 0; i < elements.length; i++) {
elements[i].innerText = this.nameTarget.value;
};
}
}
Now, as you might have guessed, the idea is to generate one <input> field per item from the #items hash, pre-filled with the relevant value and "linked" with a <span>, which it updates on value change. So far, everything works.
Here's my issue though. This part is plain old dirty vanilla js, which doesn't feel too 'stimulusy':
var elements = document.body.querySelectorAll('[data-field="' + this.nameTarget.dataset.field + '"]');
for (var i = 0; i < elements.length; i++) {
elements[i].innerText = this.nameTarget.value;
};
Surely there's some way to improve this. Any suggestion as to how to refactor this code in a more elegant way would be most welcome.
An approach would be to have two controllers, one for the 'thing that will change the content' (let's call this content) and another for the 'thing that will show any updated content somewhere else' (let's call this output).
Once you set up two controllers, it becomes a bit easier to reason about them as being discrete. One does something when a value updates from user interaction and the other should so something when it knows about an updated value.
Stimulus recommends cross controller coordination with events. JavaScript event passing is a powerful, browser native, way to communicate across elements in the DOM.
First, let's start with the simplest case in HTML only
In general, it is good to think about the HTML first, irrespective of how the content is generated on the server side as it will help you solve one problem at a time.
As an aside, I do not write Ruby and this question would be easier to parse if it only had the smallest viable HTML to reproduce the question.
Below we have two div elements, one sits above and is meant to show the name value inside the h1 tag and the email in the p tag.
The second div contains a two input tags and these are where the user will update the value.
I have hard-coded the 'initial' data as this would come from the server in the first HTML render.
<body>
<div
class="container"
data-controller="output"
data-action="content:updated#window->output#updateLabel"
>
<h1 class="title">
Hello
<span data-output-target="item" data-field="name">Joe</span>
</h1>
<p>
Email:
<span data-output-target="item" data-field="email">joe#joe.co</span>
</p>
</div>
<div data-controller="content">
<input
type="text"
data-action="content#update"
data-content-field-param="name"
value="Joe"
/>
<input
type="text"
data-action="content#update"
data-content-field-param="email"
value="joe#joe.co"
/>
</div>
</body>
Second - walk through the event flow
Once an input is updated, it will fire the conten#update event on change.
The data-content-field-param is an Action Parameter that will be available inside the event.params given to the class method update on the content controller.
This way, the one class method has knowledge of the element that has changed (via the event) and the field 'name' to give this when passing the information on.
The output controller has a separate action to 'listen' for an event called content:updated and it will listen for this event globally (at the window) and then call its own method updateLabel with the received event.
The output controller has targets with the name item and each one has the mapping of what 'field' it should referent in a simple data-field attribute.
Third - create the controllers
Below, the ContentController has a single update method that will receive any fired input element's change event.
The value can be gathered from the event's currentTarget and the field can be gathered via the event.params.field.
Then a new event is fired with the this.dispatch method, we give it a name of updated and Stimulus will automatically append the class name content giving the event name content:updated. As per docs - https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events
The OutputController has a target of name item and then a method updateLabel
updateLabel will receive the event and 'pull out' the detail given to it from the ContentController's dispatch.
Finally, updateLabel will go through each of the itemTargets and see if any have the matching field name on that element's dataset and then update the innerText when a match is found. This also means you could have multiple 'name' placeholders throughout this controller's scoped HTML.
class ContentController extends Controller {
update(event) {
const field = event.params.field;
const value = event.currentTarget.value;
this.dispatch('updated', { detail: { field, value } });
}
}
class OutputController extends Controller {
static targets = ['item'];
updateLabel(event) {
const { field, value } = event.detail;
this.itemTargets.forEach((element) => {
if (element.dataset.field === field) {
element.innerText = value;
}
});
}
}
An alternate approach is to follow the Publish-Subscribe pattern and simply have one controller that can both publish events and subscribe to them.
This leverages the recommended approach of Cross-controller coordination with events.
This approach adds a single controller that will be 'close' to the elements that need to publish/subscribe and is overall simpler to the first answer.
PubSubController - JS code example
In the controller below we have two methods, a publish which will dispatch an event, and a subscribe which will receive an event and update the contoller's element.
The value used by this controller is a key which will serve as the reference for what values matter to what subscription.
class PubSubController extends Controller {
static values = { key: String };
publish(event) {
const key = this.keyValue;
const value = event.target.value;
this.dispatch('send', { detail: { key, value } });
}
subscribe(event) {
const { key, value } = event.detail;
if (this.keyValue !== key) return;
this.element.innerText = value;
}
}
PubSubController - HTML usage example
The controller will be added to each input (to publish) and each DOM element you want to be updated (to subscribe).
Looking at the inputs you can see that they have the controller pub-sub and also an action (defaults to triggering when the input changes) to fire the publish method.
Each input also contains a reference to its key (e.g email or name).
Finally, the two spans that 'subscribe' to the content are triggered on the event pub-sub:send and pass the event to the subscribe method. These also have a key.
<body>
<div class="container">
<h1 class="title">
Hello
<span
data-controller="pub-sub"
data-action="pub-sub:send#window->pub-sub#subscribe"
data-pub-sub-key-value="name"
>Joe</span
>
</h1>
<p>
Email:
<span
data-controller="pub-sub"
data-action="pub-sub:send#window->pub-sub#subscribe"
data-pub-sub-key-value="email"
>joe#joe.co</span
>
</p>
</div>
<div>
<input
type="text"
data-controller="pub-sub"
data-action="pub-sub#publish"
data-pub-sub-key-value="name"
value="Joe"
/>
<input
type="text"
data-controller="pub-sub"
data-action="pub-sub#publish"
data-pub-sub-key-value="email"
value="joe#joe.co"
/>
</div>
</body>
Related
A piece of react-native code:
enderScene(route, navigator) {
let Component = route.component;
return (
<Component {...route.params} navigator={navigator}></Component>
);
}
this code returns a Component Object ,
But I don't understand this code ---> {...route.params}
Question:
What is meant by '...' ?
Can you tell me what is meant by " {...route.params}" ?
The '...' is called Spread syntax.
The spread syntax allows an expression to be expanded in places where multiple arguments (for function calls) or multiple elements (for array literals) or multiple variables (for destructuring assignment) are expected.
Example :
var car = ["door", "motor", "wheels"];
var truck = [...car, "something", "biggerthancar"];
// truck = ["door", "motor", "wheels", "something", "biggerthancar"]
If you want to know more about spread operator :
https://rainsoft.io/how-three-dots-changed-javascript/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator
To expand on the above, in the context of the original post the spread operator is essentially passing through all of the parameters in route.params
For example if route.params was the object
{key: 'my-route', title: 'My Route Title'}
Then
<Component {...route.params} navigator={navigator} />
Could be re-written as
<Component key={route.params.key} title={route.params.title} navigator={navigator} />
The other "side" of this is the destructuring assignment (example using stateless react components)
const Component = (props) => {
// this is simply referencing the property by the object key
let myKey = props.key
// this is using destructuring and results in the variables key, title and navigator which are from props.key, props.title and props.navigator
let { key, title, navigator } = props
return <Text>{title}</Text>
}
You can also do this in the function declaration like so which achieves the same thing
const Component = ({key, title, navigator}) => {
// now you have variables key, title and navigator
return <Text>{title}</Text>
}
See Destructuring
Ok, I was confused about that for a long period of time.
So, I'll try my best to explain it to you:
Suppose, you've a react class like bellow:
import React, {Component} from 'react';
import { Link } from 'react-router-dom';
class SingleService extends Component{
render(){
return(
<div class="col-md-4">
<span class="fa-stack fa-4x">
<i class="fas fa-circle fa-stack-2x text-primary"></i>
<i class={`fas ${this.props.icon} fa-stack-1x fa-inverse`}></i>
</span>
<h4 class="service-heading">{this.props.title}</h4>
<p class="text-muted">{this.props.description}</p>
</div>
);
}
}
export default SingleService;
Here, you can see that there are so many {this.props.variable}.
Those are used to create dynamic values when we import this above class into another class, like bellow:
import React, {Component} from 'react';
import { Link } from 'react-router-dom';
import SingleService from './SingleService';
// declaring a constant array to hold all of our services props.
// The following array is made up of the objects.
const services = [
{
title:'E-commerce',
description:'Description text on E-commerce',
icon: 'fa-shopping-cart'
}
];
class Services extends Component{
render(){
return(
<div>
<section class="page-section" id="services">
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading text-uppercase">Services</h2>
<h3 class="section-subheading text-muted">Lorem ipsum dolor sit amet consectetur.</h3>
</div>
</div>
<div class="row text-center">
{/* it's looping through an object, that's why we've used key value pair. */}
{ /*
to write js inside JSX, we use curly braces
here we're using array.map() function.
*/}
{services.map((service, index) => {
// returning our component with props.
// return (<SingleService title={service.title} description={service.description} icon={service.icon} />);
// or, we can write the following
return (<SingleService {...service}/>);
})}
</div>
</div>
</section>
</div>
);
}
}
export default Services;
Now, here, I've used the famous
return (<SingleService {...SingleService}/>);
But one very important thing, I could avoid using it simply by writing the following line:
return (<SingleService title={service.title} description={service.description} icon={service.icon} />);
So, you can see in the send return statement, I've specified all of the props variables individually and assigned values to those, whereas in the first return statement, I've passed in all pf the props together from the SingleService object at once, that will pass all od the key-value pairs.
To add to the above given answers, the ... or the spread operator is not something special to react native. It is a feature in es6. ES6 stands for ecma script and is the standard followed for javascript. This means that you could create a .js file outside of react/react-native and run it in a node env and the spread operator would still work.
I have a web page that dynamically creates components by rendering them through a RenderPartial Html Helper like this
Html.RenderPartial("_UploadStaticField", config);
Within the config object is a field called 'isUnderReview'. When this is set to true the components should be disabled by using the code below
//Single selection
<div class="editor-section">
<div class="label">
#Html.DisplayEditLabel(Model.Label, Model.Required.Value)
</div>
<div class="field large-text-field">
#Html.DropDownListFor(m => m.SelectedListOptionID, new SelectList(Model.ListOptions, "ID", "Name", Model.SelectedListOptionID).OrderBy(l => l.Value), new Dictionary<string, object>{
{"id", "staticField_" + Model.ID}})
</div>
</div>
<script>
$(document).ready(function () {
if ("#Model.IsUnderReview" == "True") {
document.getElementById("staticField_" + "#Model.ID").disabled = true;
}
});
</script>
and..
//Multiple selection
<div class="editor-section">
<div class="label">
#Html.DisplayEditLabel(Model.Label, Model.Required.Value)
</div>
<div class="field large-text-field">
#Html.ListBoxFor(x => x.SelectedRoles, filetypes, new { #class = "multiselectFileTypes" , id = "staticFieldM_" + Model.ID})
</div>
</div>
#Scripts.Render(BundleConfig.Scripts_MultiSelect)
<script>
$(document).ready(function () {
if ("#Model.IsUnderReview" == "True") {
document.getElementById("staticFieldM_" + "#Model.ID").disabled = true;
}
});
</script>
The code works to the point that the methods run but the components are still able to be used. Is there a way of cancelling any users selections which will serve as disabling as the values wont change?
Scripts should never be in partials (you potentially generating multiple inline scripts that may cause other problems, especially with the bundle). However a script is not even necessary for this, you can use a simple if statement or conditional attributes to generate what you want.
When you say "but the components are still able to be used", I'm guessing that your using a plugin to generate controls as suggested by Scripts.Render(BundleConfig.Scripts_MultiSelect) which will be hiding the original <select> element and generating its own html which is why it still interactive.
But the next problem is that disabled controls do not post back a value, so the vales of SelectedListOptionID and SelectedRoles will be their default, possibly resulting in your app failing depending on the code in your POST method.
Move the #Scripts.Render() into you view or layout, delete the scripts to disable the element and then change the partial to
#if(Model.IsUnderReview)
{
#Html.HiddenFor(m => m.SelectedListOptionID) // if you want the value to be posted
// add an element to display the Name associated with the SelectedListOptionID
// if necessary, for example your view model might include a property
// string SelectedListOptionName
}
else
{
#Html.DropDownListFor(m => m.SelectedListOptionID, new SelectList(Model.ListOptions, "ID", "Name").OrderBy(l => l.Value))
}
Side notes:
There is no reason to add you own id attribute (the
DropDownListFor() method generates <select
id="SelectedListOptionID" ... >
Remove the last parameter of the SelectList constructor
(Model.SelectedListOptionID) - its ignored by the
DropDownListFor() method. I also recommend your model contains an IEnumerable<SelectListItem> OptionsList property and you populate that in the controller so that it can simply be #Html.DropDownListFor(m => m.SelectedListOptionID, Model.OptionsList)
I have a requirement for a grails application to display a list of questions on the screen with 6 grade options listed below each of the questions. The information for these questions and grades is coming from a lookup table in the database. I have the questions and grades displaying on the screen but I'm not sure how to go about getting the lookup information to save in the database. I would also like to know if there is a way to have a certain grade selected by default for each of the questions. I tried the checked="S" but this only selects the S grade for the very bottom questions.
My code for the view is
<label for="questions"></label>
<ul class="one-to-many">
<!-- Evaluation Questions -->
<g:each in="${cdeEvaluationInstance?.questions}" var="evalQuestion" status="i">
<g:hiddenField name="cdeEvaluation.questions[${i}].id" value="${evalQuestion.id}"/>
<legend>
${evalQuestion.areaOfEval.title}
</legend>
<p>
<strong>Focus areas: ${evalQuestion.areaOfEval.focusArea}</strong>
</p>
<p>
<em> ${evalQuestion.areaOfEval.description}
</em>
</p>
<p>
<g:each in="${evalQuestion.areaOfEval.grades.sort{it.grade}}"
var="grade" ><div class="radio">
<span class="clear long">
<input type="radio"
name="radioGroup" value="${evalQuestion.grade}" checked="S" />
<label class="long"><strong> ${grade.grade}
</strong> ${grade.description}</label>
</div>
</g:each>
My code for the controller is
def evalQuestions = EvaluationService.fetchActiveEvaluationQuestions();
//def evaluation = new CdeEvaluation(questions: evalQuestions)
def evaluation = new CdeEvaluation(params)
evaluation.setQuestions(evalQuestions)
My domain for the table that the questions and answers are
package gov.mt.mdt.cde.domain.evaluation
import java.util.Date;
class CdeEvalQuestion extends Base{
CdeAreaOfEvaluation areaOfEval
CdeAreaOfEvalCriteria grade
String comments
static belongsTo = [cdeEvaluation: CdeEvaluation]
static mapping = {
id column: 'cevqu_id_seq'
id generator: 'sequence', params: [sequence: 'cevqu_id_seq']
areaOfEval column: 'caoe_id_seq'
grade column: 'caoec_id_seq'
}
static constraints = {
comments(blank:true, nullable:true, maxSize:2000)
createdBy(blank: false, nullable:false, maxSize:13)
dateCreated(blank: false, nullable:false)
lastUpdatedBy(blank: false, nullable:true, maxSize:13)
lastUpdated(blank: false, nullable:true)
}
}
I am just starting to learn grails/groovy so any help or examples you could point me to would be great. Thanks!
So selecting a particular question by default you would do something like:
<g:radioGroup name="myGroup" labels="evalQuestion.areaOfEval.grades" values="evalQuestion.areaOfEval.grades*.grade" value="evalQuestion.grade">
${it.radio} <label class="long"><strong>${it.label.grade}</strong> ${it.label.description}</label>
</g:radioGroup>
That doesn't require you write the inner each. Basically you pass an array of labels and a parallel array of values. The value attribute is the default value from the values attribute. The inner body of the radioGroup will be repeated for each label and value pair. The *. (aka spread operator) basically is the same thing as using the collect() method.
I removed the spread operator for label and I passed the full object in for the label. Then inside the body of the tag when I do it.label I have the full object and can use different fields it.label.description and it.label.grade.
As for setting the default to grade S. You'll need to write the code to find grade S from evalQuestion.areaOfEval.grades. Something like:
evalQuestion.areaOfEval.grades.find { it.grade == 'S' }
And pass that to value attribute of the tag. You could do this:
<g:set var="defaultGrade" value="evalQuestion.areaOfEval.grades.find { it.grade == 'S' }"/>
<g:radioGroup name="myGroup"
labels="evalQuestion.areaOfEval.grades"
values="evalQuestion.areaOfEval.grades*.grade"
value="defaultGrade">
I'm trying to bind a textfield to an value in object like this:
{{#each type in controller.currentTypes}}
<label>
{{type.name}}
{{view Ember.TextField valueBinding="controller.contextRemarks[type.id]"}}
</label>
{{/each}}
controller.currentTypes and controller.contextRemarks are only related in a loose one-to-many manner.
currentTypes = [{id: 1, name: 'Type name 1'}]
I don't know if I'm implementing an anti-pattern, but I don't think I can do without the dynamic index and Ember doesn't seem to allow me to use an index in a path.
I tried to solve this using an extra view or a helper, but keep getting stuck.
Is there an ember-ry solution for this?
So I worked around this problem by adding a computed property on the controller and storing the generated array in a private property:
App.MyController = Ember.Controller.extend({
remarks: function() {
var remarks = this.get('currentTypes') || [];
this._remarks = remarks.map(function(remark) {
return {
id: remark.id,
name: remark.name,
value: ''
};
});
return this._remarks;
}.property('currentTypes')
});
}
The view now looks like this:
{{#each remarks}}
<label>
{{name}}
{{view Ember.TextField valueBinding="value"}}
</label>
{{/each}}
Now I can access _remarks to find the user's input, this is the most ember like solution I can come up with and might be the "right solution. ;)
I am using the webflow plugin for Grails for the first time and am having some difficulties.
To summarize, once within the Webflow, no information appears to be returning to the controller from the form. All examples that I have looked at indicate that the params are returned to the controller action normally and then you can put objects into the flow scope as necessary. Unfortunately, the illustrated printlns are both outputting null, and any programatic output of the params shows that the expected 'testField1' and 'testField2' are not in the params object. Excuse the non-uniform text boxes and ways of submitting - they were the result of experimentation.
A simplified version of the controller action flow:
def generateProductVariantsFlow = {
start() {
action {
[productInstance:Product.get(params.id)] //the entry params contains the expected id
}
on ("success").to("selectAttributeValues")
}
selectAttributeValues() {
on("next"){TestCommand tc -> //params does not have testField1 or testField2
println "TEST COMMAND"
println "${tc.testField1}"
println "${tc.testField2}"
}.to("selectProductVariants")
on("cancel").to("finishBeforeStart")
}
selectProductVariants {
on("cancel").to("finish")
on("previous").to("selectAttributeValues")
on("next").to("confirmNewVariants")
}
//other states here
finish {
redirect(action:"list")
}
finishBeforeStart { //somewhat misleading state name, but shouldn't be relevant
redirect(controller:"product",action:"show")
}
}
The GSP and Command are equally simple -
selectAttributeValues GSP:
<%# page import="com.castaway.rigging.Product" %>
<g:form action="generateProductVariants">
<input type="integer" id="testField1" name="testField1" value="test1" />
<g:textField name="testField2" value="test2"/>
<div class="buttons">
<span class="button"><g:actionSubmit class="cancel" name="cancel" value="Cancel"/></span>
<g:link action="generateProductVariants" event="next" >Next</g:link>
</div>
</g:form>
</div>
</body>
Command:
class TestCommand implements Serializable {
def testField1
def testField2
}
Why do you use a link instead of a submit button to trigger the next event?
Clicking that link will do a GET request which will not include the form fields.
You need to use a submit button to trigger the next event.
cheers
Lee