How to select an option from dropdown select - puppeteer

I can click the selector but my question is how to select one of the options from the dropdown list?
await page.click('#telCountryInput > option:nth-child(4)')
Click the option using CSS selector does not work.
For example, select a country code from a list like below:

Puppeteer v0.13.0 has page.select() method, which does exactly that. You just have to give it the value to select. So, assuming you have an <option value="my-value"> in your <select>:
await page.select('#telCountryInput', 'my-value')

For dropdown component, I think we should consider 2 situations:
native HTML select element
component written by JS, composed of a button and a list of options, take bootstrap dropdown as example
For the second situation, I think click can solve the problem.
For the first situation, I just found 2 ways to do this:
page.select
elementHandle.type (notice updated on 27/04/2018)
page.select is a new feature added in v0.12.0.
For example you have a select element:
<label>Choose One:
<select name="choose1">
<option value="val1">Value 1</option>
<option value="val2">Value 2</option>
<option value="val3">Value 3</option>
</select>
</label>
You have two ways to select second option 'Value 2'.
// use page.select
await page.select('select[name="choose1"]', 'val2');
// use elementHandle.type
const selectElem = await page.$('select[name="choose1"]');
await selectElem.type('Value 2');
Normally elementHandle.type is used to type in text in input textbox, but since it
Focuses the element, and then sends a keydown, keypress/input, and keyup event for each character in the text.
select HTML element has input event, so that this method works.
And I personally think elementHandle.type is better since it's not required to know the option value attribute, only the label/name what man can see.
27/04/2018 Updated
I previously used elementHandle.type only on Mac OSX. Recently, my colleague reported a bug related to this. He is using Linux/Win. Also, we are both using puppeteer v1.3.0.
After trial and error, we found that this elementHandle.type can assign the value to the <select> element, but this won't trigger the change event of the element.
So I no longer recommend using elementHandle.type on <select>.
Finally, we followed this comment to dispatch change event manually. It's like:
// use manually trigger change event
await page.evaluate((optionElem, selectElem) => {
optionElem.selected = true;
const event = new Event('change', {bubbles: true});
selectElem.dispatchEvent(event);
}, optionElem, selectElem);

For native selectboxes, my solution was to execute some JS on the page itself:
await page.evaluate(() => {
document.querySelector('select option:nth-child(2)').selected = true;
})

I landed here from a message where someone was asking how to select the first option from a dropdown. This is how I just worked out how to do it:
await page.click('.select-input');
await page.waitFor(300);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
The above code first selects the relevant input. I then set a wait because mine wasn't loading quick enough. Then I used keyboard presses to navigate down to the first option.

Turn out this is easier than what I thought because the dropdown list is NOT a native HTML selction&option combination, therefore, I can actually use the code below to select the target I want.
await page.click('#telCountryInput')
await page.click('#select2-telCountryInput-results > li:nth-child(4)')

Page.select doesn't always work for me and page.type is unreliable as well. Today I came up with:
await page.evaluate((css, text) => {
let sel = document.querySelector(css)
for(let option of [...document.querySelectorAll(css + ' option')]){
if(text === option.text){
sel.value = option.value
}
}
}, '#telCountryInput', 'my-value')

#huagang
Your idea is amazing, I extended the value attribute
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>example</title>
</head>
<body>
<form id="add" method="post" action="/detail">
<label for="title"></label>
<input id="title" name="title">
<label for="tag">Tag</label>
<select id="tag">
<option value="1">java</option>
<option value="2">python</option>
<option value="3">kotlin</option>
</select>
</form>
<button id="submit" onclick="submitHandle()">Submit</button>
<script>
const submitHandle = () => {
document.getElementById('add').submit()
}
</script>
</body>
</html>
expect_value = '3'
select_tag = '#tag'
# extract all options value
option_texts = []
for option_ele in await page.querySelectorAll(f'{select_tag} > option'):
text = await page.evaluate('(element) => ({"value":element.value,"text":element.textContent})', option_ele)
option_texts.append(text)
value = ''
for v in option_texts:
if v.get('text') == expect_value:
value = v.get('value')
break
await page.select(select_tag, value)

In pyppeteer, when select by text, i can do this :
Example page with fastapi server
"""
filename: example.py
Note:
When run this example, recommend create a virtualenv by tools, like pipenv. And install dependencies.
Install dependencies:
```shell
pipenv install fastapi uvicorn python-multipart
```
Run server:
```shell
pipenv run python example.py
# pipenv run uvicorn --reload example:app
```
"""
import logging
import uvicorn
from fastapi import FastAPI, Form
from pydantic import BaseModel
from starlette.responses import HTMLResponse
HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>example</title>
</head>
<body>
<form id="add" method="post" action="/add">
<label for="title"></label>
<input id="title" name="title">
<label for="tag">Tag</label>
<select id="tag" name="tag">
<option>java</option>
<option>python</option>
<option>kotlin</option>
</select>
</form>
<button id="submit" onclick="submitHandle()">Submit</button>
<script>
const submitHandle = () => {
document.getElementById('add').submit()
}
</script>
</body>
</html>
"""
console_handler = logging.StreamHandler()
console_handler.setLevel(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
logger.addHandler(console_handler)
app = FastAPI()
class PostModel(BaseModel):
title: str
tag: str
#app.get('/posts')
def posts():
return HTMLResponse(content=HTML)
#app.post('/add')
def detail(title: str = Form(...), tag: str = Form(...)) -> PostModel:
post = PostModel(title=title, tag=tag)
logger.info(f'Add a blog. Detail: "{post.json()}"')
return post
if __name__ == '__main__':
uvicorn.run(app) # noqa
Example python spider code
import asyncio
import logging
from pyppeteer import launch
console_handler = logging.StreamHandler()
console_handler.setLevel(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
logger.addHandler(console_handler)
async def post_spider():
"""Open page and add value in form, then submit."""
browser = await launch(headless=False)
try:
page = await browser.newPage()
await page.goto('http://127.0.0.1:8000/posts')
expect_value = 'python'
title_element = await page.querySelector('#title')
await title_element.type('I love python, and python love me.')
# # If it does not work.
# await page.select('#tag', expect_value)
tag_element = await page.querySelector('#tag')
# #Extract all options value
# options_text = await page.querySelectorAllEval(
# '#tag > option',
# 'options => options.map(option => option.value)'
# )
options_text = await tag_element.querySelectorAllEval(
'option',
'options => options.map(option => option.value)'
)
# # Check expect value in options
if expect_value in options_text:
# # Use JavaScript set select element value that in options.
await page.querySelectorEval('#tag', f'element => element.value = "{expect_value}"')
tag_selected_value = await page.querySelectorEval('#tag', 'element => element.value')
logger.info(f'Selected tag element value is "{tag_selected_value}"')
submit_ele = await page.querySelector('#submit')
await submit_ele.click()
finally:
await browser.close()
if __name__ == '__main__':
asyncio.run(post_spider())
Note:
You can use evaluate a JavaScript to set one of options text to their select, if the text not in options, the select's value is not change.
This is python example, its use is similar to puppeteer and I would like to record it here to help more people.
My env:
Python: 3.10
pyppeteer: 0.2.6

I combined 2 answers and wrapped them in a function:
async function selectByText(page, selector, value) {
return await page.evaluate(
(css, text) => {
let sel = document.querySelector(css)
for (let option of [...document.querySelectorAll(css + ' option')]) {
if (text === option.text) {
sel.value = option.value
}
}
const event = new Event('change', { bubbles: true })
sel.dispatchEvent(event)
},
selector,
value,
)
}

Related

Angular changeable form according to inputs

I'm using a reactive form. I need to add/remove an input that appears in it according to some other input. Here's a simplified scenario of the issue:
Asking the user to select an option from a list. If their desired option is not there, there is an open input where they can write. If they do choose an option from the select, the input must disappear. If they do not select an option, the input must be there and it must be required.
Here's the code I made which 1) doesn't work 2) feels like it's fairly ugly and could be made in some other way.
Template:
<form [formGroup]="whateverForm" (ngSubmit)="onSubmit()">
Choose an option:
<select
formControlName="option"
(change)="verifySelection($event)">
<option value=''>None</option>
<option value='a'>Something A</option>
<option value='b'>Something B</option>
</select>
<br>
<div *ngIf="!optionSelected">
None of the above? Specify:
<input type="text" formControlName="aditional">
</div>
<br>
<br>
Form current status: {‌{formStatus}}
</form>
Code:
export class AppComponent {
whateverForm: FormGroup;
formStatus: string;
optionSelected = false;
ngOnInit() {
this.whateverForm = new FormGroup({
'option': new FormControl(null, [Validators.required]),
'aditional': new FormControl(null, [Validators.required])
});
this.whateverForm.statusChanges.subscribe(
(status) => {
this.formStatus = status;
}
);
}
verifySelection(event: any) {
if (event.target.value !== '') {
this.optionSelected = true;
this.whateverForm.get('aditional').clearValidators();
this.whateverForm.get('option').setValidators(
[Validators.required]);
} else {
this.optionSelected = false;
this.whateverForm.get('option').clearValidators();
this.whateverForm.get('aditional').setValidators(
[Validators.required]);
}
}
}
Instead of using an event, I used an observable in one of the fields. The exact solution to the problem I proposed is here.
And I solved it using what I found here (they are basically the same thing, but I included mine for completion).

HTML file input — "accept" attribute not working

After reading through the comments on this post, I came up with the following syntax for the accept attribute:
Images
<input type="file" accept="image/jpeg, image/png, image/gif, .jpeg, .png, .gif">
Audio
<input type="file" accept="audio/mpeg, audio/x-wav, .mp3, .wav">
This works perfectly on desktop browsers, but does not appear to filter files at all on iOS or Android.
Are there any cross-browser solutions available?
I was unable to get the accept attribute to work for mobile. Ultimately I had to add an onchange handler to the input (general idea shown below).
Keep in mind, you'll still want to use the accept attribute as shown in my question, because it will work on desktop.
const supportedExtensions = ['jpeg', 'jpg', 'png', 'gif'];
const handleChange = ({ target }) => {
const path = target.value.split('.');
const extension = `${path[path.length - 1]}`;
if (supportedExtensions.includes(extension)) {
// TODO: upload
} else {
// TODO: show "invalid file type" message to user
// reset value
target.value = '';
}
}
The detail listing of browser support for "accept" attribute is listed in w3 schools. Have a look. It may help you.
I got the same problem, found this page, here is my workaround using onChange event.
I know this isn't true filtering and this is pretty ugly (I don't it), but it works indeed. I tested on my iOS and Android devices.
<script type="text/javascript">
let file;
function checkFile() {
file = document.querySelector('input[type=file]').files[0];
if (file.type != "image/png") {
file = null;
document.getElementById('image').remove();
let div = document.getElementById('div');
let image = document.createElement('input');
image.setAttribute('type', 'file');
image.setAttribute('id', 'image');
image.setAttribute('accept', 'image/png');
image.setAttribute('onchange', 'checkFile()');
div.appendChild(image);
window.alert('unsupported file type');
}
}
</script>
<div id="div">
<input type="file" id="image" accept="image/png" onchange="checkFile()">
</div>

accessing angularjs textbox value in a controller

I am trying to learn AngularJS and require help in passing user entered text box text value after button click to append to a string url value while calling the http service.
I'm trying to add in the following way but it is showing me a value of undefined while appending the URl with the user entered text from the text box.
Here is my HtmlPage1.html
<form ng-submit="abc(inputValue)">
<input type="text" name="name" ng-model="inputValue" />
<button type="submit">Test</button>
</form>
and my script file Script.js
var app = angular.module("repos", [])
.controller("reposController", function ($scope, $http, $log) {
$scope.inputValue = null;
$scope.abc = function (value) {
$scope.inputValue = value;
};
$http({
method:'GET',
url: 'https://api.github.com/users/'+$scope.inputValue+'/repos'
})
.then(function (response) {
$scope.repos = response.data;
$log.info(response);
});
});
Can anyone help me in this regard on how to get the right value that the user has entered to appended to the URL?
Thanks in advance.
Your get call is placed before you enter any value. In order to call the API with inputValue, place the get call inside the button click.
Also, you do not have to pass the inputValue into the function from HTML, Angular's 2 way binding will do the job for you.
Ex:
HTML
<form ng-submit="abc()">
<input type="text" name="name" ng-model="inputValue" />
<button type="submit">Test</button>
</form>
JS:
var app = angular.module("repos", [])
.controller("reposController", function ($scope, $http, $log) {
$scope.inputValue = null;
$scope.abc = function () {
$log.info($scope.inputValue) // you will have your updated value here
$http({
method:'GET',
url: 'https://api.github.com/users/'+$scope.inputValue+'/repos'
})
.then(function (response) {
$scope.repos = response.data;
$log.info(response);
});
});
};
I hope this helps.
Just remember that you have the code on your controller thanks to 2 way binding.
There you will set up an object for models. Ad later you can use them to submit data.
In order for you to understand what I am trying to explain I made an example, I hope it Helps
In your code:
Set the ng-model on the input tag
<input type="text" name="name" ng-model="vm.data.inputValue" />
On your controller make it available as in my example
vm.data ={};
Then use a function to send it using ng-click.
<button type="submit" ng-click="vm.submit()">Test</button>
I am sure there are more ways to do this.
I am not that good, explaining so I made an example, that I hope helps:
https://jsfiddle.net/moplin/r0vda86d/
my example is basically the same but I prefer not to use $scope.

how to provide a external (Serverside)search box to our Kendo UI grid?

how to provide a external search box to our Kendo UI grid that Search in Sever Side?
function onSearch()
{
var q = $("#txtSearchString").val();
var grid = $("#kGrid").data("kendoGrid");
grid.dataSource.query({
page:1,
pageSize:20,
filter:{
logic:"or",
filters:[
{field:"ID", operator:"contains",value:q},
{field:"Title", operator:"contains",value:q},
]
}
});
}
$("#btnSearch").kendoButton({
click:onSearch
})
$("#kGrid").kendoGrid({
dataSource:{
type:"odata",
transport:{
read:"ContactType/GetContactTypes"
},
schema:{
data:function(data){
return data.d.results
},
total:function(data){
return data.d.__count
},
},
serverPaging:true,
serverFiltering:true,
pageSize:20
},
height:550,
pageable:true,
columns:[
'CustomerID',
'CompanyName',
'ContactName',
'ContactTitle',
'Address',
'City',
'PostalCode',
'Country'
]
})
}
$(document).ready(onReady);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://cdn.kendostatic.com/2014.2.903/js/kendo.all.min.js"></script>
</head>
<body>
<div>
<div>
<div>
Search By ID/Title
<input class=k-textbox type=text id="txtSearchString" placeholder="enter search text..." />
<button id="btnSearch">Search</button>
</div>
<br><br>
<div id="kGrid"></div>
</div>
</div>
</body>
</html>
public ActionResult GetContactTypes()
{
using (var context = CommerceChamberContext.GetContext())
{
var data = context.ContactTypes.Select(x => new { x.ID, x.Title }).ToList();
int count = data.Count;
var jsonData = new { total = count, data };
return Json(jsonData, JsonRequestBehavior.AllowGet);
}
}
I have this code, but it does not Filter anything...
I like to have a text box and a button to initiate the search in server side.
You aren't doing anything with the query parameters in your server action(GetContactTypes).
If you look at what is posted if the browser dev tools when you click your Search button you will see something like this in the query string parameters:
$callback:jQuery112305717723066421754_1473786887565
$inlinecount:allpages
$format:json
$top:20
$filter:(substringof('SearchForThis',ID) or substringof('SearchForThis',Title))
Because you have configured the grid to use serverFiltering: true, it is your responsibility to incorporate the $filter(and $top, and $skip) parameter into your query with an appropriate WHERE clause that uses the passed $filter values.
As it stands right now, your server action is being passed the filter but you are returning your entire ContactTypes list.
Do you want to use client-side filtering?
Server filtering = you do the filtering on the server and return only the matching rows.
Client filtering = kendo will do the filtering in javascript with the dataSource it already has(will not call your server action)

Using PUT method in HTML form

Can I use a PUT method in an HTML form to send data from the form to a server?
According to the HTML standard, you can not. The only valid values for the method attribute are get and post, corresponding to the GET and POST HTTP methods. <form method="put"> is invalid HTML and will be treated like <form>, i.e. send a GET request.
Instead, many frameworks simply use a POST parameter to tunnel the HTTP method:
<form method="post" ...>
<input type="hidden" name="_method" value="put" />
...
Of course, this requires server-side unwrapping.
XHTML 1.x forms only support GET and POST. GET and POST are the only allowed values for
the "method" attribute.
Can I use "Put" method in html form to send data from HTML Form to server?
Yes you can, but keep in mind that it will not result in a PUT but a GET request. If you use an invalid value for the method attribute of the <form> tag, the browser will use the default value get.
HTML forms (up to HTML version 4 (, 5 Draft) and XHTML 1) only support GET and POST as HTTP request methods. A workaround for this is to tunnel other methods through POST by using a hidden form field which is read by the server and the request dispatched accordingly. XHTML 2.0 once planned to support GET, POST, PUT and DELETE for forms, but it's going into XHTML5 of HTML5, which does not plan to support PUT. [update to]
You can alternatively offer a form, but instead of submitting it, create and fire a XMLHttpRequest using the PUT method with JavaScript.
_method hidden field workaround
The following simple technique is used by a few web frameworks:
add a hidden _method parameter to any form that is not GET or POST:
<input type="hidden" name="_method" value="PUT">
This can be done automatically in frameworks through the HTML creation helper method.
fix the actual form method to POST (<form method="post")
processes _method on the server and do exactly as if that method had been sent instead of the actual POST
You can achieve this in:
Rails: form_tag
Laravel: #method("PATCH")
Rationale / history of why it is not possible in pure HTML: https://softwareengineering.stackexchange.com/questions/114156/why-there-are-no-put-and-delete-methods-in-html-forms
for people using laravel
<form method="post" ...>
#csrf
#method('put')
...
</form>
Unfortunately, modern browsers do not provide native support for HTTP PUT requests. To work around this limitation, ensure your HTML form’s method attribute is “post”, then add a method override parameter to your HTML form like this:
<input type="hidden" name="_METHOD" value="PUT"/>
To test your requests you can use "Postman" a google chrome extension
To set methods PUT and DELETE I perform as following:
<form
method="PUT"
action="domain/route/param?query=value"
>
<input type="hidden" name="delete_id" value="1" />
<input type="hidden" name="put_id" value="1" />
<input type="text" name="put_name" value="content_or_not" />
<div>
<button name="update_data">Save changes</button>
<button name="remove_data">Remove</button>
</div>
</form>
<hr>
<form
method="DELETE"
action="domain/route/param?query=value"
>
<input type="hidden" name="delete_id" value="1" />
<input type="text" name="delete_name" value="content_or_not" />
<button name="delete_data">Remove item</button>
</form>
Then JS acts to perform the desired methods:
<script>
var putMethod = ( event ) => {
// Prevent redirection of Form Click
event.preventDefault();
var target = event.target;
while ( target.tagName != "FORM" ) {
target = target.parentElement;
} // While the target is not te FORM tag, it looks for the parent element
// The action attribute provides the request URL
var url = target.getAttribute( "action" );
// Collect Form Data by prefix "put_" on name attribute
var bodyForm = target.querySelectorAll( "[name^=put_]");
var body = {};
bodyForm.forEach( element => {
// I used split to separate prefix from worth name attribute
var nameArray = element.getAttribute( "name" ).split( "_" );
var name = nameArray[ nameArray.length - 1 ];
if ( element.tagName != "TEXTAREA" ) {
var value = element.getAttribute( "value" );
} else {
// if element is textarea, value attribute may return null or undefined
var value = element.innerHTML;
}
// all elements with name="put_*" has value registered in body object
body[ name ] = value;
} );
var xhr = new XMLHttpRequest();
xhr.open( "PUT", url );
xhr.setRequestHeader( "Content-Type", "application/json" );
xhr.onload = () => {
if ( xhr.status === 200 ) {
// reload() uses cache, reload( true ) force no-cache. I reload the page to make "redirects normal effect" of HTML form when submit. You can manipulate DOM instead.
location.reload( true );
} else {
console.log( xhr.status, xhr.responseText );
}
}
xhr.send( body );
}
var deleteMethod = ( event ) => {
event.preventDefault();
var confirm = window.confirm( "Certeza em deletar este conteúdo?" );
if ( confirm ) {
var target = event.target;
while ( target.tagName != "FORM" ) {
target = target.parentElement;
}
var url = target.getAttribute( "action" );
var xhr = new XMLHttpRequest();
xhr.open( "DELETE", url );
xhr.setRequestHeader( "Content-Type", "application/json" );
xhr.onload = () => {
if ( xhr.status === 200 ) {
location.reload( true );
console.log( xhr.responseText );
} else {
console.log( xhr.status, xhr.responseText );
}
}
xhr.send();
}
}
</script>
With these functions defined, I add a event listener to the buttons which make the form method request:
<script>
document.querySelectorAll( "[name=update_data], [name=delete_data]" ).forEach( element => {
var button = element;
var form = element;
while ( form.tagName != "FORM" ) {
form = form.parentElement;
}
var method = form.getAttribute( "method" );
if ( method == "PUT" ) {
button.addEventListener( "click", putMethod );
}
if ( method == "DELETE" ) {
button.addEventListener( "click", deleteMethod );
}
} );
</script>
And for the remove button on the PUT form:
<script>
document.querySelectorAll( "[name=remove_data]" ).forEach( element => {
var button = element;
button.addEventListener( "click", deleteMethod );
</script>
_ - - - - - - - - - - -
This article https://blog.garstasio.com/you-dont-need-jquery/ajax/ helps me a lot!
Beyond this, you can set postMethod function and getMethod to handle POST and GET submit methods as you like instead browser default behavior. You can do whatever you want instead use location.reload(), like show message of successful changes or successful deletion.
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
JSFiddle: https://jsfiddle.net/enriquerene/d6jvw52t/53/
If you are using nodejs, you can install the package method-override that lets you do this using a middleware.
Link to documentation: http://expressjs.com/en/resources/middleware/method-override.html
After installing this, all I had to do was the following:
var methodOverride = require('method-override')
app.use(methodOverride('_method'))
I wrote an npm package called 'html-form-enhancer'. By dropping it into your HTML source, it takes over submission of forms with methods aside from GET and POST, and also adds application/json serialization.
<script type=module" src="html-form-enhancer.js"></script>
<form method="PUT">
...
</form>
In simple words - No.
I have tried to fire a put request in the HTML form, but it sends the POST request to the server. To add the PUT request -
We can do it by listening to the submit action in the script, then fire the put request to a particular endpoint.
Screenshot from the http-server env. test